ui/VisuallyHidden: Standardize composition pattern#77190
Conversation
@wordpress/ui|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
packages/ui/CHANGELOG.md
Outdated
| ### Bug Fixes | ||
|
|
||
| - `Card`: Set default foreground color on `Card.Root` so content and `currentColor` icons (for example the `CollapsibleCard` chevron) are themeable by default ([#77013](https://github.com/WordPress/gutenberg/pull/77013)). | ||
| - `Field.Label`: Preserve the native `<label>` element when `hideFromVision` is enabled, improving assistive technology support. |
There was a problem hiding this comment.
For my own understanding, can you explain what's actually improved by this? The previous implementation did connect the field to a label via aria-labelledby, albeit to a div.
There was a problem hiding this comment.
In practical terms, not much changes — as you point out correctly, we're switching from aria-labelledby + div to a label element. Apart from being potentially compatible with a wider list of screen readers, the main advantage here is consistency (and slightly cleaner code, and slightly more idiomatic/semantic HTML)
There was a problem hiding this comment.
I updated the CHANGELOG to mark this change as internal and not a bug fix
|
Size Change: 0 B Total Size: 7.74 MB ℹ️ View Unchanged
|
|
Flaky tests detected in 6e3debf. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/24202705260
|
aduth
left a comment
There was a problem hiding this comment.
LGTM 👍 Makes me wonder if it's worth documenting something in CONTRIBUTING.md, but I guess if we view ourselves as equal consumers of VisuallyHidden, the existing documentation / stories here should be expected to suffice in our own usage.
Use `<VisuallyHidden render={<_Field.Label />}>` instead of
`<_Field.Label render={<VisuallyHidden />}>` so that the native
`<label>` element is preserved when `hideFromVision` is enabled.
This removes the `nativeLabel: false` workaround that was needed
because the previous pattern replaced the `<label>` with a `<div>`.
Made-with: Cursor
Use `<VisuallyHidden render={<_Fieldset.Legend />}>` instead of
`<_Fieldset.Legend render={<VisuallyHidden />}>` so that the legend's
semantic element is preserved when `hideFromVision` is enabled.
Made-with: Cursor
Use `<VisuallyHidden render={<_Field.Description />}>` instead of
`<_Field.Description render={<VisuallyHidden />}>` so that the
description's semantic element is preserved.
Made-with: Cursor
Add two new stories alongside the existing Default story: - WithCustomElement: demonstrates using `render` to swap the underlying HTML element (e.g. rendering as a <label>) - ComposedWithAnotherComponent: demonstrates the recommended composition pattern where VisuallyHidden is the host and the other component is passed via `render`, preserving its semantic element Made-with: Cursor
Document the two composition patterns and explain why VisuallyHidden should be the host (outer component) when composing with other semantic components via the `render` prop. Made-with: Cursor
Update Dialog.Title JSDoc to document the VisuallyHidden composition pattern, matching the existing Popover.Title documentation. Add a WithVisuallyHiddenTitle story demonstrating the pattern. Made-with: Cursor
Add tests verifying that Field.Label and Fieldset.Legend preserve their semantic element types when hideFromVision is enabled, rather than being replaced by a generic <div>. Made-with: Cursor
Made-with: Cursor
Allow `children` to be omitted on `Dialog.Title` when the component
is used as a render element (e.g. `<VisuallyHidden render={<Dialog.Title />}>`),
matching the Popover.Title pattern. Children are provided by the
wrapping component at runtime.
Made-with: Cursor
Remove the ComposedWithAnotherComponent story — the render-prop composition pattern is already demonstrated in the Popover and Dialog stories where it's used in context. Reword the VisuallyHidden JSDoc to be less prescriptive: explain both composition directions, their trade-offs, and let consumers choose based on whether the semantic element matters. Made-with: Cursor
The composition refactor preserves the native <label> element, but the previous aria-labelledby approach was not broken — the change is about consistency and removing workarounds, not fixing a bug. Made-with: Cursor
6e3debf to
986f428
Compare
I hope that the component documentation is enough, but we can definitely add more to |
What?
Standardize how
VisuallyHiddencomposes with other components in@wordpress/ui, and improve documentation.Why?
Two composition patterns existed in the codebase. Only one preserves semantic HTML elements — the other silently replaces them with a
<div>. This PR adopts the first pattern consistently and documents both for consumers and maintainers.Detailed assessment of the two patterns
Pattern A (preferred in most cases) —
<VisuallyHidden render={<OtherComponent />}><label>,<legend>,<h2>)Pattern B —
<OtherComponent render={<VisuallyHidden />}><div>nativeLabel: falseonField.LabelBase UI's
useRender+cloneElementmachinery ensures that with Pattern A, the composed component still runs its own hooks, connects to context, and computes accessibility attributes — thevisually-hiddenclass is simply merged through.How?
Field.Label,Fieldset.Legend,Field.Details) from Pattern B to Pattern Arenderprop with a custom elementVisuallyHidden+renderprop docs toDialog.Title(matching existingPopover.Titledocs), plus aWithVisuallyHiddenTitlestoryDialog.Titlechildren optional to support the render-prop composition pattern (matchingPopover.Title)hideFromVisionis enabledFiles changed
form/primitives/field/label.tsxnativeLabel: falseform/primitives/fieldset/legend.tsxform/primitives/field/details.tsxvisually-hidden/visually-hidden.tsxvisually-hidden/stories/index.story.tsxWithCustomElementstorydialog/title.tsxVisuallyHiddenexampledialog/types.tsTitleProps.childrenoptionaldialog/stories/index.story.tsxWithVisuallyHiddenTitlestoryform/primitives/field/test/index.test.tsxform/primitives/fieldset/test/index.test.tsxCHANGELOG.mdTesting Instructions
npm run test:unit -- --testPathPattern="packages/ui/src/(form|visually-hidden)"— all 37 tests should passnpm run storybook:ui) and navigate to:WithCustomElementstory renders correctlyWithVisuallyHiddenTitlestory works (title hidden visually, accessible via screen reader / DOM inspection)Testing Instructions for Keyboard
No user-facing keyboard interaction changes.
Use of AI Tools
Cursor with Claude was used for codebase analysis, plan drafting, and implementation under human review.