fix(edit-content): adopt the global .form system and standardize hint/error presentation (#37460, #37464) - #37597
adrianjm-dotCMS wants to merge 17 commits into
Conversation
…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>
…/error presentation (#37460, #37464) Field hints and required-error messages rendered as unstyled body text in the new Edit Contentlet, and errors were not red. The dotCMS form system is global CSS scoped to `.form` (apps/dotcms-ui/src/style.css), and the editor's root form never carried that class, so every rule below it was inert. #37460 -- make the global system reach the editor The load-bearing change is structural. `.form .field > label` is a DIRECT-CHILD selector, and the label sat inside a `<dot-card-field-label>` element, so it was a grandchild of `.field` and the rule never matched. `display: contents` on that wrapper does not fix it: `display` governs box generation, selectors match the DOM tree. It collapses the layout correctly while leaving the typography untouched -- a failure that looks like success, which is why AC-105 is a devtools check rather than a passing spec. `dot-card-field-label` now uses an attribute selector, `label[dotCardFieldLabel]`, so the component IS the label instead of wrapping one. The intermediate element is gone rather than worked around. 17 consumer templates updated accordingly. Also: `.form` on the root form, `.field` in `dot-card-field`, `.form-checkbox` / `.form-radio` on option rows, clean labels, and `dotFieldRequired` replacing the hand-written `p-label-input-required`. #37464 -- one hint/error presentation, gated on save `$hasError` moves from `control.invalid && control.touched` to `hasAttemptedSubmit && control.invalid`, so tabbing out of an empty required field no longer turns it red before the author has tried to save anything. The three duplicated markup variants collapse into one footer shared by every field type, the label's hint tooltip is gone (hints are always text below the control), and error and hint now render together, error first -- previously every template suppressed the hint the moment an error appeared, removing the sentence that explained how to fix it. Text Area gains the hint/error slot it never had. This is the only new behavior: a Text Area hint becomes visible for the first time, and an empty required one starts explaining why the save is blocked. Deviations from the issues, both verified rather than assumed - #37460's AC to delete the SCSS in `dot-form-file-editor` / `dot-form-import-url` is not achievable. Those compile into `dotcms-binary-field-builder`, a separate app that loads neither `apps/dotcms-ui/src/style.css` nor Tailwind, and both dialogs open `appendTo: 'body'`. Confirmed at runtime in the legacy Dojo editor after deploying the bundle: the dialogs compute their layout from that SCSS. The only change there is line 47 adopting `dotFieldRequired`, which is safe because `_misc.scss` styles that class in both bundles. - #37464's `aria-required` half of the required-indicator AC is partial: 9 of 22 fields. The rest have a label whose `for` points at an id that does not exist, so there is no declared control to mark. An earlier attempt to infer the control marked the editor-mode dropdown on Text Area and WYSIWYG, which is worse than marking nothing, so the code now marks only what the label declares. Fixing the associations is separate work, tracked for its own issue. `libs/ui`'s `dotFieldRequired` gains a boolean mode. Additive: all 77 existing callers use bare mode and none pass a boolean. Verified in the browser, not by inspection: the rule matching in the CSSOM, the four presentation states, blur producing no error while save produces four, the error clearing without a second save, scroll-to-first-error, and the legacy Dojo binary field after deploying the rebuilt bundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decision 1 said to withdraw the AC asking for the file-editor / import-URL SCSS to be deleted. Developer decision: keep it as written and document on the PR why the SCSS is retained -- it ships inside the dotcms-binary-field custom element the legacy Dojo editor loads, a bundle with neither the global stylesheet nor Tailwind. The constraint is more useful to the next reader than the removal of the criterion that surfaced it. Design has approved the visual deltas, including the checkbox/radio option labels dropping to 12.25px, which #37460's measured-impact table does not list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @adrianjm-dotCMS's task in 4m 3s —— View job PR ReviewReviewed the logic-bearing changes: the New Issues
Everything else checked out:
Notes
· branch |
…ld (#37464) A required, empty Block Editor showed no "This field is required" message, even though its control was INVALID and the save was correctly blocked. Two separate faults, found by exercising a content type covering every field type. 1. Missing pipe -- introduced by the footer consolidation in this branch The Block Editor was the only field template with no footer before #37464. Giving it the shared one made its template use `{{ '...required' | dm }}`, but the component never imported DotMessagePipe. Resolving the pipe threw NG0302, which aborted the change-detection pass partway through the template: the error block rendered nothing, and bindings after it kept stale values. Audited all 18 field templates -- this was the only one affected. The unit suite did not catch it because the Block Editor spec never renders the error state; it only shows against a required, empty field in a browser. 2. Null content -- pre-existing in libs/block-editor `setEditorJSONContent` guards an uninitialised editor and string content, but not null. `typeof null === 'object'`, so a new contentlet with an empty Story Block fell through to `content.content` and threw during ngOnInit. Fixed with `!content` in the same guard, covered by a test in the spec that already exercises that method. Verified in the browser on a content type with every field type: 20 of 20 required, empty fields now render their message, Block Editor included, with the hint below it. Note on libs/block-editor: its suite has 35 pre-existing failures, measured before this change and unchanged by it. The test added here passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rk required where ARIA allows (#37464) AC-209 has two halves. The asterisk half was already met: it is `::after` content, deliberately outside the label's accessible name so a screen reader never announces "star". That is also what left assistive technology with nothing -- which the second half, `aria-required` on the control, was meant to supply. Implementing it surfaced the larger problem underneath. Marking "the control" requires knowing WHICH element that is, and the only declaration of that is the label's `for`. On 13 of 22 field types it pointed at an id that exists nowhere: a screen reader announced those fields as "combobox" with no name at all, and clicking their label did nothing. Pre-existing, and invisible to anyone looking at the screen. Three shapes, because one pattern does not fit every widget 1. Labelable controls -- `<label for>` + `id`, then `aria-required` on the control. Host/Folder's trigger was already behaving as a combobox: it carried aria-expanded and opened a tree overlay. Declaring `role="combobox"` makes that true, and it is a role ARIA defines aria-required on. 2. Option groups and collection widgets -- `role` + `aria-labelledby`. `<label for>` only associates with labelable elements, so a `<div>` group can never be named that way however many roles it carries. The label now exposes a stable id for them to point at. Radio takes `radiogroup`, which supports aria-required; checkbox sets, Key/Value, Category, Relationship, File/Image/Binary and Custom Field take `group`, which ARIA does not define it on -- so it is deliberately absent rather than invalid. 3. Third-party editors -- their own documented option, since each owns its DOM. Monaco's `ariaLabel`, TinyMCE's `iframe_aria_text` for the body inside the frame plus `iframe_attrs.title` for the frame itself (TinyMCE hardcodes that to "Rich Text Area", identical for every rich-text field on a form), and a computed in new-block-editor preferring the field name over its generic translated label. Result: accessible name on 22 of 22 required fields, up from 9. aria-required on 11, every field type ARIA permits it on. An earlier attempt inferred the control from the first focusable descendant and marked the editor-mode dropdown as the mandatory field on Text Area and WYSIWYG -- worse than marking nothing -- so the code only ever marks what the label declares. dot-file-field takes `labelledBy` as an input defaulting to empty rather than deriving it: that component also compiles into the dotcms-binary-field-builder bundle the legacy Dojo editor loads, where no such label exists. Verified there after rebuilding and deploying the bundle -- the attribute is simply absent rather than pointing at an id that is not on the page, and both file dialogs render unchanged. pnpm nx test edit-content: 2458 passing. new-block-editor: 296 passing. Lint and format clean on all three projects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ot reach (#37464) `<label for>` only associates with LABELABLE elements. PrimeNG's select puts `inputId` on a `<span role="combobox">`, which is not one, so the browser resolved `label.control` to nothing and a screen reader announced those fields unnamed -- despite the id existing on the page. `ariaLabelledBy` is PrimeNG's own way to name that span, pointing back at the label's id. Two instances: the Select field, and the language filter in the relationship dialog. The latter is the pattern the repo held up as correct, and was the one actually broken. MultiSelect is unaffected -- it renders a labelable element for `inputId`, which is why it resolved all along. This also corrects an earlier measurement. Counting "named" as "an element with that id exists" is not the same as the browser associating them; `label.control` is. Under the honest metric the count was 21 of 22, not 22, and the gap was the Select. AC-110 needs no work: `p-treeSelect` does render its `inputId`, and site-field's label resolves to `input#site-field` with `role="combobox"`. The earlier report that PrimeNG dropped it came from measuring before the dialog's popover had mounted. Accessible name now resolves on 22 of 22 required fields: 9 via label.control, 10 via aria-labelledby, 3 via the editor's own aria-label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tterns this work needed (#37460, #37464) STYLING_STANDARDS.md contradicted the state this branch leaves the code in, in five places. It is the doc a reviewer or the next developer will reach for, so leaving it stale would have cost more than the work it documents. Reverses the central guidance. The doc called the global `.form`/`.field` classes legacy and said not to extend them to new features. #37460 did exactly that, on Design's request: edit-content was the one surface that had never adopted them, which is why its labels rendered larger and lighter than every other admin form and its hints and errors rendered unstyled. The classes are now documented as the convention, with the reversal called out rather than quietly applied. Corrects three stale rules. The label-to-control gap is supplied by `.field`, not written by hand -- the doc prescribed a `gap-2` that contradicts the system's `gap-1`. The worked example now uses the global markup instead of the hand-rolled wrapper edit-content just abandoned. And the `dotFieldRequired` section claimed the directive has no input and exactly two usages; it has four. Adds two patterns that were learned the hard way here: **Reaching a direct-child rule from a component.** `.form .field > label` is a direct-child selector, so a component rendering its own `<label>` inside its element makes that label a grandchild and the rule never matches. `display: contents` does not fix it -- `display` governs box generation while selectors match the DOM tree -- and its failure mode is the dangerous kind: the gap collapses correctly while the typography stays wrong, so a visual check passes. The fix is an attribute selector, so the component's host IS the label. **Naming a field for assistive technology.** Three shapes by widget, because `<label for>` only associates with labelable elements: a `<div role="group">` and PrimeNG's select (which puts `inputId` on a `<span role="combobox">`) can never be named that way. Records `label.control` as the verification rather than "an id exists", which is not the same thing and hid a real gap; ARIA's list of roles that accept `aria-required`, and that plain `group` is not among them; and that inferring the control from the first focusable descendant marks the editor-mode dropdown as the mandatory field on Text Area and WYSIWYG. Also records the dual role of `dot-card-field-label`'s `$testId`: the format is fixed because specs select on it, and the same value is the element id that composite widgets point `aria-labelledby` at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…shipped (#37460, #37464) Convergence findings on the spec's own artifacts. No behavior changes. The STYLE EXCEPTION header on the two file-field stylesheets named only Tailwind's absence from the legacy bundle. It now names both reasons this subtree keeps its SCSS while #37460 removed every other hand-rolled form style: the bundle loads neither apps/dotcms-ui/src/style.css nor Tailwind, and both dialogs open with appendTo: 'body', which puts them outside the editor's <form class="form"> even in dotcms-ui. Deleting the file does not fall back to the global system — it falls back to unstyled. The dot-card-field-label contract described an API that was never built: a component `imports` and an `isRequired` component input. What shipped attaches the directive as a host directive with an aliased input, because a component cannot apply a directive to its own host from its own template. The contract also missed the `[attr.id]` host binding, which is load-bearing: seven composite field types name themselves with aria-labelledby pointing at it. data-model.md said the control "carries required / aria-required" as though it were uniform. It resolves three different ways by widget, and aria-required only goes on roles ARIA defines it for — which is why the count is 22/22 accessible names but 11/22 aria-required. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…37460, #37464) STYLING_STANDARDS.md gained the form-markup and accessible-naming rules in 3522b4d, but the documents that point at it did not catch up. The index called it "Tailwind, PrimeNG theme, BEM, SCSS variables" and assigned forms to ANGULAR_STANDARDS.md, which has no form-markup section at all — so a developer following the index lands on the wrong file and writes the markup #37460 removed. core-web/CLAUDE.md already routed correctly; the two index documents now match it, and a short section says plainly that forms are split: ANGULAR_STANDARDS owns the TypeScript side, STYLING_STANDARDS owns everything visible. The form example in COMPONENT_ARCHITECTURE.md is the only end-to-end form template in docs/ and carried no class="form", so every field in it renders with the global system inert — exactly the defect #37460 was filed for. Copying the canonical example reproduced the bug. ANGULAR_STANDARDS.md's accessibility section said "pass AXE" and "ARIA attributes where needed". That is the guidance that left 13 of 22 field types with a label `for` pointing at an id that was never rendered: nothing throws, and AXE may still pass. It now carries the rule that `for` only reaches labelable elements, and how to verify it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AC-209 was reported met for the Block Editor. It was not: the editable surface had no accessible name, and a screen reader landing on it announced nothing. role="textbox", aria-multiline and aria-label were bound in the template, on the div that carries the `tiptap` directive. ngx-tiptap mounts ProseMirror's contenteditable as a CHILD of that div, so the named element never receives focus — and the page ended up declaring two nested role="textbox", the inner one anonymous. editorProps.attributes is the only way to reach the element ProseMirror owns, so the three attributes move there and come off the wrapper. Verified in the browser: the contenteditable now resolves to the field's name, and there is exactly one textbox. Across the QA content type covering every field type, that closes the last gap: 23 of 23 fields resolve an accessible name — 10 through `label[for]`, 10 through role + aria-labelledby, 3 through the editor's own option. Not addressed here, and pre-existing: aria-haspopup, aria-controls, aria-expanded and aria-activedescendant for the slash-command menu sit on the same unfocused wrapper, so they announce nothing either. Moving them needs reactivity editorProps does not offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
…null (#37464) Two review findings from PR #37597. **formControl.** The getter destructured `this.$field()` unguarded. `$field` is `input.required` on most subclasses but NOT all — custom-field and json-field both declare it with a `null` default — so the getter throws a TypeError rather than yielding "no control", and it throws from inside the `$hasError` computed. Every caller, here and in the subclasses, already handled a null control; the getter was the one that did not. It now returns `FormControl | null`, which is what all of them already assumed. **aria-required on late-mounting controls.** The afterRenderEffect in dot-card-field is reactive, not per-render, so a control that mounts in a later change-detection pass without a tracked signal changing is never visited. That is currently unreachable, but only by coincidence: every field that defers its control (category, file, custom/iframe) exposes it as `role="group"`, which ARIA gives no aria-required, and every field that can carry the attribute either renders its control synchronously or sets it in its own template (radio, host-folder). Behavior unchanged; the constraint is now written down, because giving a deferred widget an eligible role would silently stop marking it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ouched (#37460, #37464) CI's strict-gate runs tsc in strict mode over the changed lines. It failed on 45 diagnostics, all in code this branch added. **TS1117 — duplicate object keys (12).** dot-wysiwyg-tinymce's spec declared the `iframe_aria_text` / `iframe_attrs` pair three times over inside the same literal, in three separate expectations. The last one silently wins in JS, so the assertion passed and the duplication was invisible. **TS2531 / TS18047 — possibly null (32).** `spectator.query` returns `T | null` and these specs dereferenced it directly. The obvious fix is the wrong one here. Adding `?.` everywhere makes every absence assertion vacuous: `expect(x?.getAttribute('aria-required')).toBeNull()` passes just as happily when `x` is null, so the checkbox-group test — whose whole point is that ARIA defines no aria-required on plain `group` — would stay green even if the group never rendered. So: - assertions about a VALUE use `?.`, since `toBe(...)` still fails on undefined; - assertions about ABSENCE either assert existence first, or go through a helper that throws when the element is missing, which also narrows the type for free. **TS7006 (1).** Typed the spec's `setup` parameter as tinymce's `Editor`. One production change: `base-wrapper-field`'s `controlContainer.control` is itself nullable — a subclass rendered outside a form directive has no container control to ask. Pre-existing, surfaced because this branch touched the line. strict-gate: PASS. edit-content: 2425 tests across 116 files, lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ced it (#37464) Three E2E specs failed — the required-error assertion on the binary, file and image fields. A true positive: this branch broke their page object. The locator was `.error-message small`. AC-208's whole point is that `.error-message` stops existing: the file field's own error markup was replaced by the footer every field type now shares, where the <small> carries `p-field-error` itself rather than sitting inside a wrapper. So the selector matched nothing and the assertion timed out waiting for an element that renders correctly under a different shape. The audit for `.error-message` covered libs/edit-content and missed apps/dotcms-ui-e2e. This was the only stale locator there; `.hint-message` has none. Verified in the browser against a content type with all three field types required: before a save attempt each renders 0 errors, after one each renders exactly 1 `small.p-field-error`, and `.error-message small` matches nothing anywhere on the page. REQUIRED_FIELD_ERROR is unchanged — the text was never the problem, only where the test looked for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #37460
Closes #37464
Field hints and required errors rendered as unstyled body text, and errors were not red. The dotCMS form system is global CSS scoped to
.form(apps/dotcms-ui/src/style.css), and the editor's root<form>never carried that class, so every rule below it was inert. The hint/error slot was also written three different ways across 18 templates, the error fired on blur instead of on save, and the hint was suppressed exactly when the field was in error.The load-bearing change
.form .field > labelis a direct-child selector. The label sat inside a<dot-card-field-label>element, making it a grandchild of.field, so the rule never matched.display: contentsdoes not fix this.displaygoverns box generation; selectors match the DOM tree. It collapses the layout correctly while leaving the typography untouched — the gap looks right and the label stays at 14px/400. That failure looks like success, which is why AC-105 is a devtools check ("the rule appears as matched") rather than a passing spec.The component now uses an attribute selector,
label[dotCardFieldLabel], so it is the label instead of wrapping one:17 consumer templates updated. Because the host is the label, the required directive is attached with
hostDirectivesrather than from the template — a component cannot put a directive on its own host that way.What each issue changes
#37460 —
class="form"on the root<form>;.fieldindot-card-field, keeping thefield-error-markeranchor and the empty-footer collapse;.form-checkbox/.form-radioon option rows; labels stripped of typography utilities;dotFieldRequiredreplaces the hand-writtenp-label-input-required.#37464 —
$hasErrormoves fromcontrol.invalid && control.touchedtohasAttemptedSubmit && control.invalid, so tabbing out of an empty required field no longer turns it red. The three markup variants collapse into one shared footer. The label's hint tooltip is gone — hints are always text below the control. Error and hint now render together, error first; previously every template dropped the hint the moment an error appeared, removing the sentence explaining how to fix it. Text Area gains the hint/error slot it never had — the one piece of genuinely new behavior, worth a QA pass.Accessibility — AC-209
The asterisk is
::aftercontent, deliberately outside the label's accessible name so a screen reader never announces "star" — which leaves assistive technology with nothing unless the control itself says it is mandatory.Marking "the control" requires knowing which element that is, and the only declaration of it is the label's
for. On 13 of 22 field types it pointed at an id that exists nowhere: those fields announced with no name, and clicking their label did nothing. Pre-existing, and invisible to anyone looking at the screen.One pattern does not fit every widget, so there are three:
aria-required?<label for>→ the control'siddiv,span)role+aria-labelledby→ the label'sid<label for>only reaches labelable elements, so a<div role="group">can never be named that way however many roles it carries — the label now exposes a stable id for those to point at, and that id is load-bearing: renaming it silently strips the name off seven field types. ARIA definesaria-requiredoncombobox,listbox,radiogroup,spinbutton,textboxandtree— not on plaingroup, and there is nocheckboxgrouprole, so a required checkbox group shows its asterisk but has no vocabulary to announce it.Measured on a content type carrying every field type: an accessible name resolves on 23 of 23 — 10 via
label.control, 10 viaaria-labelledby, 3 via the editor's own option.aria-requiredon 11, every field ARIA permits it on.Deviations, each verified rather than assumed
#37460's AC to delete the SCSS in
dot-form-file-editor/dot-form-import-urlis not achievable — and the criterion stays in the issue. Those compile intodotcms-binary-field-builder, the app behind the<dotcms-binary-field>element the legacy Dojo editor loads. That bundle has its own 14-linestyle.css, no Tailwind and no PostCSS config, so the global system neither exists nor could compile there; independently, both dialogs openappendTo: 'body', outside any.form. Confirmed at runtime: the import-URL dialog computesgap: 10.5pxandwidth: 448pxstraight from that SCSS. Deleting it leaves both unstyled in the legacy editor. Both files now carry that reasoning in theirSTYLE EXCEPTIONheader. The only change in that subtree is line 47 adoptingdotFieldRequired.libs/ui'sdotFieldRequiredgains a boolean mode. Additive: all 77 existing callers use bare mode and none passes a boolean.Merged
main, which brought #37192 with it. That deleted the old relationship search dialog wholesale, so what this branch did to those four files is moot — and the suite drops from 2459 to 2425 tests purely because their specs went with them. Conflict-by-conflict reasoning is in the merge commit.Verified in the browser, not by inspection
Re-run after the merge; these are the post-merge numbers.
.form .field > labela matched rule in the CSSOMdot-tags-create)rgb(107,114,128)/rgb(239,68,68), no icons.field-error-markeranchorsrole="textbox"docker cp'd, both dialogs exercised after every round of changesNotes for the reviewer
DotMessagePipeimport that threw NG0302 and killed the Block Editor's change-detection pass, and a pre-existing null-content crash inlibs/block-editor(typeof null === 'object'fell through the guard).docs/frontend/STYLING_STANDARDS.mdhad the global form system filed as legacy, do not extend — the opposite of what this establishes. Rewritten, plus the docs that pointed at it: the index assigned "forms" toANGULAR_STANDARDS.md, which has no form-markup section.libs/block-editor's own suite has 35 pre-existing failures, measured before this change and unchanged by it.pnpm nx test edit-content: 2425 passing across 116 files.new-block-editor: 296.pnpm nx build dotcms-uicompiles.lintclean.🤖 Generated with Claude Code