Skip to content

Commit 56e1cf0

Browse files
snowystingerdannifyLFDanLu
authored
Announce removed selections due to table navigation (#2441)
* Announce removed selections due to table navigation * Update our last selected when not focused * code review * renaming table breadcrumb story and component Co-authored-by: Danni <[email protected]> Co-authored-by: Daniel Lu <[email protected]> Co-authored-by: Daniel Lu <[email protected]>
1 parent 9ca62f9 commit 56e1cf0

File tree

4 files changed

+171
-12
lines changed

4 files changed

+171
-12
lines changed

packages/@react-aria/grid/src/useGrid.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
117117
// Do not do this when using selectionBehavior = 'replace' to avoid selection announcements
118118
// every time the user presses the arrow keys.
119119
if (!state.selectionManager.isFocused || state.selectionManager.selectionBehavior === 'replace') {
120+
lastSelection.current = selection;
121+
120122
return;
121123
}
122124

@@ -131,9 +133,11 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
131133
messages.push(formatMessage('selectedItem', {item: addedText}));
132134
}
133135
} else if (removedKeys.size === 1 && addedKeys.size === 0) {
134-
let removedText = getRowText(removedKeys.keys().next().value);
135-
if (removedText) {
136-
messages.push(formatMessage('deselectedItem', {item: removedText}));
136+
if (state.collection.getItem(removedKeys.keys().next().value)) {
137+
let removedText = getRowText(removedKeys.keys().next().value);
138+
if (removedText) {
139+
messages.push(formatMessage('deselectedItem', {item: removedText}));
140+
}
137141
}
138142
}
139143

packages/@react-aria/table/src/useTable.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@ export function useTable<T>(props: TableProps<T>, state: TableState<T>, ref: Ref
6464
...props,
6565
id,
6666
keyboardDelegate: delegate,
67-
getRowText(key) {
67+
getRowText(key): string {
6868
let added = state.collection.getItem(key);
69+
if (!added) {
70+
return '';
71+
}
6972

7073
// If the row has a textValue, use that.
7174
if (added.textValue != null) {

packages/@react-spectrum/table/stories/Table.stories.tsx

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {action} from '@storybook/addon-actions';
1414
import {ActionButton, Button} from '@react-spectrum/button';
1515
import Add from '@spectrum-icons/workflow/Add';
16+
import {Breadcrumbs, Item} from '@react-spectrum/breadcrumbs';
1617
import {ButtonGroup} from '@react-spectrum/buttongroup';
1718
import {Cell, Column, Row, TableBody, TableHeader, TableView} from '../';
1819
import {Content} from '@react-spectrum/view';
@@ -25,10 +26,10 @@ import {Heading} from '@react-spectrum/text';
2526
import {HidingColumns} from './HidingColumns';
2627
import {IllustratedMessage} from '@react-spectrum/illustratedmessage';
2728
import {Link} from '@react-spectrum/link';
29+
import {LoadingState, SelectionMode} from '@react-types/shared';
2830
import {Radio, RadioGroup} from '@react-spectrum/radio';
2931
import React, {Key, useState} from 'react';
3032
import {SearchField} from '@react-spectrum/searchfield';
31-
import {SelectionMode} from '@react-types/shared';
3233
import {storiesOf} from '@storybook/react';
3334
import {Switch} from '@react-spectrum/switch';
3435
import {TextField} from '@react-spectrum/textfield';
@@ -1020,7 +1021,8 @@ storiesOf('TableView', module)
10201021
</TableBody>
10211022
</TableView>
10221023
)
1023-
);
1024+
)
1025+
.add('table with breadcrumb navigation', () => <TableWithBreadcrumbs />);
10241026

10251027
function AsyncLoadingExample() {
10261028
interface Item {
@@ -1299,3 +1301,73 @@ function ChangableSelectionMode() {
12991301
</Flex>
13001302
);
13011303
}
1304+
1305+
export function TableWithBreadcrumbs() {
1306+
const fs = [
1307+
{key: 'a', name: 'Folder A', type: 'folder'},
1308+
{key: 'b', name: 'File B', value: '10 MB'},
1309+
{key: 'c', name: 'File C', value: '10 MB', parent: 'a'},
1310+
{key: 'd', name: 'File D', value: '10 MB', parent: 'a'}
1311+
];
1312+
1313+
const [loadingState, setLoadingState] = useState<LoadingState>('idle' as 'idle');
1314+
const [selection, setSelection] = useState<'all' | Iterable<Key>>(new Set([]));
1315+
const [items, setItems] = useState(() => fs.filter(item => !item.parent));
1316+
1317+
const changeFolder = (folder) => {
1318+
setItems([]);
1319+
setLoadingState('loading' as 'loading');
1320+
1321+
// mimic loading behavior
1322+
setTimeout(() => {
1323+
setLoadingState('idle');
1324+
setItems(fs.filter(item => folder ? item.parent === folder : !item.parent));
1325+
}, 700);
1326+
setSelection(new Set([]));
1327+
};
1328+
1329+
return (
1330+
<Flex direction="column" width="400px">
1331+
<div>The TableView should not error if row selection changes due to items changes from external navigation (breadcrumbs).</div>
1332+
<Breadcrumbs
1333+
onAction={item => {
1334+
if (item === 'root') {
1335+
changeFolder('');
1336+
}
1337+
}}>
1338+
<Item key="root">root</Item>
1339+
<Item>a</Item>
1340+
</Breadcrumbs>
1341+
<TableView
1342+
width="400px"
1343+
aria-label="table"
1344+
selectedKeys={selection}
1345+
selectionMode="multiple"
1346+
onSelectionChange={(sel) => setSelection(sel)}>
1347+
<TableHeader>
1348+
<Column key="name" isRowHeader>Name</Column>
1349+
<Column key="value">Value</Column>
1350+
</TableHeader>
1351+
<TableBody items={items} loadingState={loadingState}>
1352+
{(item) => (
1353+
<Row key={item.key}>
1354+
{(column) => {
1355+
if (item.type === 'folder' && column === 'name') {
1356+
return (
1357+
<Cell textValue={item[column]}>
1358+
<Link onPress={() => changeFolder(item.key)}>
1359+
{item[column]}
1360+
</Link>
1361+
</Cell>
1362+
);
1363+
}
1364+
return <Cell>{item[column]}</Cell>;
1365+
}}
1366+
</Row>
1367+
)}
1368+
</TableBody>
1369+
</TableView>
1370+
<ActionButton onPress={() => setSelection(items.some(item => item.key === 'd') ? new Set(['d']) : new Set([]))}>Select D</ActionButton>
1371+
</Flex>
1372+
);
1373+
}

packages/@react-spectrum/table/test/Table.test.js

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {Link} from '@react-spectrum/link';
2929
import {Provider} from '@react-spectrum/provider';
3030
import React from 'react';
3131
import {Switch} from '@react-spectrum/switch';
32+
import {TableWithBreadcrumbs} from '../stories/Table.stories';
3233
import {TextField} from '@react-spectrum/textfield';
3334
import {theme} from '@react-spectrum/theme-default';
3435
import {typeText} from '@react-spectrum/test-utils';
@@ -2280,6 +2281,85 @@ describe('TableView', function () {
22802281
});
22812282
});
22822283

2284+
it('can announce deselect even when items are swapped out completely', () => {
2285+
let tree = render(<TableWithBreadcrumbs />);
2286+
2287+
let row = tree.getAllByRole('row')[2];
2288+
triggerPress(row);
2289+
expect(announce).toHaveBeenLastCalledWith('File B selected.');
2290+
2291+
let link = tree.getAllByRole('link')[1];
2292+
triggerPress(link);
2293+
2294+
expect(announce).toHaveBeenLastCalledWith('No items selected.');
2295+
expect(announce).toHaveBeenCalledTimes(2);
2296+
});
2297+
2298+
it('will not announce deselect caused by breadcrumb navigation', () => {
2299+
let tree = render(<TableWithBreadcrumbs />);
2300+
2301+
let link = tree.getAllByRole('link')[1];
2302+
triggerPress(link);
2303+
2304+
act(() => {
2305+
// TableWithBreadcrumbs has a setTimeout to load the results of the link navigation on Folder A
2306+
jest.runAllTimers();
2307+
});
2308+
let row = tree.getAllByRole('row')[1];
2309+
triggerPress(row);
2310+
expect(announce).toHaveBeenLastCalledWith('File C selected.');
2311+
expect(announce).toHaveBeenCalledTimes(2);
2312+
2313+
// breadcrumb root
2314+
link = tree.getAllByRole('link')[0];
2315+
triggerPress(link);
2316+
2317+
// focus isn't on the table, so we don't announce that it has been deselected
2318+
expect(announce).toHaveBeenCalledTimes(2);
2319+
});
2320+
2321+
it('updates even if not focused', () => {
2322+
let tree = render(<TableWithBreadcrumbs />);
2323+
2324+
let link = tree.getAllByRole('link')[1];
2325+
triggerPress(link);
2326+
2327+
act(() => {
2328+
// TableWithBreadcrumbs has a setTimeout to load the results of the link navigation on Folder A
2329+
jest.runAllTimers();
2330+
});
2331+
let row = tree.getAllByRole('row')[1];
2332+
triggerPress(row);
2333+
expect(announce).toHaveBeenLastCalledWith('File C selected.');
2334+
expect(announce).toHaveBeenCalledTimes(2);
2335+
let button = tree.getAllByRole('button')[0];
2336+
triggerPress(button);
2337+
expect(announce).toHaveBeenCalledTimes(2);
2338+
2339+
// breadcrumb root
2340+
link = tree.getAllByRole('menuitemradio')[0];
2341+
triggerPress(link);
2342+
2343+
act(() => {
2344+
// TableWithBreadcrumbs has a setTimeout to load the results of the link navigation on Folder A
2345+
jest.runAllTimers();
2346+
});
2347+
2348+
// focus isn't on the table, so we don't announce that it has been deselected
2349+
expect(announce).toHaveBeenCalledTimes(2);
2350+
2351+
link = tree.getAllByRole('link')[1];
2352+
triggerPress(link);
2353+
2354+
act(() => {
2355+
// TableWithBreadcrumbs has a setTimeout to load the results of the link navigation on Folder A
2356+
jest.runAllTimers();
2357+
});
2358+
2359+
expect(announce).toHaveBeenCalledTimes(3);
2360+
expect(announce).toHaveBeenLastCalledWith('No items selected.');
2361+
});
2362+
22832363
describe('selectionStyle highlight', function () {
22842364
installPointerEvent();
22852365

@@ -2747,7 +2827,7 @@ describe('TableView', function () {
27472827
});
27482828

27492829
describe('press/hover interactions and selection mode', function () {
2750-
let TableExample = (props) => (
2830+
let TableWithBreadcrumbs = (props) => (
27512831
<TableView aria-label="Table" {...props}>
27522832
<TableHeader columns={columns}>
27532833
{column => <Column>{column.name}</Column>}
@@ -2763,15 +2843,15 @@ describe('TableView', function () {
27632843
);
27642844

27652845
it('displays pressed/hover styles when row is pressed/hovered and selection mode is not "none"', function () {
2766-
let tree = render(<TableExample selectionMode="multiple" />);
2846+
let tree = render(<TableWithBreadcrumbs selectionMode="multiple" />);
27672847

27682848
let row = tree.getAllByRole('row')[1];
27692849
fireEvent.mouseDown(row, {detail: 1});
27702850
expect(row.className.includes('is-active')).toBeTruthy();
27712851
fireEvent.mouseEnter(row);
27722852
expect(row.className.includes('is-hovered')).toBeTruthy();
27732853

2774-
rerender(tree, <TableExample selectionMode="single" />);
2854+
rerender(tree, <TableWithBreadcrumbs selectionMode="single" />);
27752855
row = tree.getAllByRole('row')[1];
27762856
fireEvent.mouseDown(row, {detail: 1});
27772857
expect(row.className.includes('is-active')).toBeTruthy();
@@ -2780,7 +2860,7 @@ describe('TableView', function () {
27802860
});
27812861

27822862
it('doesn\'t show pressed/hover styles when row is pressed/hovered and selection mode is "none"', function () {
2783-
let tree = render(<TableExample selectionMode="none" />);
2863+
let tree = render(<TableWithBreadcrumbs selectionMode="none" />);
27842864

27852865
let row = tree.getAllByRole('row')[1];
27862866
fireEvent.mouseDown(row, {detail: 1});
@@ -3084,7 +3164,7 @@ describe('TableView', function () {
30843164
});
30853165

30863166
describe('with dialog trigger', function () {
3087-
let TableExample = (props) => (
3167+
let TableWithBreadcrumbs = (props) => (
30883168
<TableView aria-label="TableView with static contents" selectionMode="multiple" width={300} height={200} {...props}>
30893169
<TableHeader>
30903170
<Column key="foo">Foo</Column>
@@ -3119,7 +3199,7 @@ describe('TableView', function () {
31193199
);
31203200

31213201
it('arrow keys interactions don\'t move the focus away from the textfield in the dialog', function () {
3122-
let tree = render(<TableExample />);
3202+
let tree = render(<TableWithBreadcrumbs />);
31233203
let table = tree.getByRole('grid');
31243204
let rows = within(table).getAllByRole('row');
31253205
expect(rows).toHaveLength(2);

0 commit comments

Comments
 (0)