Skip to content

Commit e21c199

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 09a8406 commit e21c199

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,
@@ -42,6 +44,9 @@ type SelectProps<M extends SelectionMode = 'single'> = AriaSelectProps<
4244
items?: Item[];
4345
sections?: Section[];
4446
value: M extends 'single' ? string | null : string[];
47+
label?: string;
48+
description?: string;
49+
triggerId?: string;
4550
labelId?: string;
4651
ariaLabelledbyPrefix?: string;
4752
alwaysShowKey?: string;
@@ -51,6 +56,9 @@ type AutocompleteFilter = NonNullable<AutocompleteProps<Item>['filter']>;
5156
function Select<M extends SelectionMode = 'single'>({
5257
items,
5358
sections,
59+
label,
60+
description,
61+
triggerId,
5462
labelId,
5563
ariaLabelledbyPrefix,
5664
alwaysShowKey,
@@ -77,12 +85,22 @@ function Select<M extends SelectionMode = 'single'>({
7785

7886
return (
7987
<AriaSelect {...props}>
88+
{label ? (
89+
<Label className="fr-label">
90+
{label}
91+
{description ? (
92+
<Text slot="description" className="fr-hint-text">
93+
{description}
94+
</Text>
95+
) : null}
96+
</Label>
97+
) : null}
8098
{props.selectionMode == 'single' ? (
81-
<Button className="fr-select">
99+
<Button id={triggerId} className="fr-select">
82100
<SelectValue />
83101
</Button>
84102
) : (
85-
<MultipleSelectValue />
103+
<MultipleSelectValue triggerId={triggerId} />
86104
)}
87105
<Popover
88106
className="react-aria-Popover select-popover"
@@ -118,13 +136,13 @@ function Select<M extends SelectionMode = 'single'>({
118136
);
119137
}
120138

121-
function MultipleSelectValue() {
139+
function MultipleSelectValue({ triggerId }: { triggerId?: string }) {
122140
const selectButtonRef = useRef<HTMLButtonElement>(null);
123141
return (
124142
<SelectValue<Item>>
125143
{({ selectedItems, state, defaultChildren }) => (
126144
<>
127-
<Button className="fr-select" ref={selectButtonRef}>
145+
<Button id={triggerId} className="fr-select" ref={selectButtonRef}>
128146
<span className="react-aria-SelectValue" data-placeholder>
129147
<Plural
130148
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(),
@@ -107,8 +108,10 @@ const SelectProps = s.partial(
107108
items: s.union([s.array(Item), ArrayOfStrings, ArrayOfTuples]),
108109
sections: s.array(Section),
109110
id: s.string(),
111+
triggerId: s.string(),
110112
className: s.string(),
111113
name: s.string(),
114+
label: s.string(),
112115
description: s.string(),
113116
isRequired: s.boolean(),
114117
isDisabled: s.boolean(),

0 commit comments

Comments
 (0)