Skip to content

feat(edit-content): fill the column, clear on the field, and rebuild the picker footer (#37465) - #37555

Merged
adrianjm-dotCMS merged 11 commits into
mainfrom
issue-37465-calendar-field-width-clear-picker-footer-impl
Sep 18, 2026
Merged

adrianjm-dotCMS merged 11 commits into
mainfrom
issue-37465-calendar-field-width-clear-picker-footer-impl

Conversation

@adrianjm-dotCMS

@adrianjm-dotCMS adrianjm-dotCMS commented Sep 15, 2026

Copy link
Copy Markdown
Member

PR 2 of 2 — implementation

Fixes #37465

Base is the spec branch, not main. PR 1 (#37542) carries spec.md and is still awaiting
approval, so this branches off it rather than waiting for it to merge — the
Spec-Kit flow gates on
approval, not on merge. Rebase onto main once PR 1 lands.

Screenshot 2026-09-15 at 6 28 38 PM Screenshot 2026-09-15 at 6 28 14 PM Screenshot 2026-09-15 at 6 28 04 PM Screenshot 2026-09-15 at 6 28 00 PM Screenshot 2026-09-15 at 6 27 52 PM

What changed

Date, Time and Date-and-time were the only fields in the new Edit Contentlet that behaved unlike
their neighbours — narrower than their column, impossible to empty once set, and carrying PrimeNG's
stock picker footer with a redundant Clear and a Today that read the browser's clock.

Width Block-level flex at full width; the input grows, the trigger stays flush right, and the focus ring and invalid border still enclose both as one unit
Clearing showClear is unconditional, so every type can be emptied — not just the expire-date field
Picker footer Rebuilt through #buttonbar: timezone left (on the two types carrying a time), a single secondary-outlined Today / Now right, PrimeNG's Clear gone
Today / Now Resolves from the server clock, and leaves the picker open on all three types — the same as selecting a day or time
Hint The timezone line under the input is gone; the hint returns to the field footer, where every other field type puts it

8 files: 6 modified, 1 new spec, plus three keys in Language.properties.

Two things worth a reviewer's attention

1. PrimeNG's supplied todayCallback is deliberately unused. It opens with const date = new Date()
— the browser's clock, which is the defect FR-013 exists to correct. Wiring the footer button to it
would look right, pass a naive test, and reintroduce the bug. The button goes through
getCurrentServerTime and the existing onCalendarChange conversion instead, so there is still only
one conversion path.

Verified in the browser against a running instance: server on UTC, browser on UTC-4 — Today set
15:57, not 11:57.

2. PrimeNG's default clear icon is a bare <svg> with a click handler — unfocusable, no
accessible name, unreachable by keyboard. A real <button> is projected through #clearicon. The
accessibility tree now announces all three as button "Clear".

A defect the test suite could not have caught

On reopening saved content, no field showed a clear control until the author happened to focus it
— a value with no way to clear it. PrimeNG gates that control on a DOM read of
inputfieldViewChild.nativeElement.value, which updateInputfield() writes without markForCheck();
the DatePicker is OnPush, so on the load path the condition is evaluated before the value lands and
nothing re-evaluates it. An effect now schedules the pass in a microtask.

It is worth being explicit about why the suite was green over it: the settle() test helper had been
calling the DatePicker's own change detector — doing in the test what the component failed to do in
production. It now runs only the host's detection plus a microtask turn, the same turn the browser
gives it, so these tests can fail for this cause again.

Found by manual verification in Chrome against a running dotCMS.

Testing

pnpm nx test edit-content2390 passing, 117 files. pnpm nx lint edit-content — 0 errors
(3 pre-existing warnings in untouched files).

Written test-first throughout, with developer approval and confirmed-Red gates per Constitution
Principle V. Two exceptions were declared and signed off rather than left silent:

  • No automated layout test — jsdom computes no layout, so a unit test could only assert that a CSS declaration exists, which restates the implementation and would pass even if the control rendered wrongly. Covered by manual verification instead.
  • No end-to-end cross-timezone test — no e2e coverage exists for date fields, and a faithful test needs a server far enough from the runner to cross midnight. The unit test guards the same logic with a faked clock and an offset timezone.

Manually verified in Chrome against a running instance, on throwaway content types since deleted:
field widths identical to a sibling Text field in both single- and two-column layouts (721px / 543px),
input-to-trigger gap 0px, one focus ring over the whole unit, error border over input and trigger,
all three footers correct, and a full save/reopen round-trip confirming the stored shapes are
unchanged — Date-only at UTC midnight, Time-only against a consistent date base, Date-and-time as a
UTC instant. That manual pass predates the last commit, which keeps the picker open after Today on
Date-only fields; that change is covered by unit tests.

🤖 Generated with Claude Code

This PR fixes: #37465

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @adrianjm-dotCMS's task in 2m 26s —— View job


Code Review — PR #37555 (latest push)

  • Read the current state of the changed files
  • Verify prior review findings were addressed
  • Check for new issues in the latest commits
  • Post review

I reviewed the diff against origin/main and cross-checked every prior review thread against the current code.

New Issues

  • 🟡 Medium: calendar-field.component.ts:154/192Assumption: the clear control's one-shot afterNextRender fix covers only the first render. If $systemTimezone() resolves after the field has already rendered with a saved value, the effect at line 154 re-runs processExistingValue and calls internalFormControl.setValue(...), but nothing re-runs the picker's clear-control condition (PrimeNG's markForCheck on an already-refreshed OnPush view only lands next cycle, and afterNextRender has already fired). That reproduces the exact "value shown with no way to clear it" symptom this PR fixes, in a narrower path. The code comment itself flags this trade-off ("If a later write ever turns out to need this too, replace it with an effect"). What to verify: whether systemConfig().systemTimezone can ever arrive after this field's first render (e.g. slow config load, language/version switch that re-patches values without re-creating the view). If it always resolves before render, this is a non-issue; if not, the reprocess effect needs the same forced detection the load path gets. Fix this →

Resolved

  • calendar-field.component.html:55 — overlay keyboard handling restored via onFooterButtonKeydownonContainerButtonKeydown (Escape/Tab focus trap).
  • calendar-field.component.ts:169-192 — render-race comment corrected (cites :3322/:3262, states markForCheck is present), switched to afterNextRender; the destroy-guard concern is moot since callbacks are torn down on destroy.
  • calendar-field.component.html:21[fluid]="true" replaces the hand-rolled full-width SCSS; the four duplicating declarations and min-width: 0 are gone.
  • calendar-field.component.html:15 — dead (onClearClick) binding removed; only (onClear) remains.
  • calendar-field.component.scss::ng-deep .calendar-field__clear block removed; chrome moved to Tailwind utilities on the projected <button>.
  • calendar-field.component.html:80 — clear glyph is now a Material Symbol (close), matching the rest of edit-content.
  • calendar-field.component.html:43 — timezone text uses text-gray-500, matching the field hint.
  • calendar-field.component.html:44$systemTimezone()?.label (safe navigation) for the strict typecheck gate.
  • calendar-field.component.html:53 — footer action disabled via [disabled]="!$canResolveServerTime()" until a server timezone id resolves, so it can no longer persist a browser-clock instant; UTC servers stay enabled.
  • dot-edit-content-calendar-field.component.ts / calendar-field.component.tsChangeDetectionStrategy.OnPush and its import dropped (framework default).
  • dot-edit-content-calendar-field.component.spec.ts — the spectator.query(DatePicker).cd.detectChanges() anti-pattern is gone (now the microtask settle() shape).
  • calendar-field.component.spec.ts — timezone-unavailable cases added, Time-only assertion now checks the exact stored instant, queryActionButton fallback dropped, stale/contradictory comments removed, vestigial async removed, openPicker() exemption documented.

Everything else in the diff (the stacked hint/error markup with its documented temporary utilities, the three Language.properties keys, the accessible-name id list) is consistent with the reasoning in the threads and the repo conventions. No security, replay-safety, or error-path concerns in scope — this is client-side presentation with no new network or data access.

Net: the blocking findings from the earlier reviews are all resolved, and oidacra has approved. The single item above is a non-blocking edge to confirm.

@adrianjm-dotCMS
adrianjm-dotCMS marked this pull request as ready for review September 15, 2026 16:21
@adrianjm-dotCMS
adrianjm-dotCMS added this pull request to stack #37557 September 15, 2026 18:39
@github-actions github-actions Bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Sep 15, 2026

@nicobytes nicobytes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — five axes (correctness, readability, architecture, security, performance)

The reasoning in the description is unusually good, and two things are genuinely well done: routing Today/Now through getCurrentServerTime instead of PrimeNG's todayCallback is the right call and is pinned by a test that fakes the clock and sits on an offset zone, so it can fail again for the original cause; and rewriting settle() so it no longer runs the DatePicker's own detector is the kind of test fix that costs you a green build today and saves you a production bug later. I checked the disabled case against PrimeNG rather than assuming: showClear && !$disabled() && inputfieldViewChild?.nativeElement?.value (primeng/datepicker 21.1.3), so making showClear unconditional cannot expose a clear control on a read-only field — and there is a test for it. No security or performance concerns in scope; no untrusted input, no new network or data access.

Three things I'd want addressed before merge, all verified against the installed PrimeNG 21.1.3 source rather than inferred:

  1. Keyboard handling is lost in the new picker footer. The custom <p-button> does not wire onContainerButtonKeydown, which is what implements Escape and the Tab focus trap inside the overlay. Details inline.
  2. The comment explaining the OnPush workaround states a cause that is not true in 21.1.3. writeControlValue() does call cd.markForCheck(). The symptom is real; the mechanism is a view-query race on the first render. That distinction changes both the right hook and how narrow the fix can be. Details inline.
  3. The forced detectChanges() runs in a microtask with no destroy guard, and field components live under @if (...) in the form template. Details inline.

Plus one architecture cleanup worth taking: PrimeNG already ships the full-width behaviour as [fluid], and the sibling tag field in this same feature already uses it — the ::ng-deep block re-implements it.

The rest are small: one dead output binding, one test that re-introduces the anti-pattern the new settle() was written to remove, and three stale comments.

Not blocking, noted for the record

  • dot-edit-content-custom-field.component.html:33 still carries data-testid="calendar-field-hint", a copy-paste leftover from this field. Not this PR's job, but the id is free now.
  • Dropping the console.warn on non-number values is correct per the repo's Critical Rules, and $value is typed number | null, so nothing is being silently swallowed.
  • The two declared test exceptions (no jsdom layout test, no cross-timezone e2e) are the right calls and are argued rather than hidden.

adrianjm-dotCMS added a commit that referenced this pull request Sep 17, 2026
…render-race fix (#37465)

Three of nicobytes' findings on PR #37555, each verified against primeng
21.1.3 before changing anything.

Escape and Tab regressed inside the picker. Every control PrimeNG renders in
the panel is wired with (keydown)="onContainerButtonKeydown($event)" —
including the two stock footer buttons this feature replaces — and the panel
root binds only (click), so that per-control binding is the only place the
overlay's keyboard behaviour lives: Escape returns focus to the input and
closes the overlay (:1793), Tab traps focus (:1776, focusTrap defaults to
true). The replacement footer did not forward the event, so Escape did
nothing and Tab walked straight out of the body-appended overlay — in a change
whose stated goal includes accessibility. onFooterButtonKeydown now forwards
it through the viewChild the component already held.

The comment on the clear-control workaround named the wrong cause. It said
updateInputfield() writes the DOM value "without marking the component dirty";
writeControlValue does call cd.markForCheck() (:3262), so on the next PrimeNG
bump someone would have checked for a missing markForCheck, found it present,
and concluded the workaround was obsolete. The real mechanism is a view-query
race: the clear-control condition reads inputfieldViewChild?.nativeElement
?.value (:3322) and that query is non-static, so it is unresolved when the
condition is first evaluated. The comment now says so and cites both lines.

That also changes the fix. A first-render race is what afterNextRender is
for: it runs once instead of on every $value() change, and it is torn down
with the component, which removes the second finding — the effect's microtask
could fire detectChanges() against a destroyed ChangeDetectorRef, reachable
because the field sits under an @if that a single store update can tear down
in the same task.

Both fixes are pinned by tests that were confirmed to fail without them:
removing the keydown binding reds the new Escape test, and disabling
afterNextRender reds eight clear-control tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adrianjm-dotCMS added a commit that referenced this pull request Sep 17, 2026
…ce, and cleanups (#37465)

Eighteen findings from nicobytes and oidacra. Every claim about PrimeNG was
checked against primeng 21.1.3 before changing anything, and each behavioural
fix is pinned by a test confirmed to fail without it.

Behaviour:

- Escape and Tab regressed inside the picker. PrimeNG wires every control it
  renders in the panel with (keydown)="onContainerButtonKeydown($event)" —
  including the stock footer buttons this feature replaces — and the panel root
  binds only (click), so that per-control binding is the only place the
  overlay's keyboard handling lives: Escape returns focus to the input and
  closes (:1793), Tab traps focus (:1776). The replacement footer did not
  forward it. onFooterButtonKeydown does. Removing the binding reds the new
  Escape test.

- The clear-control workaround named the wrong cause. writeControlValue does
  call cd.markForCheck() (:3262); the real mechanism is a view-query race —
  the condition reads inputfieldViewChild?.nativeElement?.value (:3322) and
  that query is non-static, so it is unresolved on the first pass. The comment
  now says so and cites both lines, so the next PrimeNG bump checks the right
  thing. That also changes the fix: afterNextRender matches a first-render
  race, runs once instead of on every $value() change, and is torn down with
  the component — which removes the second finding, an unguarded microtask
  that could call detectChanges() on a destroyed view. Disabling it reds eight
  tests.

- The Today/Now button stayed clickable while the timezone label was hidden
  for not having resolved. getCurrentServerTime then falls back to reading the
  browser's UTC components as local time — harmless while its only caller was
  $defaultDate, which merely positions the calendar, but this is the first
  call site that persists the result. It is now disabled until the timezone
  resolves; a server explicitly on UTC stays enabled, since there the branch is
  correct rather than a fallback.

- (onClearClick) could no longer fire: PrimeNG emits it only from
  onClearButtonClick (:3129), reachable from the stock Clear button this
  feature removes or from the buttonbar clearCallback we do not use. The
  on-field X goes through clear(), which emits onClear (:1746). The binding and
  its three tests are gone — those tests drove the event synthetically with
  triggerEventHandler, so they asserted a path production cannot take.

Simplifications: [fluid] replaces seven lines of SCSS that re-implemented it
(verified in the browser: p-datepicker-fluid and p-inputtext-fluid land, all
four fields measure 520px across two columns, focus ring and invalid border
still enclose input and trigger); the clear button's chrome moves to Tailwind
since it is our own node, not a PrimeNG internal; Material Symbols replaces
PrimeIcons; one gray scale; optional chaining on the timezone label; explicit
OnPush dropped from both components.

Tests: three cases for the timezone-unavailable state, which nothing covered;
the TIME assertion now checks the stored instant rather than only its presence;
the parent spec no longer drives the DatePicker's own detector, which is what
masked the load-path bug; the ?? fallback, two stale docblocks and five
vestigial asyncs are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from issue-37465-calendar-field-width-clear-picker-footer to main September 18, 2026 15:37
adrianjm-dotCMS and others added 11 commits September 18, 2026 15:37
…the picker footer (#37465)

Date, Time and Date-and-time were the only fields in the new Edit Contentlet
that behaved unlike their neighbours: narrower than their column, impossible
to empty once set, and carrying a picker footer that offered a redundant
Clear while hiding the timezone the value is read in.

- Width: the control is block-level flex at full width; the input grows and
  the trigger stays flush right, with the focus ring and the invalid border
  still enclosing both as one unit.
- Clearing: showClear is unconditional, so every type can be emptied, not
  just the expire-date field. PrimeNG's default clear icon is a bare <svg>
  with a click handler — unfocusable and unnamed — so a real <button> is
  projected through #clearicon instead.
- Picker footer: rebuilt through #buttonbar. The timezone reads on the left
  for the two types that carry a time; a single secondary-outlined Today
  (Now for time-only) sits on the right; PrimeNG's Clear is gone.
- Today/Now resolves from the SERVER clock via getCurrentServerTime and the
  existing onCalendarChange conversion. PrimeNG's supplied todayCallback is
  deliberately unused: it reads new Date(), the browser's clock, which is the
  defect this corrects. Verified in the browser with the server on UTC and the
  browser on UTC-4 — the field took 15:57, not 11:57.
- The timezone line under the input is gone and the hint returns to the field
  footer, as it renders for every other field type.

Two behaviours beyond the issue's scope, accepted deliberately to reduce debt
in a file already being touched: the required error no longer evicts the hint
(they stack, error first), and both carry their colours directly, because
.form .p-field-error in style.css never applies — the new editor has no .form
ancestor. Both belong to #37464 / #37460 and here cover the calendar field
only, not the other ~15 field types.

One defect was found by manual verification and would not have been found by
the suite: on reopening saved content no field showed a clear control until
the author focused it. PrimeNG gates that control on a DOM read that
updateInputfield() performs without markForCheck(), and the DatePicker is
OnPush. An effect now schedules the pass in a microtask. The test helper had
been masking it by running the DatePicker's own detector — doing in the test
what the component failed to do in production — and no longer does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng labels (#37465)

Review on PR #37542 surfaced a real bug: getCurrentServerTime(null) returned
UTC clock components reinterpreted as local, while every other timezone-less
path in this field (convertServerTimeToUtc, convertUtcToServerTime) treats a
missing zone as "use the browser's local clock as-is". The mismatch meant
Today/Now displayed one time and stored another, off by the browser's UTC
offset — permanently, if the timezone request fails outright rather than
merely arriving late.

Falls back to the browser's own local clock instead, matching the convention
already used elsewhere. No change to the conversion helpers themselves, so
FR-017's storage guarantees are untouched.

Also: the footer's timezone text now truncates (min-w-0 + truncate, with a
title attribute carrying the full label) rather than being able to push the
Today/Now button out of the overlay on a very long zone name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… spec

- Today/Now no longer falls back to the browser's clock when the system
  timezone is missing. getCurrentServerTime is restored to its previous
  implementation, so the button always goes through the existing
  getCurrentServerTime / convertServerTimeToUtc path, as the issue's
  acceptance criterion requires. The test asserting the fallback is
  removed.
- Today/Now no longer closes the picker on Date-only fields. The field
  sets hideOnDateTimeSelect to false for all three types, so selecting a
  day already keeps the picker open; the shortcut now behaves the same.
  The test covers all three types.
- The footer timezone label drops its truncation and tooltip; the label
  is always short enough for the picker.
- Removes the test for the timezone-unavailable footer state, which the
  spec no longer describes. The @if guard stays: it prevents reading
  .label on null and implements the Date-only rule (FR-008a).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…37465)

CI's "Frontend Unit Tests" job failed on its format-test goal, which runs
nx format:check. The tests themselves passed; two files were unformatted:

- calendar-field.component.spec.ts: two stray blank lines.
- dot-edit-content-calendar-field.component.html: Tailwind class order,
  which prettier-plugin-tailwindcss sorts.

Both came in through edits whose commits staged a different file set, so
lint-staged's format:write never saw them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…render-race fix (#37465)

Three of nicobytes' findings on PR #37555, each verified against primeng
21.1.3 before changing anything.

Escape and Tab regressed inside the picker. Every control PrimeNG renders in
the panel is wired with (keydown)="onContainerButtonKeydown($event)" —
including the two stock footer buttons this feature replaces — and the panel
root binds only (click), so that per-control binding is the only place the
overlay's keyboard behaviour lives: Escape returns focus to the input and
closes the overlay (:1793), Tab traps focus (:1776, focusTrap defaults to
true). The replacement footer did not forward the event, so Escape did
nothing and Tab walked straight out of the body-appended overlay — in a change
whose stated goal includes accessibility. onFooterButtonKeydown now forwards
it through the viewChild the component already held.

The comment on the clear-control workaround named the wrong cause. It said
updateInputfield() writes the DOM value "without marking the component dirty";
writeControlValue does call cd.markForCheck() (:3262), so on the next PrimeNG
bump someone would have checked for a missing markForCheck, found it present,
and concluded the workaround was obsolete. The real mechanism is a view-query
race: the clear-control condition reads inputfieldViewChild?.nativeElement
?.value (:3322) and that query is non-static, so it is unresolved when the
condition is first evaluated. The comment now says so and cites both lines.

That also changes the fix. A first-render race is what afterNextRender is
for: it runs once instead of on every $value() change, and it is torn down
with the component, which removes the second finding — the effect's microtask
could fire detectChanges() against a destroyed ChangeDetectorRef, reachable
because the field sits under an @if that a single store update can tear down
in the same task.

Both fixes are pinned by tests that were confirmed to fail without them:
removing the keydown binding reds the new Escape test, and disabling
afterNextRender reds eight clear-control tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce, and cleanups (#37465)

Eighteen findings from nicobytes and oidacra. Every claim about PrimeNG was
checked against primeng 21.1.3 before changing anything, and each behavioural
fix is pinned by a test confirmed to fail without it.

Behaviour:

- Escape and Tab regressed inside the picker. PrimeNG wires every control it
  renders in the panel with (keydown)="onContainerButtonKeydown($event)" —
  including the stock footer buttons this feature replaces — and the panel root
  binds only (click), so that per-control binding is the only place the
  overlay's keyboard handling lives: Escape returns focus to the input and
  closes (:1793), Tab traps focus (:1776). The replacement footer did not
  forward it. onFooterButtonKeydown does. Removing the binding reds the new
  Escape test.

- The clear-control workaround named the wrong cause. writeControlValue does
  call cd.markForCheck() (:3262); the real mechanism is a view-query race —
  the condition reads inputfieldViewChild?.nativeElement?.value (:3322) and
  that query is non-static, so it is unresolved on the first pass. The comment
  now says so and cites both lines, so the next PrimeNG bump checks the right
  thing. That also changes the fix: afterNextRender matches a first-render
  race, runs once instead of on every $value() change, and is torn down with
  the component — which removes the second finding, an unguarded microtask
  that could call detectChanges() on a destroyed view. Disabling it reds eight
  tests.

- The Today/Now button stayed clickable while the timezone label was hidden
  for not having resolved. getCurrentServerTime then falls back to reading the
  browser's UTC components as local time — harmless while its only caller was
  $defaultDate, which merely positions the calendar, but this is the first
  call site that persists the result. It is now disabled until the timezone
  resolves; a server explicitly on UTC stays enabled, since there the branch is
  correct rather than a fallback.

- (onClearClick) could no longer fire: PrimeNG emits it only from
  onClearButtonClick (:3129), reachable from the stock Clear button this
  feature removes or from the buttonbar clearCallback we do not use. The
  on-field X goes through clear(), which emits onClear (:1746). The binding and
  its three tests are gone — those tests drove the event synthetically with
  triggerEventHandler, so they asserted a path production cannot take.

Simplifications: [fluid] replaces seven lines of SCSS that re-implemented it
(verified in the browser: p-datepicker-fluid and p-inputtext-fluid land, all
four fields measure 520px across two columns, focus ring and invalid border
still enclose input and trigger); the clear button's chrome moves to Tailwind
since it is our own node, not a PrimeNG internal; Material Symbols replaces
PrimeIcons; one gray scale; optional chaining on the timezone label; explicit
OnPush dropped from both components.

Tests: three cases for the timezone-unavailable state, which nothing covered;
the TIME assertion now checks the stored instant rather than only its presence;
the parent spec no longer drives the DatePicker's own detector, which is what
masked the load-path bug; the ?? fallback, two stale docblocks and five
vestigial asyncs are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…imezone (#37465)

Implements FR-017 and FR-008b, the two requirements added to the spec after
nicobytes' review of PR #37542. Both were specified there and left for this
branch; without them the implementation does not satisfy its own spec.

FR-017 — a clear now survives save and reopen. handleChangeValue received null
both when a field had never held a value and when the author cleared it and
saved, and re-applied processFieldDefaultValue in both, so on a field with a
`now` or fixed default the clear silently did not survive a reload. The child
now takes the contentlet and gates the default on whether it already exists,
keyed on `inode` — the same signal the store uses to choose between
initializeExistingContent and initializeNewContent. A default belongs to
content being created; on a saved contentlet an empty field is a value the
author chose.

Two parent tests moved with it: they asserted the default applies while
passing a saved contentlet, which is exactly the combination FR-017 rules out.
They now describe content being created, which is what they were really about.

FR-008b — the timezone reaches assistive technology. The rebuilt footer
rendered a plain span, so a screen-reader user learned the zone only by
navigating into the overlay, if at all. A visually-hidden node in the field
carries the same label and is wired through PrimeNG's ariaLabelledBy, which it
forwards to the real <input> (primeng-datepicker.mjs:3298).

Worth recording why it is not aria-describedby, which is what the requirement
would suggest: PrimeNG exposes no ariaDescribedBy input, and setting the
attribute on <p-datepicker> lands it on the host element rather than the input
that takes focus — verified, the attribute was simply absent from the input.
The footer label cannot be the target either: it lives in the overlay, so it
exists only while the picker is open and the reference would dangle the rest
of the time.

All five new tests were confirmed to fail without their implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)

The Frontend Unit Tests job failed on the strict gate, not on a test. It runs
tools/scripts/strict-gate/run.mjs with strictNullChecks on, scoped to the
lines this PR touched, while libs/edit-content sets strict: false — so nothing
local surfaced it.

All of it is in the two spec files:

- Host properties Spectator assigns through hostProps are now `!`-asserted or
  given an initializer (TS2564).
- DotCMSContentlet was missing from the child spec's imports. An earlier edit
  in this branch targeted the pre-Prettier text of that import block and
  silently did nothing; the type had been in use since (TS2552).
- expireDateVar is `string | undefined`, not nullable, so the two
  CONTENT_TYPE_WITHOUT_EXPIRE mocks use undefined (TS2322).
- spectator.query returns `T | null`, so the DatePicker and element reads are
  narrowed (TS18047, TS2531, TS2532).

No production code changed and no assertion was weakened — the narrowing is on
reads the tests already made unconditionally.

Verified with the same command CI runs, plus format:check, lint and the full
suite (2399 passing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#37465)

The FR-008b wiring took the field name away. `aria-labelledby` replaces the
accessible name rather than adding to it, so pointing it at the timezone alone
meant a screen reader announced the input as "Eastern Time (GMT-5)" with no
field name — on exactly the two field types this feature targets.

Verified before fixing, and it was worse than it looked from the diff: the
input's own `aria-label` reads null. The `[attr.aria-label]="field.name"` sat
on `<p-datepicker>`, the host, and PrimeNG does not forward it to the input —
so nothing carried the name to the element that takes focus.

`ariaLabelledBy` now points at an id list: a hidden node with the field name,
then the timezone node when there is one. Both are always in the DOM, unlike
the footer label, which exists only while the overlay is open.

Four tests cover it — the name survives on all three field types and with no
timezone resolved. Confirmed they fail against the previous wiring.

One parent test moved with it: it asserted `aria-label` on the host, which is
the attribute no screen reader reads here. It now resolves the input's
`aria-labelledby` list, which is what a screen reader actually computes.

Also types the CALENDAR_OPTIONS_PER_TYPE lookup as CalendarTypes. The
diff-scoped strict gate flagged it once the edits above pulled that line into
the diff — pre-existing, but ours now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…er conversion (#37465)

This is what failed CI, and it was my own test — not the NG0101 noise that
fills the log. Those recursive-tick errors come from other projects' specs
that pass; the only FAIL was:

  calendar-field.component.spec.ts > should set the server time on a Time-only field
  AssertionError: expected 1775786400000 to be 1775772000000

Four hours apart — exactly my machine's offset. When I strengthened this
assertion for oidacra's review I built the expected instant with a bare
`new Date(year, month, day, hour, ...)`, which reads those components in the
RUNNER's zone, while production routes them through `convertServerTimeToUtc`
with MOCK_TIMEZONE (America/New_York). Locally the two coincide at UTC-4, so
it passed here and failed on CI, which runs UTC.

Now derived through the same conversion the component uses. Verified under
TZ=UTC, America/Caracas, Asia/Tokyo, Europe/Madrid and Pacific/Auckland — the
last one crossing the date line — so the assertion no longer depends on where
it runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…7465)

Regression I introduced swapping PrimeIcons for Material Symbols. Measured in
the browser rather than eyeballed:

  clear glyph   24x24, 5px below the input's centre line
  trigger icon  14x14, centred

One cause behind both symptoms. `.material-symbols-outlined` sets
`font-size: 24px` globally and the plain `text-base` I wrote lost to it on
source order, so the glyph rendered at 24px. PrimeNG's clear slot is
absolutely positioned with `margin-top: -7px`, calibrated for a ~14px icon, so
a 24px glyph no longer lands on the centre line — the misalignment was the
size bug's second symptom, not a separate problem.

`text-base! leading-none!` gives exactly 14x14 — the theme root is 14px, so
`text-base` is 14px here, not 16. The `!` is what beats the global rule and is
noted in the template as load-bearing. `leading-none` keeps the box square;
without it the 21px line-height leaves the glyph 3.5px low.

Verified across all three field types: 14x14 each, 1px off the centre line —
where the stock PrimeIcon sat at 2px, so this is marginally better aligned
than what it replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adrianjm-dotCMS
adrianjm-dotCMS force-pushed the issue-37465-calendar-field-width-clear-picker-footer-impl branch from 0bf3dc5 to 6b54571 Compare September 18, 2026 15:37
@adrianjm-dotCMS
adrianjm-dotCMS added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit b101dd5 Sep 18, 2026
50 checks passed
@adrianjm-dotCMS
adrianjm-dotCMS deleted the issue-37465-calendar-field-width-clear-picker-footer-impl branch September 18, 2026 17:10
adrianjm-dotCMS added a commit that referenced this pull request Sep 18, 2026
Brings in #37192 (the relationship field on the shared search surface) and #37555
(#37465's Date/Time work), both of which landed while this branch was open.

Conflicts, and how each was settled:

- dot-select-existing-content/**/search/ — deleted in main by #37192, which replaced the
  bespoke dialog with the shared search surface. Deletion accepted. What this branch did
  to those four files is moot: the `.form` adoption, the `for="language-field"` →
  `for="site-field"` fix (AC-109) and the language combobox naming (AC-110) all targeted
  markup that no longer exists. No dangling references remain.

- calendar-field.component.{html,scss,spec.ts,ts} and dot-edit-content-calendar-field.component.ts
  — untouched by this branch; took main's merged #37555.

- dot-edit-content-calendar-field.component.{html,spec.ts} — rebuilt on main's version with
  this branch's delta re-applied, keeping the `[contentlet]` input #37555 added. The comment
  main carries there asked for exactly this ("drop the utilities, keeping the semantic
  classes, once either lands").

- host-folder-field.component.{html,ts} — took main's restructuring (#37192 added a projected
  trigger and keyboard activation) and re-applied the accessibility attributes on the DEFAULT
  trigger only. A projected trigger is Content Drive's filter chip, which has no <label for>
  to associate with.

pnpm nx test edit-content: 116 files, 2425 tests, green. The drop from 2459 is the specs
that went with the deleted dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Improve Date, Time, and Date-and-Time field width, clearing, and picker footer in the new Edit Contentlet

3 participants