@react-aria/selection 3.14.0 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/import.mjs +140 -39
- package/dist/main.js +139 -38
- package/dist/main.js.map +1 -1
- package/dist/module.js +140 -39
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +14 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -10
- package/src/ListKeyboardDelegate.ts +166 -18
- package/src/useSelectableCollection.ts +24 -6
- package/src/useSelectableItem.ts +1 -5
- package/src/useTypeSelect.ts +1 -1
- package/src/utils.ts +1 -2
|
@@ -10,23 +10,57 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {Collection, KeyboardDelegate, Node} from '@react-types/shared';
|
|
13
|
+
import {Collection, Direction, KeyboardDelegate, Node, Orientation} from '@react-types/shared';
|
|
14
|
+
import {isScrollable} from '@react-aria/utils';
|
|
14
15
|
import {Key, RefObject} from 'react';
|
|
15
16
|
|
|
17
|
+
interface ListKeyboardDelegateOptions<T> {
|
|
18
|
+
collection: Collection<Node<T>>,
|
|
19
|
+
ref: RefObject<HTMLElement>,
|
|
20
|
+
layout?: 'stack' | 'grid',
|
|
21
|
+
orientation?: Orientation,
|
|
22
|
+
direction?: Direction,
|
|
23
|
+
disabledKeys?: Set<Key>
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
17
27
|
private collection: Collection<Node<T>>;
|
|
18
28
|
private disabledKeys: Set<Key>;
|
|
19
29
|
private ref: RefObject<HTMLElement>;
|
|
20
30
|
private collator: Intl.Collator;
|
|
31
|
+
private layout: 'stack' | 'grid';
|
|
32
|
+
private orientation?: Orientation;
|
|
33
|
+
private direction?: Direction;
|
|
34
|
+
|
|
35
|
+
constructor(collection: Collection<Node<T>>, disabledKeys: Set<Key>, ref: RefObject<HTMLElement>, collator?: Intl.Collator);
|
|
36
|
+
constructor(options: ListKeyboardDelegateOptions<T>);
|
|
37
|
+
constructor(...args: any[]) {
|
|
38
|
+
if (args.length === 1) {
|
|
39
|
+
let opts = args[0] as ListKeyboardDelegateOptions<T>;
|
|
40
|
+
this.collection = opts.collection;
|
|
41
|
+
this.ref = opts.ref;
|
|
42
|
+
this.disabledKeys = opts.disabledKeys || new Set();
|
|
43
|
+
this.orientation = opts.orientation;
|
|
44
|
+
this.direction = opts.direction;
|
|
45
|
+
this.layout = opts.layout || 'stack';
|
|
46
|
+
} else {
|
|
47
|
+
this.collection = args[0];
|
|
48
|
+
this.disabledKeys = args[1];
|
|
49
|
+
this.ref = args[2];
|
|
50
|
+
this.collator = args[3];
|
|
51
|
+
this.layout = 'stack';
|
|
52
|
+
this.orientation = 'vertical';
|
|
53
|
+
}
|
|
21
54
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
55
|
+
// If this is a vertical stack, remove the left/right methods completely
|
|
56
|
+
// so they aren't called by useDroppableCollection.
|
|
57
|
+
if (this.layout === 'stack' && this.orientation === 'vertical') {
|
|
58
|
+
this.getKeyLeftOf = undefined;
|
|
59
|
+
this.getKeyRightOf = undefined;
|
|
60
|
+
}
|
|
27
61
|
}
|
|
28
62
|
|
|
29
|
-
|
|
63
|
+
getNextKey(key: Key) {
|
|
30
64
|
key = this.collection.getKeyAfter(key);
|
|
31
65
|
while (key != null) {
|
|
32
66
|
let item = this.collection.getItem(key);
|
|
@@ -40,7 +74,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
40
74
|
return null;
|
|
41
75
|
}
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
getPreviousKey(key: Key) {
|
|
44
78
|
key = this.collection.getKeyBefore(key);
|
|
45
79
|
while (key != null) {
|
|
46
80
|
let item = this.collection.getItem(key);
|
|
@@ -54,6 +88,82 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
54
88
|
return null;
|
|
55
89
|
}
|
|
56
90
|
|
|
91
|
+
private findKey(
|
|
92
|
+
key: Key,
|
|
93
|
+
nextKey: (key: Key) => Key,
|
|
94
|
+
shouldSkip: (prevRect: DOMRect, itemRect: DOMRect) => boolean
|
|
95
|
+
) {
|
|
96
|
+
let item = this.getItem(key);
|
|
97
|
+
if (!item) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Find the item above or below in the same column.
|
|
102
|
+
let prevRect = item.getBoundingClientRect();
|
|
103
|
+
do {
|
|
104
|
+
key = nextKey(key);
|
|
105
|
+
item = this.getItem(key);
|
|
106
|
+
} while (item && shouldSkip(prevRect, item.getBoundingClientRect()));
|
|
107
|
+
|
|
108
|
+
return key;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private isSameRow(prevRect: DOMRect, itemRect: DOMRect) {
|
|
112
|
+
return prevRect.top === itemRect.top || prevRect.left !== itemRect.left;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private isSameColumn(prevRect: DOMRect, itemRect: DOMRect) {
|
|
116
|
+
return prevRect.left === itemRect.left || prevRect.top !== itemRect.top;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getKeyBelow(key: Key) {
|
|
120
|
+
if (this.layout === 'grid' && this.orientation === 'vertical') {
|
|
121
|
+
return this.findKey(key, (key) => this.getNextKey(key), this.isSameRow);
|
|
122
|
+
} else {
|
|
123
|
+
return this.getNextKey(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getKeyAbove(key: Key) {
|
|
128
|
+
if (this.layout === 'grid' && this.orientation === 'vertical') {
|
|
129
|
+
return this.findKey(key, (key) => this.getPreviousKey(key), this.isSameRow);
|
|
130
|
+
} else {
|
|
131
|
+
return this.getPreviousKey(key);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private getNextColumn(key: Key, right: boolean) {
|
|
136
|
+
return right ? this.getPreviousKey(key) : this.getNextKey(key);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getKeyRightOf(key: Key) {
|
|
140
|
+
if (this.layout === 'grid') {
|
|
141
|
+
if (this.orientation === 'vertical') {
|
|
142
|
+
return this.getNextColumn(key, this.direction === 'rtl');
|
|
143
|
+
} else {
|
|
144
|
+
return this.findKey(key, (key) => this.getNextColumn(key, this.direction === 'rtl'), this.isSameColumn);
|
|
145
|
+
}
|
|
146
|
+
} else if (this.orientation === 'horizontal') {
|
|
147
|
+
return this.getNextColumn(key, this.direction === 'rtl');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getKeyLeftOf(key: Key) {
|
|
154
|
+
if (this.layout === 'grid') {
|
|
155
|
+
if (this.orientation === 'vertical') {
|
|
156
|
+
return this.getNextColumn(key, this.direction === 'ltr');
|
|
157
|
+
} else {
|
|
158
|
+
return this.findKey(key, (key) => this.getNextColumn(key, this.direction === 'ltr'), this.isSameColumn);
|
|
159
|
+
}
|
|
160
|
+
} else if (this.orientation === 'horizontal') {
|
|
161
|
+
return this.getNextColumn(key, this.direction === 'ltr');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
57
167
|
getFirstKey() {
|
|
58
168
|
let key = this.collection.getFirstKey();
|
|
59
169
|
while (key != null) {
|
|
@@ -93,14 +203,33 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
93
203
|
return null;
|
|
94
204
|
}
|
|
95
205
|
|
|
96
|
-
|
|
206
|
+
if (!isScrollable(menu)) {
|
|
207
|
+
return this.getFirstKey();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let containerRect = menu.getBoundingClientRect();
|
|
211
|
+
let itemRect = item.getBoundingClientRect();
|
|
212
|
+
if (this.orientation === 'horizontal') {
|
|
213
|
+
let containerX = containerRect.x - menu.scrollLeft;
|
|
214
|
+
let pageX = Math.max(0, (itemRect.x - containerX) + itemRect.width - containerRect.width);
|
|
215
|
+
|
|
216
|
+
while (item && (itemRect.x - containerX) > pageX) {
|
|
217
|
+
key = this.getKeyAbove(key);
|
|
218
|
+
item = key == null ? null : this.getItem(key);
|
|
219
|
+
itemRect = item?.getBoundingClientRect();
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
let containerY = containerRect.y - menu.scrollTop;
|
|
223
|
+
let pageY = Math.max(0, (itemRect.y - containerY) + itemRect.height - containerRect.height);
|
|
97
224
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
225
|
+
while (item && (itemRect.y - containerY) > pageY) {
|
|
226
|
+
key = this.getKeyAbove(key);
|
|
227
|
+
item = key == null ? null : this.getItem(key);
|
|
228
|
+
itemRect = item?.getBoundingClientRect();
|
|
229
|
+
}
|
|
101
230
|
}
|
|
102
231
|
|
|
103
|
-
return key;
|
|
232
|
+
return key ?? this.getFirstKey();
|
|
104
233
|
}
|
|
105
234
|
|
|
106
235
|
getKeyPageBelow(key: Key) {
|
|
@@ -110,14 +239,33 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
110
239
|
return null;
|
|
111
240
|
}
|
|
112
241
|
|
|
113
|
-
|
|
242
|
+
if (!isScrollable(menu)) {
|
|
243
|
+
return this.getLastKey();
|
|
244
|
+
}
|
|
114
245
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
246
|
+
let containerRect = menu.getBoundingClientRect();
|
|
247
|
+
let itemRect = item.getBoundingClientRect();
|
|
248
|
+
if (this.orientation === 'horizontal') {
|
|
249
|
+
let containerX = containerRect.x - menu.scrollLeft;
|
|
250
|
+
let pageX = Math.min(menu.scrollWidth, (itemRect.x - containerX) - itemRect.width + containerRect.width);
|
|
251
|
+
|
|
252
|
+
while (item && (itemRect.x - containerX) < pageX) {
|
|
253
|
+
key = this.getKeyBelow(key);
|
|
254
|
+
item = key == null ? null : this.getItem(key);
|
|
255
|
+
itemRect = item?.getBoundingClientRect();
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
let containerY = containerRect.y - menu.scrollTop;
|
|
259
|
+
let pageY = Math.min(menu.scrollHeight, (itemRect.y - containerY) - itemRect.height + containerRect.height);
|
|
260
|
+
|
|
261
|
+
while (item && (itemRect.y - containerY) < pageY) {
|
|
262
|
+
key = this.getKeyBelow(key);
|
|
263
|
+
item = key == null ? null : this.getItem(key);
|
|
264
|
+
itemRect = item?.getBoundingClientRect();
|
|
265
|
+
}
|
|
118
266
|
}
|
|
119
267
|
|
|
120
|
-
return key;
|
|
268
|
+
return key ?? this.getLastKey();
|
|
121
269
|
}
|
|
122
270
|
|
|
123
271
|
getKeyForSearch(search: string, fromKey?: Key) {
|
|
@@ -165,6 +165,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
165
165
|
if (delegate.getKeyLeftOf) {
|
|
166
166
|
e.preventDefault();
|
|
167
167
|
let nextKey = delegate.getKeyLeftOf(manager.focusedKey);
|
|
168
|
+
if (nextKey == null && shouldFocusWrap) {
|
|
169
|
+
nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey);
|
|
170
|
+
}
|
|
168
171
|
navigateToKey(nextKey, direction === 'rtl' ? 'first' : 'last');
|
|
169
172
|
}
|
|
170
173
|
break;
|
|
@@ -173,6 +176,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
173
176
|
if (delegate.getKeyRightOf) {
|
|
174
177
|
e.preventDefault();
|
|
175
178
|
let nextKey = delegate.getKeyRightOf(manager.focusedKey);
|
|
179
|
+
if (nextKey == null && shouldFocusWrap) {
|
|
180
|
+
nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey);
|
|
181
|
+
}
|
|
176
182
|
navigateToKey(nextKey, direction === 'rtl' ? 'last' : 'first');
|
|
177
183
|
}
|
|
178
184
|
break;
|
|
@@ -314,7 +320,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
314
320
|
if (element) {
|
|
315
321
|
// This prevents a flash of focus on the first/last element in the collection, or the collection itself.
|
|
316
322
|
focusWithoutScrolling(element);
|
|
317
|
-
|
|
323
|
+
|
|
324
|
+
let modality = getInteractionModality();
|
|
325
|
+
if (modality === 'keyboard') {
|
|
326
|
+
scrollIntoViewport(element, {containingElement: ref.current});
|
|
327
|
+
}
|
|
318
328
|
}
|
|
319
329
|
}
|
|
320
330
|
};
|
|
@@ -358,17 +368,25 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
358
368
|
|
|
359
369
|
// If not virtualized, scroll the focused element into view when the focusedKey changes.
|
|
360
370
|
// When virtualized, Virtualizer handles this internally.
|
|
371
|
+
let lastFocusedKey = useRef(manager.focusedKey);
|
|
361
372
|
useEffect(() => {
|
|
362
373
|
let modality = getInteractionModality();
|
|
363
|
-
if (
|
|
374
|
+
if (manager.isFocused && manager.focusedKey != null && scrollRef?.current) {
|
|
364
375
|
let element = scrollRef.current.querySelector(`[data-key="${manager.focusedKey}"]`) as HTMLElement;
|
|
365
|
-
if (element) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
scrollIntoViewport(element, {containingElement: ref.current});
|
|
376
|
+
if (element && modality === 'keyboard') {
|
|
377
|
+
if (!isVirtualized) {
|
|
378
|
+
scrollIntoView(scrollRef.current, element);
|
|
369
379
|
}
|
|
380
|
+
scrollIntoViewport(element, {containingElement: ref.current});
|
|
370
381
|
}
|
|
371
382
|
}
|
|
383
|
+
|
|
384
|
+
// If the focused key becomes null (e.g. the last item is deleted), focus the whole collection.
|
|
385
|
+
if (manager.isFocused && manager.focusedKey == null && lastFocusedKey.current != null) {
|
|
386
|
+
focusSafely(ref.current);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
lastFocusedKey.current = manager.focusedKey;
|
|
372
390
|
}, [isVirtualized, scrollRef, manager.focusedKey, manager.isFocused, ref]);
|
|
373
391
|
|
|
374
392
|
let handlers = {
|
package/src/useSelectableItem.ts
CHANGED
|
@@ -103,7 +103,6 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
103
103
|
key,
|
|
104
104
|
ref,
|
|
105
105
|
shouldSelectOnPressUp,
|
|
106
|
-
isVirtualized,
|
|
107
106
|
shouldUseVirtualFocus,
|
|
108
107
|
focus,
|
|
109
108
|
isDisabled,
|
|
@@ -266,10 +265,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
266
265
|
};
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
|
|
270
|
-
itemProps['data-key'] = key;
|
|
271
|
-
}
|
|
272
|
-
|
|
268
|
+
itemProps['data-key'] = key;
|
|
273
269
|
itemPressProps.preventFocusOnPress = shouldUseVirtualFocus;
|
|
274
270
|
let {pressProps, isPressed} = usePress(itemPressProps);
|
|
275
271
|
|
package/src/useTypeSelect.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
|
|
|
53
53
|
|
|
54
54
|
let onKeyDown = (e: KeyboardEvent) => {
|
|
55
55
|
let character = getStringForKey(e.key);
|
|
56
|
-
if (!character || e.ctrlKey || e.metaKey) {
|
|
56
|
+
if (!character || e.ctrlKey || e.metaKey || !e.currentTarget.contains(e.target as HTMLElement)) {
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
|
package/src/utils.ts
CHANGED
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {isAppleDevice} from '@react-aria/utils';
|
|
14
|
-
import {isMac} from '@react-aria/utils';
|
|
13
|
+
import {isAppleDevice, isMac} from '@react-aria/utils';
|
|
15
14
|
|
|
16
15
|
interface Event {
|
|
17
16
|
altKey: boolean,
|