Skip to content

Commit 32be2d9

Browse files
reidbarberLFDanLusnowystinger
authored
Use ListView item's description text as aria-describedby (#3121)
* Use item description text as aria-describedby * lint * move description addition to hook * add descriptionProps * fix missing row aria-describedby * use aria-labelledby if description is present * add aria-labelledby check to test * update long text story with better textValue * fixing double announcement for cases with textValue and description * fix test Co-authored-by: Daniel Lu <[email protected]> Co-authored-by: Robert Snow <[email protected]>
1 parent 9e5ed25 commit 32be2d9

File tree

4 files changed

+33
-10
lines changed

4 files changed

+33
-10
lines changed

packages/@react-aria/list/src/useListItem.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {getRowId, listMap} from './utils';
1515
import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, RefObject} from 'react';
1616
import {isFocusVisible} from '@react-aria/interactions';
1717
import type {ListState} from '@react-stately/list';
18-
import {mergeProps} from '@react-aria/utils';
18+
import {mergeProps, useSlotId} from '@react-aria/utils';
1919
import {Node as RSNode} from '@react-types/shared';
2020
import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
2121
import {useLocale} from '@react-aria/i18n';
@@ -33,7 +33,9 @@ export interface ListItemAria extends SelectableItemStates {
3333
/** Props for the list row element. */
3434
rowProps: HTMLAttributes<HTMLElement>,
3535
/** Props for the grid cell element within the list row. */
36-
gridCellProps: HTMLAttributes<HTMLElement>
36+
gridCellProps: HTMLAttributes<HTMLElement>,
37+
/** Props for the list item description element, if any. */
38+
descriptionProps: HTMLAttributes<HTMLElement>
3739
}
3840

3941
/**
@@ -52,6 +54,7 @@ export function useListItem<T>(props: AriaListItemOptions, state: ListState<T>,
5254

5355
let {direction} = useLocale();
5456
let {onAction} = listMap.get(state);
57+
let descriptionId = useSlotId();
5558
let focus = () => {
5659
// Don't shift focus to the row if the active element is a element within the row already
5760
// (e.g. clicking on a row button)
@@ -164,9 +167,10 @@ export function useListItem<T>(props: AriaListItemOptions, state: ListState<T>,
164167
role: 'row',
165168
onKeyDownCapture: onKeyDown,
166169
onFocus,
167-
'aria-label': node.textValue,
170+
'aria-label': node.textValue || undefined,
168171
'aria-selected': state.selectionManager.canSelectItem(node.key) ? state.selectionManager.isSelected(node.key) : undefined,
169172
'aria-disabled': state.selectionManager.isDisabled(node.key) || undefined,
173+
'aria-labelledby': descriptionId && node.textValue ? `${getRowId(state, node.key)} ${descriptionId}` : undefined,
170174
id: getRowId(state, node.key)
171175
});
172176

@@ -182,6 +186,9 @@ export function useListItem<T>(props: AriaListItemOptions, state: ListState<T>,
182186
return {
183187
rowProps,
184188
gridCellProps,
189+
descriptionProps: {
190+
id: descriptionId
191+
},
185192
...itemStates
186193
};
187194
}

packages/@react-spectrum/list/src/ListViewItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ export function ListViewItem<T>(props: ListViewItemProps<T>) {
5050
focusProps: focusWithinProps
5151
} = useFocusRing({within: true});
5252
let {isFocusVisible, focusProps} = useFocusRing();
53-
5453
let {
5554
rowProps,
5655
gridCellProps,
5756
isPressed,
57+
descriptionProps,
5858
isSelected,
5959
isDisabled,
6060
allowsSelection,
@@ -242,7 +242,7 @@ export function ListViewItem<T>(props: ListViewItemProps<T>) {
242242
<SlotProvider
243243
slots={{
244244
text: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-content']},
245-
description: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-description']},
245+
description: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-description'], ...descriptionProps},
246246
icon: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-icon'], size: 'M'},
247247
image: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-image']},
248248
actionButton: {UNSAFE_className: listStyles['react-spectrum-ListViewItem-actions'], isQuiet: true},

packages/@react-spectrum/list/stories/ListView.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,11 @@ storiesOf('ListView', module)
224224
</ListView>
225225
))
226226
.add('long text', args => (
227-
<ListView width="250px" aria-label="long text ListView" {...args}>
228-
<Item textValue="row 1">row 1 with a very very very very very long title</Item>
229-
<Item textValue="row 2">
230-
<Text>Text slot with a really really really long name</Text>
231-
<Text slot="description">Description slot with a really really long name</Text>
227+
<ListView width="250px" {...args}>
228+
<Item textValue="Homeward Bound: The Incredible Journey">Homeward Bound: The Incredible Journey</Item>
229+
<Item textValue="Monsters University">
230+
<Text>Monsters University</Text>
231+
<Text slot="description">As a first grader, Mike Wazowski begins to dream of becoming a Scarer</Text>
232232
</Item>
233233
</ListView>
234234
));

packages/@react-spectrum/list/test/ListView.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {installPointerEvent, triggerPress} from '@react-spectrum/test-utils';
2222
import {Item, ListView} from '../src';
2323
import {Provider} from '@react-spectrum/provider';
2424
import React from 'react';
25+
import {Text} from '@react-spectrum/text';
2526
import {theme} from '@react-spectrum/theme-default';
2627
import userEvent from '@testing-library/user-event';
2728

@@ -592,6 +593,21 @@ describe('ListView', function () {
592593
expect(grid).toHaveAttribute('data-testid', 'test');
593594
});
594595

596+
it('should use item description text as aria-describedby', function () {
597+
let {getAllByRole} = render(
598+
<ListView aria-label="List">
599+
<Item textValue="Label">
600+
<Text>Label</Text>
601+
<Text slot="description">Description</Text>
602+
</Item>
603+
</ListView>
604+
);
605+
606+
let rows = getAllByRole('row');
607+
let description = within(rows[0]).getByText('Description');
608+
expect(rows[0]).toHaveAttribute('aria-labelledby', `${rows[0].id} ${description.id}`);
609+
});
610+
595611
describe('selection', function () {
596612
let items = [
597613
{key: 'foo', label: 'Foo'},

0 commit comments

Comments
 (0)