fix(ui): wire up aria-describedby for form field descriptions
Form field descriptions were visually rendered but not connected to inputs via aria-describedby, making them invisible to screen readers. Added a descriptionSlot prop to FieldLabel that renders the description as a React Aria <Text slot="description"> element, letting React Aria automatically wire up aria-describedby on the associated input. Applied to TextField, PasswordField, SearchField, Select, RadioGroup, and CheckboxGroup. Slider already handled this manually and is unchanged. Also added a missing WithDescription story for RadioGroup. Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': patch
|
||||
---
|
||||
|
||||
Fixed form field descriptions not being connected to inputs via `aria-describedby`, making them accessible to screen readers. Added a `descriptionSlot` prop to `FieldLabel` that uses React Aria's slot mechanism to automatically wire up the connection.
|
||||
|
||||
**Affected components:** FieldLabel, TextField, PasswordField, SearchField, Select, RadioGroup, CheckboxGroup
|
||||
@@ -1243,6 +1243,7 @@ export const FieldLabelDefinition: {
|
||||
readonly htmlFor: {};
|
||||
readonly id: {};
|
||||
readonly descriptionId: {};
|
||||
readonly descriptionSlot: {};
|
||||
readonly className: {};
|
||||
};
|
||||
};
|
||||
@@ -1255,6 +1256,7 @@ export type FieldLabelOwnProps = {
|
||||
htmlFor?: string;
|
||||
id?: string;
|
||||
descriptionId?: string;
|
||||
descriptionSlot?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<div className={classes.content}>{children}</div>
|
||||
<FieldError />
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Label } from 'react-aria-components';
|
||||
import { Label, Text } from 'react-aria-components';
|
||||
import { forwardRef } from 'react';
|
||||
import type { FieldLabelProps } from './types';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
@@ -35,6 +35,7 @@ export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
|
||||
htmlFor,
|
||||
id,
|
||||
descriptionId,
|
||||
descriptionSlot,
|
||||
} = ownProps;
|
||||
|
||||
if (!label) return null;
|
||||
@@ -52,9 +53,14 @@ export const FieldLabel = forwardRef<HTMLDivElement, FieldLabelProps>(
|
||||
</Label>
|
||||
)}
|
||||
{description && (
|
||||
<div className={classes.description} id={descriptionId}>
|
||||
<Text
|
||||
slot={descriptionSlot}
|
||||
className={classes.description}
|
||||
elementType="div"
|
||||
id={descriptionId}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ export const FieldLabelDefinition = defineComponent<FieldLabelOwnProps>()({
|
||||
htmlFor: {},
|
||||
id: {},
|
||||
descriptionId: {},
|
||||
descriptionSlot: {},
|
||||
className: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,6 +46,11 @@ export type FieldLabelOwnProps = {
|
||||
*/
|
||||
descriptionId?: string;
|
||||
|
||||
/**
|
||||
* The slot name to set on the description's React Aria `<Text>` element.
|
||||
*/
|
||||
descriptionSlot?: string;
|
||||
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export const PasswordField = forwardRef<HTMLDivElement, PasswordFieldProps>(
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<div
|
||||
className={classes.inputWrapper}
|
||||
|
||||
@@ -34,6 +34,20 @@ export const Default = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const WithDescription = meta.story({
|
||||
args: {
|
||||
...Default.input.args,
|
||||
description: 'Choose only one option',
|
||||
},
|
||||
render: args => (
|
||||
<RadioGroup {...args}>
|
||||
<Radio value="bulbasaur">Bulbasaur</Radio>
|
||||
<Radio value="charmander">Charmander</Radio>
|
||||
<Radio value="squirtle">Squirtle</Radio>
|
||||
</RadioGroup>
|
||||
),
|
||||
});
|
||||
|
||||
export const Horizontal = meta.story({
|
||||
args: {
|
||||
...Default.input.args,
|
||||
|
||||
@@ -64,6 +64,7 @@ export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<div className={classes.content}>{children}</div>
|
||||
<FieldError />
|
||||
|
||||
@@ -96,6 +96,7 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<div
|
||||
className={classes.inputWrapper}
|
||||
|
||||
@@ -80,6 +80,7 @@ export const Select = forwardRef<
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<SelectTrigger icon={icon} />
|
||||
<FieldError />
|
||||
|
||||
@@ -59,6 +59,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
|
||||
label={label}
|
||||
secondaryLabel={secondaryLabelText}
|
||||
description={description}
|
||||
descriptionSlot="description"
|
||||
/>
|
||||
<div
|
||||
className={classes.inputWrapper}
|
||||
|
||||
Reference in New Issue
Block a user