Skip to content

Commit c5d2e27

Browse files
author
Michael Jordan
authored
fix(#4036): Section headings within ListBox and Menu are not accessible on mobile screen readers (#4037)
* fix(#4036): Section headings within ListBox and Menu are not accessible on mobile screen readers
1 parent fc1460f commit c5d2e27

File tree

8 files changed

+22
-28
lines changed

8 files changed

+22
-28
lines changed

packages/@react-aria/listbox/docs/useListBox.mdx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,25 +287,20 @@ are correct.
287287

288288
```tsx example export=true render=false
289289
import {useListBoxSection} from '@react-aria/listbox';
290-
import {useSeparator} from '@react-aria/separator';
291290

292291
function ListBoxSection({section, state}) {
293292
let {itemProps, headingProps, groupProps} = useListBoxSection({
294293
heading: section.rendered,
295294
'aria-label': section['aria-label']
296295
});
297296

298-
let {separatorProps} = useSeparator({
299-
elementType: 'li'
300-
});
301-
302-
// If the section is not the first, add a separator element.
297+
// If the section is not the first, add a separator element to provide visual separation.
303298
// The heading is rendered inside an <li> element, which contains
304299
// a <ul> with the child items.
305300
return <>
306301
{section.key !== state.collection.getFirstKey() &&
307302
<li
308-
{...separatorProps}
303+
role="presentation"
309304
style={{
310305
borderTop: '1px solid gray',
311306
margin: '2px 5px'

packages/@react-aria/listbox/src/useListBoxSection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export function useListBoxSection(props: AriaListBoxSectionProps): ListBoxSectio
4747
},
4848
headingProps: heading ? {
4949
// Techincally, listbox cannot contain headings according to ARIA.
50-
// We hide the heading from assistive technology, and only use it
51-
// as a label for the nested group.
50+
// We hide the heading from assistive technology, using role="presentation",
51+
// and only use it as a visual label for the nested group.
5252
id: headingId,
53-
'aria-hidden': true
53+
role: 'presentation'
5454
} : {},
5555
groupProps: {
5656
role: 'group',

packages/@react-aria/menu/src/useMenuSection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export function useMenuSection(props: AriaMenuSectionProps): MenuSectionAria {
4747
},
4848
headingProps: heading ? {
4949
// Techincally, menus cannot contain headings according to ARIA.
50-
// We hide the heading from assistive technology, and only use it
51-
// as a label for the nested group.
50+
// We hide the heading from assistive technology, using role="presentation",
51+
// and only use it as a label for the nested group.
5252
id: headingId,
53-
'aria-hidden': true
53+
role: 'presentation'
5454
} : {},
5555
groupProps: {
5656
role: 'group',

packages/@react-spectrum/combobox/test/ComboBox.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3175,9 +3175,9 @@ describe('ComboBox', function () {
31753175
expect(items).toHaveLength(6);
31763176
expect(groups).toHaveLength(2);
31773177
expect(groups[0]).toHaveAttribute('aria-labelledby', getByText('Section One').id);
3178-
expect(getByText('Section One')).toHaveAttribute('aria-hidden', 'true');
3178+
expect(getByText('Section One')).toHaveAttribute('role', 'presentation');
31793179
expect(groups[1]).toHaveAttribute('aria-labelledby', getByText('Section Two').id);
3180-
expect(getByText('Section Two')).toHaveAttribute('aria-hidden', 'true');
3180+
expect(getByText('Section Two')).toHaveAttribute('role', 'presentation');
31813181
expect(document.activeElement).toBe(combobox);
31823182
expect(combobox).toHaveAttribute('aria-activedescendant', items[0].id);
31833183

packages/@react-spectrum/listbox/src/ListBoxSection.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import React, {Fragment, ReactNode, useContext, useRef} from 'react';
1919
import styles from '@adobe/spectrum-css-temp/components/menu/vars.css';
2020
import {useListBoxSection} from '@react-aria/listbox';
2121
import {useLocale} from '@react-aria/i18n';
22-
import {useSeparator} from '@react-aria/separator';
2322

2423
interface ListBoxSectionProps<T> extends Omit<VirtualizerItemOptions, 'ref'> {
2524
headerLayoutInfo: LayoutInfo,
@@ -35,10 +34,6 @@ export function ListBoxSection<T>(props: ListBoxSectionProps<T>) {
3534
'aria-label': item['aria-label']
3635
});
3736

38-
let {separatorProps} = useSeparator({
39-
elementType: 'li'
40-
});
41-
4237
let headerRef = useRef();
4338
useVirtualizerItem({
4439
layoutInfo: headerLayoutInfo,
@@ -54,7 +49,7 @@ export function ListBoxSection<T>(props: ListBoxSectionProps<T>) {
5449
<div role="presentation" ref={headerRef} style={layoutInfoToStyle(headerLayoutInfo, direction)}>
5550
{item.key !== state.collection.getFirstKey() &&
5651
<div
57-
{...separatorProps}
52+
role="presentation"
5853
className={classNames(
5954
styles,
6055
'spectrum-Menu-divider'

packages/@react-spectrum/listbox/test/ListBox.test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,15 @@ describe('ListBox', function () {
9898
expect(section).toHaveAttribute('aria-labelledby');
9999
let heading = document.getElementById(section.getAttribute('aria-labelledby'));
100100
expect(heading).toBeTruthy();
101-
expect(heading).toHaveAttribute('aria-hidden', 'true');
102-
}
101+
expect(heading).toHaveAttribute('role', 'presentation');
103102

104-
let dividers = within(listbox).getAllByRole('separator');
105-
expect(dividers.length).toBe(withSection.length - 1);
103+
// Separator should render for sections after first section
104+
if (section !== sections[0]) {
105+
let divider = heading.previousElementSibling;
106+
expect(divider).toHaveAttribute('role', 'presentation');
107+
expect(divider).toHaveClass('spectrum-Menu-divider');
108+
}
109+
}
106110

107111
let items = within(listbox).getAllByRole('option');
108112
expect(items.length).toBe(withSection.reduce((acc, curr) => (acc + curr.children.length), 0));

packages/@react-spectrum/menu/test/Menu.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('Menu', function () {
8787
expect(section).toHaveAttribute('aria-labelledby');
8888
let heading = document.getElementById(section.getAttribute('aria-labelledby'));
8989
expect(heading).toBeTruthy();
90-
expect(heading).toHaveAttribute('aria-hidden', 'true');
90+
expect(heading).toHaveAttribute('role', 'presentation');
9191
}
9292

9393
let dividers = within(menu).getAllByRole('separator');

packages/@react-spectrum/picker/test/Picker.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,9 +1470,9 @@ describe('Picker', function () {
14701470
expect(items[5]).toHaveAttribute('aria-labelledby', within(items[5]).getByText('Floof').id);
14711471
expect(groups[1]).toContainElement(items[5]);
14721472

1473-
expect(getByText('Section 1')).toHaveAttribute('aria-hidden', 'true');
1473+
expect(getByText('Section 1')).toHaveAttribute('role', 'presentation');
14741474
expect(groups[1]).toHaveAttribute('aria-labelledby', getByText('Section 2').id);
1475-
expect(getByText('Section 2')).toHaveAttribute('aria-hidden', 'true');
1475+
expect(getByText('Section 2')).toHaveAttribute('role', 'presentation');
14761476

14771477
expect(document.activeElement).toBe(listbox);
14781478

0 commit comments

Comments
 (0)