-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathuseGridSelectionAnnouncement.ts
130 lines (114 loc) · 4.97 KB
/
useGridSelectionAnnouncement.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/*
* Copyright 2022 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {announce} from '@react-aria/live-announcer';
import {Collection, Key, Node, Selection} from '@react-types/shared';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {SelectionManager} from '@react-stately/selection';
import {useEffectEvent, useUpdateEffect} from '@react-aria/utils';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useRef} from 'react';
export interface GridSelectionAnnouncementProps {
/**
* A function that returns the text that should be announced by assistive technology when a row is added or removed from selection.
* @default (key) => state.collection.getItem(key)?.textValue
*/
getRowText?: (key: Key) => string
}
interface GridSelectionState<T> {
/** A collection of items in the grid. */
collection: Collection<Node<T>>,
/** A set of items that are disabled. */
disabledKeys: Set<Key>,
/** A selection manager to read and update multiple selection state. */
selectionManager: SelectionManager
}
export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncementProps, state: GridSelectionState<T>): void {
let {
getRowText = (key) => state.collection.getTextValue?.(key) ?? state.collection.getItem(key)?.textValue
} = props;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/grid');
// Many screen readers do not announce when items in a grid are selected/deselected.
// We do this using an ARIA live region.
let selection = state.selectionManager.rawSelection;
let lastSelection = useRef(selection);
let announceSelectionChange = useEffectEvent(() => {
if (!state.selectionManager.isFocused || selection === lastSelection.current) {
lastSelection.current = selection;
return;
}
let addedKeys = diffSelection(selection, lastSelection.current);
let removedKeys = diffSelection(lastSelection.current, selection);
// If adding or removing a single row from the selection, announce the name of that item.
let isReplace = state.selectionManager.selectionBehavior === 'replace';
let messages: string[] = [];
if ((state.selectionManager.selectedKeys.size === 1 && isReplace)) {
let firstKey = state.selectionManager.selectedKeys.keys().next().value;
if (firstKey != null && state.collection.getItem(firstKey)) {
let currentSelectionText = getRowText(firstKey);
if (currentSelectionText) {
messages.push(stringFormatter.format('selectedItem', {item: currentSelectionText}));
}
}
} else if (addedKeys.size === 1 && removedKeys.size === 0) {
let firstKey = addedKeys.keys().next().value;
if (firstKey != null) {
let addedText = getRowText(firstKey);
if (addedText) {
messages.push(stringFormatter.format('selectedItem', {item: addedText}));
}
}
} else if (removedKeys.size === 1 && addedKeys.size === 0) {
let firstKey = removedKeys.keys().next().value;
if (firstKey != null && state.collection.getItem(firstKey)) {
let removedText = getRowText(firstKey);
if (removedText) {
messages.push(stringFormatter.format('deselectedItem', {item: removedText}));
}
}
}
// Announce how many items are selected, except when selecting the first item.
if (state.selectionManager.selectionMode === 'multiple') {
if (messages.length === 0 || selection === 'all' || selection.size > 1 || lastSelection.current === 'all' || lastSelection.current?.size > 1) {
messages.push(selection === 'all'
? stringFormatter.format('selectedAll')
: stringFormatter.format('selectedCount', {count: selection.size})
);
}
}
if (messages.length > 0) {
announce(messages.join(' '));
}
lastSelection.current = selection;
});
useUpdateEffect(() => {
if (state.selectionManager.isFocused) {
announceSelectionChange();
} else {
// Wait a frame in case the collection is about to become focused (e.g. on mouse down).
let raf = requestAnimationFrame(announceSelectionChange);
return () => cancelAnimationFrame(raf);
}
}, [selection, state.selectionManager.isFocused]);
}
function diffSelection(a: Selection, b: Selection): Set<Key> {
let res = new Set<Key>();
if (a === 'all' || b === 'all') {
return res;
}
for (let key of a.keys()) {
if (!b.has(key)) {
res.add(key);
}
}
return res;
}