Repository navigation
fix(components): the required asterisk is a real aria-hidden span, not CSS generated content (objectui#10368) - #10428
Conversation
…S generated content (objectui#10368) The six labelled form renderers drew the required marker as a Tailwind `::after` content utility on the control's associated label. The accessible-name computation includes generated content, so Chromium named a required field labelled "Title" as "Title*". A pseudo-element cannot carry `aria-hidden`, so the marker is now a real `span aria-hidden="true"` with `data-required-marker`, the shape the form renderer's FormLabel already uses. The FieldContainer comment and the CheckboxSchema.required docblock that described the old style are corrected. The two pins that asserted the clean name under happy-dom (which computes no generated content) now carry their verdict on the markup; a new test covers all six sites plus a source guard. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…ontainer marker comment Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
`renderComponent` takes `BaseSchema`, whose index signature already admits these renderer keys, so the casts only added lint warnings. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…ctui#10368 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10368
Clause-②: no
What changed
Six published renderers in
@object-ui/componentsdrew the required asterisk as CSS generated content: a Tailwind::aftercontent utility on the control's associated label. The accessible-name computation includes generated content, so Chromium named a required field labelled "Title" asTitle*. A pseudo-element cannot carryaria-hidden.spanwitharia-hidden="true"anddata-required-marker="true". This is the shape the form renderer'sFormLabelmarker already uses (ADR-0054 C4), and the shape of objectui#3299 /ActionParamDialogand objectui#10361.custom/field.tsx(FieldContainer),renderers/basic/text-input.tsx(element:text_input), andrenderers/form/input.tsx,textarea.tsx,checkbox.tsxandselect.tsx. They were re-located by content onorigin/main2fc2a2439. Before the change, a grep for the asterisk content utility matched these six class sites plus thefield.tsxcomment in non-testsrc.FieldContainercomment said the CSS asterisk "never enters" the name.CheckboxSchema.requireddocblock inpackages/types/src/form.tsdescribed the pseudo-element style. The rewrite also drops that docblock's two stale cross-file line addresses and cites by content instead.src, so a spelled-out utility would compile the rule back intostyle.css.Measurement: Chromium's accessibility tree on the compiled class (A3)
The card's number came from an equivalent CSS rule. This one is from the compiled class itself.
packages/components/src/index.cssthrough the same postcss +@tailwindcss/postcsspipeline thatscripts/build-css.mjsruns. It loads the page in Chromium141.0.7390.37and reads CDPAccessibility.getFullAXTree.2fc2a2439. "After" is8d744575d, the last commit that touches source or tests.FieldContainerTitle*Titlearia-required)element:text_inputTitle*TitleinputTitle*TitletextareaTitle*TitlecheckboxTitle*Titlerequiredproperty listed (see A2)selectaria-required)The compiled sheet contains the asterisk content rule before the change and does not contain it after.
PM mechanism assumptions
rgb(239, 68, 68)before (::after) and after (the span). The margin-left is2pxin both.text-destructiveon the label itself, as before. The span also carriestext-destructiveandml-0.5.peer-disabled:*classes apply to the label box, and the span sits inside that box.FieldContainerusesaria-requiredand still has no nativerequired.element:text_input,inputandtextareause the nativerequired.checkbox: Radix writesaria-required="true"on therole=checkboxbutton.select: Radix writesaria-required="true"on the combobox trigger.requiredentry for thatrole=checkboxnode, both before and after, even though the attribute is present. This is a reading of the inspector's mapping, not something this PR changes.after:class. The only test that asserted a required label's clean name was pin 1, rewritten below.@testing-library/dom10.4.1: an exact-stringgetByLabelText('Title')no longer finds a required field, because it reads the label's whole text, hidden*included.getByLabelText(/Title/)andgetByRole('textbox', { name: 'Title' })still find it. The changeset says so.Pins rewritten, not deleted
field-container-aria-required.test.tsx. The last case's verdict used to betoHaveAccessibleName('Title')under happy-dom, next to a comment saying the asterisk could never leak.content-[orcontent-(utility, bare or behind a variant). The*is onearia-hiddenelement. The label text outsidearia-hiddensubtrees is exactlyTitle.dom-accessibility-api0.6.3 (the jest-dom copy) returnsTitle *for the same span withoutaria-hiddenandTitlewith it, so that line does go red on the span losingaria-hidden. It is still blind to generated content.text-input-description-association.test.tsx: premise partly falsified. None of this pin's cases setrequired. Its header already said the asterisk IS in the name in a real browser, so this pin never falsely asserted a clean name for a required field.requiredis kept.required-marker-markup.test.tsx.srcfile. It has a population control (the six site files are in the scanned set) and a pattern control (every spelling it guards matches, andcontent-nonedoes not). Tests are excluded from Tailwind's sources and from this scan.import.meta.url, not the cwd.Reverse proof
ablation-replacewith an anchor that must hit (at304f68f82, committed). The run covered the new test and both pins.git diff HEADwas empty.custom/field.tsxFieldContainerrequired case · source guard · pin 1's rewritten caserenderers/basic/text-input.tsxelement:text_inputrequired case · source guard · pin 2's new required caserenderers/form/input.tsxinputrequired case · source guardLocal verification
The final HEAD is
98977bbfe. It differs from8d744575donly in the changeset's wording. No source or test file has changed since8d744575d.304f68f82. 82 files, 915 passed, 10 skipped. The run covered:form-renderers.test.tsx;renderers/form/__tests__/;text-inputlabel and inputs suites;packages/typessuites that nameform.ts.8d744575d. 23 files, 394 passed. The run covered the three new and rewritten files, plus every other components test file that names these renderers.304f68f82and8d744575d, only the new test file changed (itsas anycasts were removed).tsc --noEmitexit 0, andtsc -p tsconfig.test.jsonexit 0.--listFilesincludes all three test files.@object-ui/types:type-checkexit 0.8d744575d..ts/.tsxfiles, at98977bbfe. 0 errors and 34 warnings. The same files on the base tree also give 34 warnings, so this PR adds no new warnings.**/*.{ts,tsx}block covers all 10 files, and the--format jsonoutput has 10 entries.parserOptions.projectorprojectService), and no rule undereslint-rules/reads the filesystem. So this diff cannot change the lint verdict on any untouched file.98977bbfe, all exit 0:check-changeset-presence: 1 changeset declared.check-changeset-no-major.check:new-line-citations: 0 new citations.check:control-bytes.check:changeset-claims: 12 pending changesets name touched files. Each was re-read, and all are still true.check:pending-changeset-literals,check:test-path-roots,check:handler-key-reads,check:vi-mock-override-shape.Acceptance notes
select.tsx's label has nohtmlFor, and the trigger gets no id. Its marker changed shape, but its name never had the asterisk. The new test deliberately does not pin the association either way. See the out-of-scope finding in the report.form-renderers.test.tsx's "should show required indicator when required" asserts onlytext-destructiveon the label, although its comment says it checks the indicator. It is unchanged and still green, and it is outside this claim's file surface.form.ts. The file has other cross-file line addresses, for example in theCheckboxSchema.wrapperClassdocblock. Only therequireddocblock was in this claim's surface, so the others are left for whoever next touches them.check:test-path-roots. The new test's recursive walker passes its directory as a parameter. The gate lists those two calls under--blindrather than classifying them. The root isimport.meta.url.Generated by Claude Code