@react-aria/selection 3.21.0 → 3.23.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/DOMLayoutDelegate.main.js +5 -2
- package/dist/DOMLayoutDelegate.main.js.map +1 -1
- package/dist/DOMLayoutDelegate.mjs +5 -2
- package/dist/DOMLayoutDelegate.module.js +5 -2
- package/dist/DOMLayoutDelegate.module.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/useSelectableCollection.main.js +92 -31
- package/dist/useSelectableCollection.main.js.map +1 -1
- package/dist/useSelectableCollection.mjs +92 -31
- package/dist/useSelectableCollection.module.js +92 -31
- package/dist/useSelectableCollection.module.js.map +1 -1
- package/dist/useSelectableItem.main.js +41 -13
- package/dist/useSelectableItem.main.js.map +1 -1
- package/dist/useSelectableItem.mjs +43 -15
- package/dist/useSelectableItem.module.js +43 -15
- package/dist/useSelectableItem.module.js.map +1 -1
- package/dist/utils.main.js +18 -4
- package/dist/utils.main.js.map +1 -1
- package/dist/utils.mjs +17 -5
- package/dist/utils.module.js +17 -5
- package/dist/utils.module.js.map +1 -1
- package/package.json +8 -8
- package/src/DOMLayoutDelegate.ts +2 -1
- package/src/useSelectableCollection.ts +103 -26
- package/src/useSelectableItem.ts +51 -17
- package/src/utils.ts +19 -5
package/dist/utils.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {isAppleDevice as $jUnAJ$isAppleDevice,
|
|
1
|
+
import {isAppleDevice as $jUnAJ$isAppleDevice, useId as $jUnAJ$useId} from "@react-aria/utils";
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* Copyright 2020 Adobe. All rights reserved.
|
|
@@ -16,11 +16,23 @@ function $feb5ffebff200149$export$d3e3bd3e26688c04(e) {
|
|
|
16
16
|
// On Windows and Ubuntu, Alt + Space has a system wide meaning.
|
|
17
17
|
return (0, $jUnAJ$isAppleDevice)() ? e.altKey : e.ctrlKey;
|
|
18
18
|
}
|
|
19
|
-
function $feb5ffebff200149$export$
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
function $feb5ffebff200149$export$c3d8340acf92597f(collectionRef, key) {
|
|
20
|
+
var _collectionRef_current, _collectionRef_current1;
|
|
21
|
+
let selector = `[data-key="${CSS.escape(String(key))}"]`;
|
|
22
|
+
let collection = (_collectionRef_current = collectionRef.current) === null || _collectionRef_current === void 0 ? void 0 : _collectionRef_current.dataset.collection;
|
|
23
|
+
if (collection) selector = `[data-collection="${CSS.escape(collection)}"]${selector}`;
|
|
24
|
+
return (_collectionRef_current1 = collectionRef.current) === null || _collectionRef_current1 === void 0 ? void 0 : _collectionRef_current1.querySelector(selector);
|
|
25
|
+
}
|
|
26
|
+
const $feb5ffebff200149$var$collectionMap = new WeakMap();
|
|
27
|
+
function $feb5ffebff200149$export$881eb0d9f3605d9d(collection) {
|
|
28
|
+
let id = (0, $jUnAJ$useId)();
|
|
29
|
+
$feb5ffebff200149$var$collectionMap.set(collection, id);
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
function $feb5ffebff200149$export$6aeb1680a0ae8741(collection) {
|
|
33
|
+
return $feb5ffebff200149$var$collectionMap.get(collection);
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
|
|
25
|
-
export {$feb5ffebff200149$export$d3e3bd3e26688c04 as isNonContiguousSelectionModifier, $feb5ffebff200149$export$
|
|
37
|
+
export {$feb5ffebff200149$export$d3e3bd3e26688c04 as isNonContiguousSelectionModifier, $feb5ffebff200149$export$c3d8340acf92597f as getItemElement, $feb5ffebff200149$export$881eb0d9f3605d9d as useCollectionId, $feb5ffebff200149$export$6aeb1680a0ae8741 as getCollectionId};
|
|
26
38
|
//# sourceMappingURL=utils.module.js.map
|
package/dist/utils.module.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {isAppleDevice as $jUnAJ$isAppleDevice,
|
|
1
|
+
import {isAppleDevice as $jUnAJ$isAppleDevice, useId as $jUnAJ$useId} from "@react-aria/utils";
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
* Copyright 2020 Adobe. All rights reserved.
|
|
@@ -16,11 +16,23 @@ function $feb5ffebff200149$export$d3e3bd3e26688c04(e) {
|
|
|
16
16
|
// On Windows and Ubuntu, Alt + Space has a system wide meaning.
|
|
17
17
|
return (0, $jUnAJ$isAppleDevice)() ? e.altKey : e.ctrlKey;
|
|
18
18
|
}
|
|
19
|
-
function $feb5ffebff200149$export$
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
function $feb5ffebff200149$export$c3d8340acf92597f(collectionRef, key) {
|
|
20
|
+
var _collectionRef_current, _collectionRef_current1;
|
|
21
|
+
let selector = `[data-key="${CSS.escape(String(key))}"]`;
|
|
22
|
+
let collection = (_collectionRef_current = collectionRef.current) === null || _collectionRef_current === void 0 ? void 0 : _collectionRef_current.dataset.collection;
|
|
23
|
+
if (collection) selector = `[data-collection="${CSS.escape(collection)}"]${selector}`;
|
|
24
|
+
return (_collectionRef_current1 = collectionRef.current) === null || _collectionRef_current1 === void 0 ? void 0 : _collectionRef_current1.querySelector(selector);
|
|
25
|
+
}
|
|
26
|
+
const $feb5ffebff200149$var$collectionMap = new WeakMap();
|
|
27
|
+
function $feb5ffebff200149$export$881eb0d9f3605d9d(collection) {
|
|
28
|
+
let id = (0, $jUnAJ$useId)();
|
|
29
|
+
$feb5ffebff200149$var$collectionMap.set(collection, id);
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
function $feb5ffebff200149$export$6aeb1680a0ae8741(collection) {
|
|
33
|
+
return $feb5ffebff200149$var$collectionMap.get(collection);
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
|
|
25
|
-
export {$feb5ffebff200149$export$d3e3bd3e26688c04 as isNonContiguousSelectionModifier, $feb5ffebff200149$export$
|
|
37
|
+
export {$feb5ffebff200149$export$d3e3bd3e26688c04 as isNonContiguousSelectionModifier, $feb5ffebff200149$export$c3d8340acf92597f as getItemElement, $feb5ffebff200149$export$881eb0d9f3605d9d as useCollectionId, $feb5ffebff200149$export$6aeb1680a0ae8741 as getCollectionId};
|
|
26
38
|
//# sourceMappingURL=utils.module.js.map
|
package/dist/utils.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;AAAA;;;;;;;;;;CAUC;
|
|
1
|
+
{"mappings":";;AAAA;;;;;;;;;;CAUC;AAYM,SAAS,0CAAiC,CAAQ;IACvD,qFAAqF;IACrF,gEAAgE;IAChE,OAAO,CAAA,GAAA,oBAAY,MAAM,EAAE,MAAM,GAAG,EAAE,OAAO;AAC/C;AAEO,SAAS,0CAAe,aAA4C,EAAE,GAAQ;QAElE,wBAIV;IALP,IAAI,WAAW,CAAC,WAAW,EAAE,IAAI,MAAM,CAAC,OAAO,MAAM,EAAE,CAAC;IACxD,IAAI,cAAa,yBAAA,cAAc,OAAO,cAArB,6CAAA,uBAAuB,OAAO,CAAC,UAAU;IAC1D,IAAI,YACF,WAAW,CAAC,kBAAkB,EAAE,IAAI,MAAM,CAAC,YAAY,EAAE,EAAE,UAAU;IAEvE,QAAO,0BAAA,cAAc,OAAO,cAArB,8CAAA,wBAAuB,aAAa,CAAC;AAC9C;AAEA,MAAM,sCAAgB,IAAI;AACnB,SAAS,0CAAgB,UAA2B;IACzD,IAAI,KAAK,CAAA,GAAA,YAAI;IACb,oCAAc,GAAG,CAAC,YAAY;IAC9B,OAAO;AACT;AAEO,SAAS,0CAAgB,UAA2B;IACzD,OAAO,oCAAc,GAAG,CAAC;AAC3B","sources":["packages/@react-aria/selection/src/utils.ts"],"sourcesContent":["/*\n * Copyright 2020 Adobe. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {Collection, Key} from '@react-types/shared';\nimport {isAppleDevice, useId} from '@react-aria/utils';\nimport {RefObject} from 'react';\n\ninterface Event {\n altKey: boolean,\n ctrlKey: boolean,\n metaKey: boolean\n}\n\nexport function isNonContiguousSelectionModifier(e: Event) {\n // Ctrl + Arrow Up/Arrow Down has a system wide meaning on macOS, so use Alt instead.\n // On Windows and Ubuntu, Alt + Space has a system wide meaning.\n return isAppleDevice() ? e.altKey : e.ctrlKey;\n}\n\nexport function getItemElement(collectionRef: RefObject<HTMLElement | null>, key: Key) {\n let selector = `[data-key=\"${CSS.escape(String(key))}\"]`;\n let collection = collectionRef.current?.dataset.collection;\n if (collection) {\n selector = `[data-collection=\"${CSS.escape(collection)}\"]${selector}`;\n }\n return collectionRef.current?.querySelector(selector);\n}\n\nconst collectionMap = new WeakMap();\nexport function useCollectionId(collection: Collection<any>) {\n let id = useId();\n collectionMap.set(collection, id);\n return id;\n}\n\nexport function getCollectionId(collection: Collection<any>) {\n return collectionMap.get(collection)!;\n}\n"],"names":[],"version":3,"file":"utils.module.js.map"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/selection",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"url": "https://github.com/adobe/react-spectrum"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@react-aria/focus": "^3.
|
|
26
|
-
"@react-aria/i18n": "^3.12.
|
|
27
|
-
"@react-aria/interactions": "^3.
|
|
28
|
-
"@react-aria/utils": "^3.
|
|
29
|
-
"@react-stately/selection": "^3.
|
|
30
|
-
"@react-types/shared": "^3.
|
|
25
|
+
"@react-aria/focus": "^3.20.0",
|
|
26
|
+
"@react-aria/i18n": "^3.12.6",
|
|
27
|
+
"@react-aria/interactions": "^3.24.0",
|
|
28
|
+
"@react-aria/utils": "^3.28.0",
|
|
29
|
+
"@react-stately/selection": "^3.20.0",
|
|
30
|
+
"@react-types/shared": "^3.28.0",
|
|
31
31
|
"@swc/helpers": "^0.5.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "4d3c72c94eea2d72eb3a0e7d56000c6ef7e39726"
|
|
41
41
|
}
|
package/src/DOMLayoutDelegate.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {getItemElement} from './utils';
|
|
13
14
|
import {Key, LayoutDelegate, Rect, RefObject, Size} from '@react-types/shared';
|
|
14
15
|
|
|
15
16
|
export class DOMLayoutDelegate implements LayoutDelegate {
|
|
@@ -24,7 +25,7 @@ export class DOMLayoutDelegate implements LayoutDelegate {
|
|
|
24
25
|
if (!container) {
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
|
-
let item = key != null ?
|
|
28
|
+
let item = key != null ? getItemElement(this.ref, key) : null;
|
|
28
29
|
if (!item) {
|
|
29
30
|
return null;
|
|
30
31
|
}
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils';
|
|
13
14
|
import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared';
|
|
14
15
|
import {flushSync} from 'react-dom';
|
|
15
16
|
import {FocusEvent, KeyboardEvent, useEffect, useRef} from 'react';
|
|
16
|
-
import {focusSafely,
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
|
|
17
|
+
import {focusSafely, getInteractionModality} from '@react-aria/interactions';
|
|
18
|
+
import {getFocusableTreeWalker, moveVirtualFocus} from '@react-aria/focus';
|
|
19
|
+
import {getItemElement, isNonContiguousSelectionModifier, useCollectionId} from './utils';
|
|
20
20
|
import {MultipleSelectionManager} from '@react-stately/selection';
|
|
21
21
|
import {useLocale} from '@react-aria/i18n';
|
|
22
22
|
import {useTypeSelect} from './useTypeSelect';
|
|
@@ -140,7 +140,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
140
140
|
manager.setFocusedKey(key, childFocus);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
let item =
|
|
143
|
+
let item = getItemElement(ref, key);
|
|
144
144
|
let itemProps = manager.getItemProps(key);
|
|
145
145
|
if (item) {
|
|
146
146
|
router.open(item, e, itemProps.href, itemProps.routerOptions);
|
|
@@ -222,6 +222,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
222
222
|
}
|
|
223
223
|
case 'Home':
|
|
224
224
|
if (delegate.getFirstKey) {
|
|
225
|
+
if (manager.focusedKey === null && e.shiftKey) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
225
228
|
e.preventDefault();
|
|
226
229
|
let firstKey: Key | null = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e));
|
|
227
230
|
manager.setFocusedKey(firstKey);
|
|
@@ -236,6 +239,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
236
239
|
break;
|
|
237
240
|
case 'End':
|
|
238
241
|
if (delegate.getLastKey) {
|
|
242
|
+
if (manager.focusedKey === null && e.shiftKey) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
239
245
|
e.preventDefault();
|
|
240
246
|
let lastKey = delegate.getLastKey(manager.focusedKey, isCtrlKeyPressed(e));
|
|
241
247
|
manager.setFocusedKey(lastKey);
|
|
@@ -336,12 +342,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
manager.setFocused(true);
|
|
339
|
-
|
|
340
345
|
if (manager.focusedKey == null) {
|
|
341
|
-
let
|
|
346
|
+
let navigateToKey = (key: Key | undefined | null) => {
|
|
342
347
|
if (key != null) {
|
|
343
348
|
manager.setFocusedKey(key);
|
|
344
|
-
if (selectOnFocus) {
|
|
349
|
+
if (selectOnFocus && !manager.isSelected(key)) {
|
|
345
350
|
manager.replaceSelection(key);
|
|
346
351
|
}
|
|
347
352
|
}
|
|
@@ -351,9 +356,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
351
356
|
// and either focus the first or last item accordingly.
|
|
352
357
|
let relatedTarget = e.relatedTarget as Element;
|
|
353
358
|
if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
354
|
-
|
|
359
|
+
navigateToKey(manager.lastSelectedKey ?? delegate.getLastKey?.());
|
|
355
360
|
} else {
|
|
356
|
-
|
|
361
|
+
navigateToKey(manager.firstSelectedKey ?? delegate.getFirstKey?.());
|
|
357
362
|
}
|
|
358
363
|
} else if (!isVirtualized && scrollRef.current) {
|
|
359
364
|
// Restore the scroll position to what it was before.
|
|
@@ -363,10 +368,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
363
368
|
|
|
364
369
|
if (manager.focusedKey != null && scrollRef.current) {
|
|
365
370
|
// Refocus and scroll the focused item into view if it exists within the scrollable region.
|
|
366
|
-
let element =
|
|
367
|
-
if (element) {
|
|
371
|
+
let element = getItemElement(ref, manager.focusedKey);
|
|
372
|
+
if (element instanceof HTMLElement) {
|
|
368
373
|
// This prevents a flash of focus on the first/last element in the collection, or the collection itself.
|
|
369
|
-
if (!element.contains(document.activeElement)) {
|
|
374
|
+
if (!element.contains(document.activeElement) && !shouldUseVirtualFocus) {
|
|
370
375
|
focusWithoutScrolling(element);
|
|
371
376
|
}
|
|
372
377
|
|
|
@@ -385,7 +390,75 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
385
390
|
}
|
|
386
391
|
};
|
|
387
392
|
|
|
393
|
+
// Ref to track whether the first item in the collection should be automatically focused. Specifically used for autocomplete when user types
|
|
394
|
+
// to focus the first key AFTER the collection updates.
|
|
395
|
+
// TODO: potentially expand the usage of this
|
|
396
|
+
let shouldVirtualFocusFirst = useRef(false);
|
|
397
|
+
// Add event listeners for custom virtual events. These handle updating the focused key in response to various keyboard events
|
|
398
|
+
// at the autocomplete level
|
|
399
|
+
// TODO: fix type later
|
|
400
|
+
useEvent(ref, FOCUS_EVENT, !shouldUseVirtualFocus ? undefined : (e: any) => {
|
|
401
|
+
let {detail} = e;
|
|
402
|
+
e.stopPropagation();
|
|
403
|
+
manager.setFocused(true);
|
|
404
|
+
|
|
405
|
+
// If the user is typing forwards, autofocus the first option in the list.
|
|
406
|
+
if (detail?.focusStrategy === 'first') {
|
|
407
|
+
shouldVirtualFocusFirst.current = true;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
let updateActiveDescendant = useEffectEvent(() => {
|
|
412
|
+
let keyToFocus = delegate.getFirstKey?.() ?? null;
|
|
413
|
+
|
|
414
|
+
// If no focusable items exist in the list, make sure to clear any activedescendant that may still exist
|
|
415
|
+
if (keyToFocus == null) {
|
|
416
|
+
moveVirtualFocus(ref.current);
|
|
417
|
+
|
|
418
|
+
// If there wasn't a focusable key but the collection had items, then that means we aren't in an intermediate load state and all keys are disabled.
|
|
419
|
+
// Reset shouldVirtualFocusFirst so that we don't erronously autofocus an item when the collection is filtered again.
|
|
420
|
+
if (manager.collection.size > 0) {
|
|
421
|
+
shouldVirtualFocusFirst.current = false;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
manager.setFocusedKey(keyToFocus);
|
|
425
|
+
// Only set shouldVirtualFocusFirst to false if we've successfully set the first key as the focused key
|
|
426
|
+
// If there wasn't a key to focus, we might be in a temporary loading state so we'll want to still focus the first key
|
|
427
|
+
// after the collection updates after load
|
|
428
|
+
shouldVirtualFocusFirst.current = false;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
useUpdateLayoutEffect(() => {
|
|
433
|
+
if (shouldVirtualFocusFirst.current) {
|
|
434
|
+
updateActiveDescendant();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
}, [manager.collection, updateActiveDescendant]);
|
|
438
|
+
|
|
439
|
+
let resetFocusFirstFlag = useEffectEvent(() => {
|
|
440
|
+
// If user causes the focused key to change in any other way, clear shouldVirtualFocusFirst so we don't
|
|
441
|
+
// accidentally move focus from under them. Skip this if the collection was empty because we might be in a load
|
|
442
|
+
// state and will still want to focus the first item after load
|
|
443
|
+
if (manager.collection.size > 0) {
|
|
444
|
+
shouldVirtualFocusFirst.current = false;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
useUpdateLayoutEffect(() => {
|
|
449
|
+
resetFocusFirstFlag();
|
|
450
|
+
}, [manager.focusedKey, resetFocusFirstFlag]);
|
|
451
|
+
|
|
452
|
+
useEvent(ref, CLEAR_FOCUS_EVENT, !shouldUseVirtualFocus ? undefined : (e: any) => {
|
|
453
|
+
e.stopPropagation();
|
|
454
|
+
manager.setFocused(false);
|
|
455
|
+
if (e.detail?.clearFocusKey) {
|
|
456
|
+
manager.setFocusedKey(null);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
388
460
|
const autoFocusRef = useRef(autoFocus);
|
|
461
|
+
const didAutoFocusRef = useRef(false);
|
|
389
462
|
useEffect(() => {
|
|
390
463
|
if (autoFocusRef.current) {
|
|
391
464
|
let focusedKey: Key | null = null;
|
|
@@ -415,23 +488,28 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
415
488
|
if (focusedKey == null && !shouldUseVirtualFocus && ref.current) {
|
|
416
489
|
focusSafely(ref.current);
|
|
417
490
|
}
|
|
491
|
+
|
|
492
|
+
// Wait until the collection has items to autofocus.
|
|
493
|
+
if (manager.collection.size > 0) {
|
|
494
|
+
autoFocusRef.current = false;
|
|
495
|
+
didAutoFocusRef.current = true;
|
|
496
|
+
}
|
|
418
497
|
}
|
|
419
|
-
|
|
420
|
-
}, []);
|
|
498
|
+
});
|
|
421
499
|
|
|
422
500
|
// Scroll the focused element into view when the focusedKey changes.
|
|
423
501
|
let lastFocusedKey = useRef(manager.focusedKey);
|
|
424
502
|
useEffect(() => {
|
|
425
|
-
if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current ||
|
|
503
|
+
if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || didAutoFocusRef.current) && scrollRef.current && ref.current) {
|
|
426
504
|
let modality = getInteractionModality();
|
|
427
|
-
let element = ref
|
|
428
|
-
if (!element) {
|
|
505
|
+
let element = getItemElement(ref, manager.focusedKey);
|
|
506
|
+
if (!(element instanceof HTMLElement)) {
|
|
429
507
|
// If item element wasn't found, return early (don't update autoFocusRef and lastFocusedKey).
|
|
430
508
|
// The collection may initially be empty (e.g. virtualizer), so wait until the element exists.
|
|
431
509
|
return;
|
|
432
510
|
}
|
|
433
511
|
|
|
434
|
-
if (modality === 'keyboard' ||
|
|
512
|
+
if (modality === 'keyboard' || didAutoFocusRef.current) {
|
|
435
513
|
scrollIntoView(scrollRef.current, element);
|
|
436
514
|
|
|
437
515
|
// Avoid scroll in iOS VO, since it may cause overlay to close (i.e. RAC submenu)
|
|
@@ -447,7 +525,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
447
525
|
}
|
|
448
526
|
|
|
449
527
|
lastFocusedKey.current = manager.focusedKey;
|
|
450
|
-
|
|
528
|
+
didAutoFocusRef.current = false;
|
|
451
529
|
});
|
|
452
530
|
|
|
453
531
|
// Intercept FocusScope restoration since virtualized collections can reuse DOM nodes.
|
|
@@ -480,17 +558,16 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
480
558
|
|
|
481
559
|
// If nothing is focused within the collection, make the collection itself tabbable.
|
|
482
560
|
// This will be marshalled to either the first or last item depending on where focus came from.
|
|
483
|
-
// If using virtual focus, don't set a tabIndex at all so that VoiceOver on iOS 14 doesn't try
|
|
484
|
-
// to move real DOM focus to the element anyway.
|
|
485
561
|
let tabIndex: number | undefined = undefined;
|
|
486
562
|
if (!shouldUseVirtualFocus) {
|
|
487
563
|
tabIndex = manager.focusedKey == null ? 0 : -1;
|
|
488
564
|
}
|
|
489
565
|
|
|
566
|
+
let collectionId = useCollectionId(manager.collection);
|
|
490
567
|
return {
|
|
491
|
-
collectionProps: {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
568
|
+
collectionProps: mergeProps(handlers, {
|
|
569
|
+
tabIndex,
|
|
570
|
+
'data-collection': collectionId
|
|
571
|
+
})
|
|
495
572
|
};
|
|
496
573
|
}
|
package/src/useSelectableItem.ts
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {DOMAttributes, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
|
|
14
|
-
import {focusSafely} from '@react-aria/
|
|
15
|
-
import {
|
|
16
|
-
import {mergeProps, openLink, useRouter} from '@react-aria/utils';
|
|
13
|
+
import {DOMAttributes, DOMProps, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
|
|
14
|
+
import {focusSafely, PressProps, useLongPress, usePress} from '@react-aria/interactions';
|
|
15
|
+
import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
|
|
16
|
+
import {isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
|
|
17
|
+
import {moveVirtualFocus} from '@react-aria/focus';
|
|
17
18
|
import {MultipleSelectionManager} from '@react-stately/selection';
|
|
18
|
-
import {PressProps, useLongPress, usePress} from '@react-aria/interactions';
|
|
19
19
|
import {useEffect, useRef} from 'react';
|
|
20
20
|
|
|
21
|
-
export interface SelectableItemOptions {
|
|
21
|
+
export interface SelectableItemOptions extends DOMProps {
|
|
22
22
|
/**
|
|
23
23
|
* An interface for reading and updating multiple selection state.
|
|
24
24
|
*/
|
|
@@ -108,6 +108,7 @@ export interface SelectableItemAria extends SelectableItemStates {
|
|
|
108
108
|
*/
|
|
109
109
|
export function useSelectableItem(options: SelectableItemOptions): SelectableItemAria {
|
|
110
110
|
let {
|
|
111
|
+
id,
|
|
111
112
|
selectionManager: manager,
|
|
112
113
|
key,
|
|
113
114
|
ref,
|
|
@@ -120,7 +121,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
120
121
|
linkBehavior = 'action'
|
|
121
122
|
} = options;
|
|
122
123
|
let router = useRouter();
|
|
123
|
-
|
|
124
|
+
id = useId(id);
|
|
124
125
|
let onSelect = (e: PressEvent | LongPressEvent | PointerEvent) => {
|
|
125
126
|
if (e.pointerType === 'keyboard' && isNonContiguousSelectionModifier(e)) {
|
|
126
127
|
manager.toggleSelection(key);
|
|
@@ -159,13 +160,20 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
159
160
|
};
|
|
160
161
|
|
|
161
162
|
// Focus the associated DOM node when this item becomes the focusedKey
|
|
163
|
+
// TODO: can't make this useLayoutEffect bacause it breaks menus inside dialogs
|
|
164
|
+
// However, if this is a useEffect, it runs twice and dispatches two blur events and immediately sets
|
|
165
|
+
// aria-activeDescendant in useAutocomplete... I've worked around this for now
|
|
162
166
|
useEffect(() => {
|
|
163
167
|
let isFocused = key === manager.focusedKey;
|
|
164
|
-
if (isFocused && manager.isFocused
|
|
165
|
-
if (
|
|
166
|
-
focus
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if (isFocused && manager.isFocused) {
|
|
169
|
+
if (!shouldUseVirtualFocus) {
|
|
170
|
+
if (focus) {
|
|
171
|
+
focus();
|
|
172
|
+
} else if (document.activeElement !== ref.current && ref.current) {
|
|
173
|
+
focusSafely(ref.current);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
moveVirtualFocus(ref.current);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -241,7 +249,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
241
249
|
}
|
|
242
250
|
};
|
|
243
251
|
|
|
244
|
-
// If allowsDifferentPressOrigin, make selection happen on pressUp (e.g. open menu on press down, selection on menu item happens on press up.)
|
|
252
|
+
// If allowsDifferentPressOrigin and interacting with mouse, make selection happen on pressUp (e.g. open menu on press down, selection on menu item happens on press up.)
|
|
245
253
|
// Otherwise, have selection happen onPress (prevents listview row selection when clicking on interactable elements in the row)
|
|
246
254
|
if (!allowsDifferentPressOrigin) {
|
|
247
255
|
itemPressProps.onPress = (e) => {
|
|
@@ -257,12 +265,16 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
257
265
|
};
|
|
258
266
|
} else {
|
|
259
267
|
itemPressProps.onPressUp = hasPrimaryAction ? undefined : (e) => {
|
|
260
|
-
if (e.pointerType
|
|
268
|
+
if (e.pointerType === 'mouse' && allowsSelection) {
|
|
261
269
|
onSelect(e);
|
|
262
270
|
}
|
|
263
271
|
};
|
|
264
272
|
|
|
265
|
-
itemPressProps.onPress = hasPrimaryAction ? performAction :
|
|
273
|
+
itemPressProps.onPress = hasPrimaryAction ? performAction : (e) => {
|
|
274
|
+
if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse' && allowsSelection) {
|
|
275
|
+
onSelect(e);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
266
278
|
}
|
|
267
279
|
} else {
|
|
268
280
|
itemPressProps.onPressStart = (e) => {
|
|
@@ -303,8 +315,28 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
303
315
|
};
|
|
304
316
|
}
|
|
305
317
|
|
|
318
|
+
itemProps['data-collection'] = getCollectionId(manager.collection);
|
|
306
319
|
itemProps['data-key'] = key;
|
|
307
320
|
itemPressProps.preventFocusOnPress = shouldUseVirtualFocus;
|
|
321
|
+
|
|
322
|
+
// When using virtual focus, make sure the focused key gets updated on press.
|
|
323
|
+
if (shouldUseVirtualFocus) {
|
|
324
|
+
itemPressProps = mergeProps(itemPressProps, {
|
|
325
|
+
onPressStart(e) {
|
|
326
|
+
if (e.pointerType !== 'touch') {
|
|
327
|
+
manager.setFocused(true);
|
|
328
|
+
manager.setFocusedKey(key);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
onPress(e) {
|
|
332
|
+
if (e.pointerType === 'touch') {
|
|
333
|
+
manager.setFocused(true);
|
|
334
|
+
manager.setFocusedKey(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
308
340
|
let {pressProps, isPressed} = usePress(itemPressProps);
|
|
309
341
|
|
|
310
342
|
// Double clicking with a mouse with selectionBehavior = 'replace' performs an action.
|
|
@@ -350,9 +382,11 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
350
382
|
return {
|
|
351
383
|
itemProps: mergeProps(
|
|
352
384
|
itemProps,
|
|
353
|
-
allowsSelection || hasPrimaryAction ? pressProps : {},
|
|
385
|
+
allowsSelection || hasPrimaryAction || shouldUseVirtualFocus ? pressProps : {},
|
|
354
386
|
longPressEnabled ? longPressProps : {},
|
|
355
|
-
{onDoubleClick, onDragStartCapture, onClick}
|
|
387
|
+
{onDoubleClick, onDragStartCapture, onClick, id},
|
|
388
|
+
// Prevent DOM focus from moving on mouse down when using virtual focus
|
|
389
|
+
shouldUseVirtualFocus ? {onMouseDown: e => e.preventDefault()} : undefined
|
|
356
390
|
),
|
|
357
391
|
isPressed,
|
|
358
392
|
isSelected: manager.isSelected(key),
|
package/src/utils.ts
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {Collection, Key} from '@react-types/shared';
|
|
14
|
+
import {isAppleDevice, useId} from '@react-aria/utils';
|
|
15
|
+
import {RefObject} from 'react';
|
|
14
16
|
|
|
15
17
|
interface Event {
|
|
16
18
|
altKey: boolean,
|
|
@@ -24,10 +26,22 @@ export function isNonContiguousSelectionModifier(e: Event) {
|
|
|
24
26
|
return isAppleDevice() ? e.altKey : e.ctrlKey;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
export function getItemElement(collectionRef: RefObject<HTMLElement | null>, key: Key) {
|
|
30
|
+
let selector = `[data-key="${CSS.escape(String(key))}"]`;
|
|
31
|
+
let collection = collectionRef.current?.dataset.collection;
|
|
32
|
+
if (collection) {
|
|
33
|
+
selector = `[data-collection="${CSS.escape(collection)}"]${selector}`;
|
|
30
34
|
}
|
|
35
|
+
return collectionRef.current?.querySelector(selector);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const collectionMap = new WeakMap();
|
|
39
|
+
export function useCollectionId(collection: Collection<any>) {
|
|
40
|
+
let id = useId();
|
|
41
|
+
collectionMap.set(collection, id);
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
export function getCollectionId(collection: Collection<any>) {
|
|
46
|
+
return collectionMap.get(collection)!;
|
|
33
47
|
}
|