Skip to content

Commit 9574ce2

Browse files
fix(fuselage): improve a11y for MultiSelect components (#1940)
Co-authored-by: Douglas Fabris <devfabris@gmail.com>
1 parent 00052dc commit 9574ce2

10 files changed

Lines changed: 342 additions & 24 deletions

File tree

.changeset/giant-oranges-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/fuselage': patch
3+
---
4+
5+
fix(fuselage): improve a11y for `MultiSelect` components

packages/fuselage-forms/src/__snapshots__/test.spec.tsx.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,6 @@ exports[`renders WithMultiSelect without crashing 1`] = `
215215
class="rcx-box rcx-box--full rcx-field__row"
216216
>
217217
<div
218-
aria-describedby="react-aria-:rb:-description react-aria-:rb:-error react-aria-:rb:-hint"
219-
aria-invalid="true"
220-
aria-labelledby="react-aria-:rb:-label"
221218
class="rcx-box rcx-box--full rcx-select"
222219
>
223220
<div
@@ -227,10 +224,13 @@ exports[`renders WithMultiSelect without crashing 1`] = `
227224
class="rcx-box rcx-box--full rcx-css-w398ts"
228225
>
229226
<button
227+
aria-describedby="react-aria-:rb:-description react-aria-:rb:-error react-aria-:rb:-hint"
230228
aria-expanded="false"
231229
aria-haspopup="listbox"
230+
aria-invalid="true"
232231
aria-labelledby="react-aria-:rb:-label"
233232
class="rcx-box rcx-box--full rcx-input-box--undecorated rcx-select__focus rcx-css-trljwa rcx-css-3fq6z9"
233+
role="combobox"
234234
type="button"
235235
/>
236236
<button
Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
import { composeStories } from '@storybook/react-webpack5';
2+
import { axe } from 'jest-axe';
23
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';
34

45
import { render } from '../../testing';
56

67
import * as stories from './MultiSelect.stories';
78

8-
const { Default } = composeStories(stories);
9+
const testCases = Object.values(composeStories(stories)).map((Story) => [
10+
Story.storyName || 'Story',
11+
Story,
12+
]);
913

1014
withResizeObserverMock();
1115

12-
describe('[MultiSelect Component]', () => {
13-
it('renders without crashing', () => {
14-
render(<Default />);
15-
});
16-
});
16+
test.each(testCases)(
17+
`renders %s without crashing`,
18+
async (_storyname, Story) => {
19+
const tree = render(<Story />);
20+
expect(tree.baseElement).toMatchSnapshot();
21+
},
22+
);
23+
24+
test.each(testCases)(
25+
'%s should have no a11y violations',
26+
async (_storyname, Story) => {
27+
const { container } = render(<Story />);
28+
29+
const results = await axe(container);
30+
expect(results).toHaveNoViolations();
31+
},
32+
);

packages/fuselage/src/components/MultiSelect/MultiSelect.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const options: SelectOption[] = [
2828
];
2929

3030
const Template: StoryFn<typeof MultiSelect> = (args) => (
31-
<MultiSelect {...args} />
31+
<MultiSelect aria-label='MultiSelect' {...args} />
3232
);
3333

3434
export const Default: StoryFn<typeof MultiSelect> = Template.bind({});

packages/fuselage/src/components/MultiSelect/MultiSelect.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
144144
return show();
145145
});
146146

147+
const listboxId = props.id ? `${props.id}-listbox` : undefined;
148+
149+
const {
150+
id,
151+
name,
152+
'aria-label': ariaLabel,
153+
'aria-labelledby': ariaLabelledBy,
154+
'aria-describedby': ariaDescribedBy,
155+
'aria-invalid': ariaInvalid,
156+
'aria-required': ariaRequired,
157+
...containerProps
158+
} = props;
159+
147160
return (
148161
<Box
149162
is='div'
@@ -152,7 +165,7 @@ const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
152165
ref={containerRef}
153166
onClick={handleClick}
154167
disabled={disabled}
155-
{...props}
168+
{...containerProps}
156169
>
157170
<FlexItem grow={1}>
158171
<Margins inline='x4'>
@@ -175,8 +188,17 @@ const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
175188
'onBlur': hide,
176189
'onKeyDown': handleKeyDown,
177190
'onKeyUp': handleKeyUp,
191+
'role': 'combobox',
178192
'aria-expanded': visible === AnimatedVisibility.VISIBLE,
179-
'aria-labelledby': props['aria-labelledby'],
193+
'aria-haspopup': 'listbox',
194+
'aria-controls': listboxId,
195+
id,
196+
name,
197+
'aria-label': ariaLabel,
198+
'aria-labelledby': ariaLabelledBy,
199+
'aria-describedby': ariaDescribedBy,
200+
'aria-invalid': ariaInvalid,
201+
'aria-required': ariaRequired,
180202
})}
181203
{internalValue.map((value: SelectOption[0]) => {
182204
const currentOption = options.find(
@@ -237,7 +259,7 @@ const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>(
237259
multiple
238260
filter={filter}
239261
renderItem={renderItem || CheckOption}
240-
role='listbox'
262+
id={listboxId}
241263
options={filteredOptions}
242264
onSelect={internalChanged}
243265
cursor={cursor}

packages/fuselage/src/components/MultiSelect/MultiSelectAnchor.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
AriaAttributes,
23
FocusEventHandler,
34
KeyboardEventHandler,
45
MouseEventHandler,
@@ -15,18 +16,17 @@ type MultiSelectAnchorProps = {
1516
onBlur: FocusEventHandler;
1617
onKeyUp: KeyboardEventHandler;
1718
onKeyDown: KeyboardEventHandler;
18-
};
19+
role?: string;
20+
id?: string;
21+
name?: string;
22+
} & AriaAttributes;
1923

2024
const MultiSelectAnchor = forwardRef<Element, MultiSelectAnchorProps>(
21-
function MultiSelectAnchor(props, ref) {
25+
function MultiSelectAnchor({ children, ...props }, ref) {
2226
return (
23-
<SelectFocus
24-
rcx-input-box--undecorated
25-
ref={ref}
26-
aria-haspopup='listbox'
27-
order={1}
28-
{...props}
29-
/>
27+
<SelectFocus rcx-input-box--undecorated ref={ref} order={1} {...props}>
28+
{children}
29+
</SelectFocus>
3030
);
3131
},
3232
);

packages/fuselage/src/components/MultiSelect/MultiSelectAnchorParams.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ export type MultiSelectAnchorParams = {
1515
onBlur: FocusEventHandler;
1616
onKeyUp: KeyboardEventHandler;
1717
onKeyDown: KeyboardEventHandler;
18+
role?: string;
19+
id?: string;
20+
name?: string;
1821
} & AriaAttributes;

packages/fuselage/src/components/MultiSelect/MultiSelectFilteredAnchor.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
AriaAttributes,
23
FocusEventHandler,
34
FormEvent,
45
KeyboardEventHandler,
@@ -20,7 +21,10 @@ type MultiSelectFilteredAnchorProps = {
2021
onBlur: FocusEventHandler;
2122
onKeyUp: KeyboardEventHandler;
2223
onKeyDown: KeyboardEventHandler;
23-
};
24+
role?: string;
25+
id?: string;
26+
name?: string;
27+
} & AriaAttributes;
2428

2529
const MultiSelectFilteredAnchor = forwardRef<
2630
HTMLInputElement,
@@ -40,7 +44,6 @@ const MultiSelectFilteredAnchor = forwardRef<
4044
}
4145
{...props}
4246
rcx-input-box--undecorated
43-
aria-haspopup='listbox'
4447
order={1}
4548
/>
4649
</FlexItem>

0 commit comments

Comments
 (0)