feat(edit-content): fill the column, clear on the field, and rebuild the picker footer (#37465) - #37555
Conversation
|
Claude finished @adrianjm-dotCMS's task in 2m 26s —— View job Code Review — PR #37555 (latest push)
I reviewed the diff against New Issues
Resolved
Everything else in the diff (the stacked hint/error markup with its documented temporary utilities, the three Net: the blocking findings from the earlier reviews are all resolved, and |
nicobytes
left a comment
There was a problem hiding this comment.
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:
- Keyboard handling is lost in the new picker footer. The custom
<p-button>does not wireonContainerButtonKeydown, which is what implements Escape and the Tab focus trap inside the overlay. Details inline. - The comment explaining the OnPush workaround states a cause that is not true in 21.1.3.
writeControlValue()does callcd.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. - 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:33still carriesdata-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.warnon non-number values is correct per the repo's Critical Rules, and$valueis typednumber | 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.
…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>
…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>
0bf3dc5 to
6b54571
Compare
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>
PR 2 of 2 — implementation
Fixes #37465
Base is the spec branch, not
main. PR 1 (#37542) carriesspec.mdand is still awaitingapproval, so this branches off it rather than waiting for it to merge — the
Spec-Kit flow gates on
approval, not on merge. Rebase onto
mainonce PR 1 lands.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.
showClearis unconditional, so every type can be emptied — not just the expire-date field#buttonbar: timezone left (on the two types carrying a time), a single secondary-outlinedToday/Nowright, PrimeNG'sCleargone8 files: 6 modified, 1 new spec, plus three keys in
Language.properties.Two things worth a reviewer's attention
1. PrimeNG's supplied
todayCallbackis deliberately unused. It opens withconst 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
getCurrentServerTimeand the existingonCalendarChangeconversion instead, so there is still onlyone conversion path.
Verified in the browser against a running instance: server on UTC, browser on UTC-4 —
Todayset15:57, not 11:57.
2. PrimeNG's default clear icon is a bare
<svg>with a click handler — unfocusable, noaccessible name, unreachable by keyboard. A real
<button>is projected through#clearicon. Theaccessibility 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, whichupdateInputfield()writes withoutmarkForCheck();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 beencalling 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-content— 2390 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:
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