Skip to content

Commit 5cef0c0

Browse files
mfoclaude
andcommitted
a11y: add triggerId and label/description props to Select
Allows external <label for="..."> to point to the select's trigger <button> via a known id. Also adds label/description rendering support inside AriaSelect for accessible form patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0dd6242 commit 5cef0c0

3 files changed

Lines changed: 28 additions & 4 deletions

File tree

app/javascript/components/ComboBox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ export function ComboBox({
7979
ariaLabelledbyPrefix,
8080
description,
8181
className,
82+
inputId,
8283
inputRef,
8384
isLoading,
8485
isOpen,
8586
placeholder,
8687
items,
8788
...props
8889
}: ComboBoxProps & {
90+
inputId?: string;
8991
inputRef?: RefObject<HTMLInputElement | null>;
9092
isOpen?: boolean;
9193
placeholder?: string;
@@ -119,6 +121,7 @@ export function ComboBox({
119121
) : null}
120122
<div className="fr-ds-combobox__input" style={{ position: 'relative' }}>
121123
<Input
124+
id={inputId}
122125
className="fr-select fr-autocomplete"
123126
ref={inputRef}
124127
aria-busy={isLoading}

app/javascript/components/Select.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
Autocomplete,
44
SelectValue,
55
Button,
6+
Label,
7+
Text,
68
Popover,
79
Virtualizer,
810
ListLayout,
@@ -37,6 +39,9 @@ type SelectProps<M extends SelectionMode = 'single'> = AriaSelectProps<
3739
> & {
3840
items: Item[];
3941
value: M extends 'single' ? string | null : string[];
42+
label?: string;
43+
description?: string;
44+
triggerId?: string;
4045
labelId?: string;
4146
ariaLabelledbyPrefix?: string;
4247
alwaysShowKey?: string;
@@ -45,6 +50,9 @@ type AutocompleteFilter = NonNullable<AutocompleteProps<Item>['filter']>;
4550

4651
function Select<M extends SelectionMode = 'single'>({
4752
items,
53+
label,
54+
description,
55+
triggerId,
4856
labelId,
4957
ariaLabelledbyPrefix,
5058
alwaysShowKey,
@@ -67,12 +75,22 @@ function Select<M extends SelectionMode = 'single'>({
6775

6876
return (
6977
<AriaSelect {...props}>
78+
{label ? (
79+
<Label className="fr-label">
80+
{label}
81+
{description ? (
82+
<Text slot="description" className="fr-hint-text">
83+
{description}
84+
</Text>
85+
) : null}
86+
</Label>
87+
) : null}
7088
{props.selectionMode == 'single' ? (
71-
<Button className="fr-select">
89+
<Button id={triggerId} className="fr-select">
7290
<SelectValue />
7391
</Button>
7492
) : (
75-
<MultipleSelectValue />
93+
<MultipleSelectValue triggerId={triggerId} />
7694
)}
7795
<Popover
7896
className="react-aria-Popover select-popover"
@@ -91,13 +109,13 @@ function Select<M extends SelectionMode = 'single'>({
91109
);
92110
}
93111

94-
function MultipleSelectValue() {
112+
function MultipleSelectValue({ triggerId }: { triggerId?: string }) {
95113
const selectButtonRef = useRef<HTMLButtonElement>(null);
96114
return (
97115
<SelectValue<Item>>
98116
{({ selectedItems, state, defaultChildren }) => (
99117
<>
100-
<Button className="fr-select" ref={selectButtonRef}>
118+
<Button id={triggerId} className="fr-select" ref={selectButtonRef}>
101119
<span className="react-aria-SelectValue" data-placeholder>
102120
<Plural
103121
value={selectedItems.length}

app/javascript/components/react-aria/props.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type Section = { label: string; items: Item[] };
3333
const ComboBoxPropsSchema = s.partial(
3434
s.object({
3535
id: s.string(),
36+
inputId: s.string(),
3637
className: s.string(),
3738
name: s.string(),
3839
label: s.string(),
@@ -105,8 +106,10 @@ export type RemoteComboBoxProps = s.Infer<typeof RemoteComboBoxProps> & {
105106
const SelectProps = s.partial(
106107
s.object({
107108
id: s.string(),
109+
triggerId: s.string(),
108110
className: s.string(),
109111
name: s.string(),
112+
label: s.string(),
110113
description: s.string(),
111114
isRequired: s.boolean(),
112115
isDisabled: s.boolean(),

0 commit comments

Comments
 (0)