@react-aria/selection 3.20.0 → 3.21.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/LICENSE +201 -0
- package/dist/DOMLayoutDelegate.main.js +9 -6
- package/dist/DOMLayoutDelegate.main.js.map +1 -1
- package/dist/DOMLayoutDelegate.mjs +9 -6
- package/dist/DOMLayoutDelegate.module.js +9 -6
- package/dist/DOMLayoutDelegate.module.js.map +1 -1
- package/dist/ListKeyboardDelegate.main.js +38 -30
- package/dist/ListKeyboardDelegate.main.js.map +1 -1
- package/dist/ListKeyboardDelegate.mjs +38 -30
- package/dist/ListKeyboardDelegate.module.js +38 -30
- package/dist/ListKeyboardDelegate.module.js.map +1 -1
- package/dist/types.d.ts +12 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/useSelectableCollection.main.js +37 -25
- package/dist/useSelectableCollection.main.js.map +1 -1
- package/dist/useSelectableCollection.mjs +37 -25
- package/dist/useSelectableCollection.module.js +37 -25
- package/dist/useSelectableCollection.module.js.map +1 -1
- package/dist/useSelectableItem.main.js +5 -5
- package/dist/useSelectableItem.main.js.map +1 -1
- package/dist/useSelectableItem.mjs +5 -5
- package/dist/useSelectableItem.module.js +5 -5
- package/dist/useSelectableItem.module.js.map +1 -1
- package/dist/useTypeSelect.main.js +12 -10
- package/dist/useTypeSelect.main.js.map +1 -1
- package/dist/useTypeSelect.mjs +12 -10
- package/dist/useTypeSelect.module.js +12 -10
- package/dist/useTypeSelect.module.js.map +1 -1
- package/package.json +12 -11
- package/src/DOMLayoutDelegate.ts +11 -8
- package/src/ListKeyboardDelegate.ts +46 -34
- package/src/useSelectableCollection.ts +38 -32
- package/src/useSelectableItem.ts +7 -7
- package/src/useTypeSelect.ts +16 -14
|
@@ -18,7 +18,7 @@ function $fb3050f43d946246$export$e32c88dfddc6e1d8(options) {
|
|
|
18
18
|
let { keyboardDelegate: keyboardDelegate, selectionManager: selectionManager, onTypeSelect: onTypeSelect } = options;
|
|
19
19
|
let state = (0, $dAE4Y$useRef)({
|
|
20
20
|
search: '',
|
|
21
|
-
timeout:
|
|
21
|
+
timeout: undefined
|
|
22
22
|
}).current;
|
|
23
23
|
let onKeyDown = (e)=>{
|
|
24
24
|
let character = $fb3050f43d946246$var$getStringForKey(e.key);
|
|
@@ -32,14 +32,16 @@ function $fb3050f43d946246$export$e32c88dfddc6e1d8(options) {
|
|
|
32
32
|
if (!('continuePropagation' in e)) e.stopPropagation();
|
|
33
33
|
}
|
|
34
34
|
state.search += character;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
if (keyboardDelegate.getKeyForSearch != null) {
|
|
36
|
+
// Use the delegate to find a key to focus.
|
|
37
|
+
// Prioritize items after the currently focused item, falling back to searching the whole list.
|
|
38
|
+
let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);
|
|
39
|
+
// If no key found, search from the top.
|
|
40
|
+
if (key == null) key = keyboardDelegate.getKeyForSearch(state.search);
|
|
41
|
+
if (key != null) {
|
|
42
|
+
selectionManager.setFocusedKey(key);
|
|
43
|
+
if (onTypeSelect) onTypeSelect(key);
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
46
|
clearTimeout(state.timeout);
|
|
45
47
|
state.timeout = setTimeout(()=>{
|
|
@@ -50,7 +52,7 @@ function $fb3050f43d946246$export$e32c88dfddc6e1d8(options) {
|
|
|
50
52
|
typeSelectProps: {
|
|
51
53
|
// Using a capturing listener to catch the keydown event before
|
|
52
54
|
// other hooks in order to handle the Spacebar event.
|
|
53
|
-
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown :
|
|
55
|
+
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined
|
|
54
56
|
}
|
|
55
57
|
};
|
|
56
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;AAAA;;;;;;;;;;CAUC;AAMD;;CAEC,GACD,MAAM,mDAA6B,MAAM,WAAW;AA2B7C,SAAS,0CAAc,OAA8B;IAC1D,IAAI,oBAAC,gBAAgB,oBAAE,gBAAgB,gBAAE,YAAY,EAAC,GAAG;IACzD,IAAI,QAAQ,CAAA,GAAA,aAAK,
|
|
1
|
+
{"mappings":";;AAAA;;;;;;;;;;CAUC;AAMD;;CAEC,GACD,MAAM,mDAA6B,MAAM,WAAW;AA2B7C,SAAS,0CAAc,OAA8B;IAC1D,IAAI,oBAAC,gBAAgB,oBAAE,gBAAgB,gBAAE,YAAY,EAAC,GAAG;IACzD,IAAI,QAAQ,CAAA,GAAA,aAAK,EAAwE;QACvF,QAAQ;QACR,SAAS;IACX,GAAG,OAAO;IAEV,IAAI,YAAY,CAAC;QACf,IAAI,YAAY,sCAAgB,EAAE,GAAG;QACrC,IAAI,CAAC,aAAa,EAAE,OAAO,IAAI,EAAE,OAAO,IAAI,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,GAC5E;QAGF,8EAA8E;QAC9E,8EAA8E;QAC9E,+EAA+E;QAC/E,4EAA4E;QAC5E,IAAI,cAAc,OAAO,MAAM,MAAM,CAAC,IAAI,GAAG,MAAM,GAAG,GAAG;YACvD,EAAE,cAAc;YAChB,IAAI,CAAE,CAAA,yBAAyB,CAAA,GAC7B,EAAE,eAAe;QAErB;QAEA,MAAM,MAAM,IAAI;QAEhB,IAAI,iBAAiB,eAAe,IAAI,MAAM;YAC5C,2CAA2C;YAC3C,+FAA+F;YAC/F,IAAI,MAAM,iBAAiB,eAAe,CAAC,MAAM,MAAM,EAAE,iBAAiB,UAAU;YAEpF,wCAAwC;YACxC,IAAI,OAAO,MACT,MAAM,iBAAiB,eAAe,CAAC,MAAM,MAAM;YAGrD,IAAI,OAAO,MAAM;gBACf,iBAAiB,aAAa,CAAC;gBAC/B,IAAI,cACF,aAAa;YAEjB;QACF;QAEA,aAAa,MAAM,OAAO;QAC1B,MAAM,OAAO,GAAG,WAAW;YACzB,MAAM,MAAM,GAAG;QACjB,GAAG;IACL;IAEA,OAAO;QACL,iBAAiB;YACf,+DAA+D;YAC/D,qDAAqD;YACrD,kBAAkB,iBAAiB,eAAe,GAAG,YAAY;QACnE;IACF;AACF;AAEA,SAAS,sCAAgB,GAAW;IAClC,mDAAmD;IACnD,+DAA+D;IAC/D,6BAA6B;IAC7B,0CAA0C;IAC1C,IAAI,IAAI,MAAM,KAAK,KAAK,CAAC,UAAU,IAAI,CAAC,MACtC,OAAO;IAGT,OAAO;AACT","sources":["packages/@react-aria/selection/src/useTypeSelect.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 {DOMAttributes, Key, KeyboardDelegate} from '@react-types/shared';\nimport {KeyboardEvent, useRef} from 'react';\nimport {MultipleSelectionManager} from '@react-stately/selection';\n\n/**\n * Controls how long to wait before clearing the typeahead buffer.\n */\nconst TYPEAHEAD_DEBOUNCE_WAIT_MS = 1000; // 1 second\n\nexport interface AriaTypeSelectOptions {\n /**\n * A delegate that returns collection item keys with respect to visual layout.\n */\n keyboardDelegate: KeyboardDelegate,\n /**\n * An interface for reading and updating multiple selection state.\n */\n selectionManager: MultipleSelectionManager,\n /**\n * Called when an item is focused by typing.\n */\n onTypeSelect?: (key: Key) => void\n}\n\nexport interface TypeSelectAria {\n /**\n * Props to be spread on the owner of the options.\n */\n typeSelectProps: DOMAttributes\n}\n\n/**\n * Handles typeahead interactions with collections.\n */\nexport function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {\n let {keyboardDelegate, selectionManager, onTypeSelect} = options;\n let state = useRef<{search: string, timeout: ReturnType<typeof setTimeout> | undefined}>({\n search: '',\n timeout: undefined\n }).current;\n\n let onKeyDown = (e: KeyboardEvent) => {\n let character = getStringForKey(e.key);\n if (!character || e.ctrlKey || e.metaKey || !e.currentTarget.contains(e.target as HTMLElement)) {\n return;\n }\n\n // Do not propagate the Spacebar event if it's meant to be part of the search.\n // When we time out, the search term becomes empty, hence the check on length.\n // Trimming is to account for the case of pressing the Spacebar more than once,\n // which should cycle through the selection/deselection of the focused item.\n if (character === ' ' && state.search.trim().length > 0) {\n e.preventDefault();\n if (!('continuePropagation' in e)) {\n e.stopPropagation();\n }\n }\n\n state.search += character;\n\n if (keyboardDelegate.getKeyForSearch != null) {\n // Use the delegate to find a key to focus.\n // Prioritize items after the currently focused item, falling back to searching the whole list.\n let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);\n\n // If no key found, search from the top.\n if (key == null) {\n key = keyboardDelegate.getKeyForSearch(state.search);\n }\n\n if (key != null) {\n selectionManager.setFocusedKey(key);\n if (onTypeSelect) {\n onTypeSelect(key);\n }\n }\n }\n\n clearTimeout(state.timeout);\n state.timeout = setTimeout(() => {\n state.search = '';\n }, TYPEAHEAD_DEBOUNCE_WAIT_MS);\n };\n\n return {\n typeSelectProps: {\n // Using a capturing listener to catch the keydown event before\n // other hooks in order to handle the Spacebar event.\n onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined\n }\n };\n}\n\nfunction getStringForKey(key: string) {\n // If the key is of length 1, it is an ASCII value.\n // Otherwise, if there are no ASCII characters in the key name,\n // it is a Unicode character.\n // See https://www.w3.org/TR/uievents-key/\n if (key.length === 1 || !/^[A-Z]/i.test(key)) {\n return key;\n }\n\n return '';\n}\n"],"names":[],"version":3,"file":"useTypeSelect.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.21.0",
|
|
4
4
|
"description": "Spectrum UI components in React",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -22,19 +22,20 @@
|
|
|
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.22.
|
|
28
|
-
"@react-aria/utils": "^3.
|
|
29
|
-
"@react-stately/selection": "^3.
|
|
30
|
-
"@react-types/shared": "^3.
|
|
25
|
+
"@react-aria/focus": "^3.19.0",
|
|
26
|
+
"@react-aria/i18n": "^3.12.4",
|
|
27
|
+
"@react-aria/interactions": "^3.22.5",
|
|
28
|
+
"@react-aria/utils": "^3.26.0",
|
|
29
|
+
"@react-stately/selection": "^3.18.0",
|
|
30
|
+
"@react-types/shared": "^3.26.0",
|
|
31
31
|
"@swc/helpers": "^0.5.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
|
|
35
|
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
|
|
34
|
+
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
35
|
+
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
39
|
-
}
|
|
40
|
-
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "71f0ef23053f9e03ee7e97df736e8b083e006849"
|
|
41
|
+
}
|
package/src/DOMLayoutDelegate.ts
CHANGED
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
import {Key, LayoutDelegate, Rect, RefObject, Size} from '@react-types/shared';
|
|
14
14
|
|
|
15
15
|
export class DOMLayoutDelegate implements LayoutDelegate {
|
|
16
|
-
private ref: RefObject<HTMLElement>;
|
|
16
|
+
private ref: RefObject<HTMLElement | null>;
|
|
17
17
|
|
|
18
|
-
constructor(ref: RefObject<HTMLElement>) {
|
|
18
|
+
constructor(ref: RefObject<HTMLElement | null>) {
|
|
19
19
|
this.ref = ref;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
getItemRect(key: Key): Rect | null {
|
|
23
23
|
let container = this.ref.current;
|
|
24
|
+
if (!container) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
24
27
|
let item = key != null ? container.querySelector(`[data-key="${CSS.escape(key.toString())}"]`) : null;
|
|
25
28
|
if (!item) {
|
|
26
29
|
return null;
|
|
@@ -40,18 +43,18 @@ export class DOMLayoutDelegate implements LayoutDelegate {
|
|
|
40
43
|
getContentSize(): Size {
|
|
41
44
|
let container = this.ref.current;
|
|
42
45
|
return {
|
|
43
|
-
width: container
|
|
44
|
-
height: container
|
|
46
|
+
width: container?.scrollWidth ?? 0,
|
|
47
|
+
height: container?.scrollHeight ?? 0
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
getVisibleRect(): Rect {
|
|
49
52
|
let container = this.ref.current;
|
|
50
53
|
return {
|
|
51
|
-
x: container
|
|
52
|
-
y: container
|
|
53
|
-
width: container
|
|
54
|
-
height: container
|
|
54
|
+
x: container?.scrollLeft ?? 0,
|
|
55
|
+
y: container?.scrollTop ?? 0,
|
|
56
|
+
width: container?.offsetWidth ?? 0,
|
|
57
|
+
height: container?.offsetHeight ?? 0
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -74,47 +74,54 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
74
74
|
return this.disabledBehavior === 'all' && (item.props?.isDisabled || this.disabledKeys.has(item.key));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
private findNextNonDisabled(key: Key, getNext: (key: Key) => Key | null): Key | null {
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
private findNextNonDisabled(key: Key | null, getNext: (key: Key) => Key | null): Key | null {
|
|
78
|
+
let nextKey = key;
|
|
79
|
+
while (nextKey != null) {
|
|
80
|
+
let item = this.collection.getItem(nextKey);
|
|
80
81
|
if (item?.type === 'item' && !this.isDisabled(item)) {
|
|
81
|
-
return
|
|
82
|
+
return nextKey;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
nextKey = getNext(nextKey);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
getNextKey(key: Key) {
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
let nextKey: Key | null = key;
|
|
93
|
+
nextKey = this.collection.getKeyAfter(nextKey);
|
|
94
|
+
return this.findNextNonDisabled(nextKey, key => this.collection.getKeyAfter(key));
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
getPreviousKey(key: Key) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
let nextKey: Key | null = key;
|
|
99
|
+
nextKey = this.collection.getKeyBefore(nextKey);
|
|
100
|
+
return this.findNextNonDisabled(nextKey, key => this.collection.getKeyBefore(key));
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
private findKey(
|
|
101
104
|
key: Key,
|
|
102
|
-
nextKey: (key: Key) => Key,
|
|
105
|
+
nextKey: (key: Key) => Key | null,
|
|
103
106
|
shouldSkip: (prevRect: Rect, itemRect: Rect) => boolean
|
|
104
107
|
) {
|
|
105
|
-
let
|
|
106
|
-
|
|
108
|
+
let tempKey: Key | null = key;
|
|
109
|
+
let itemRect = this.layoutDelegate.getItemRect(tempKey);
|
|
110
|
+
if (!itemRect || tempKey == null) {
|
|
107
111
|
return null;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
// Find the item above or below in the same column.
|
|
111
115
|
let prevRect = itemRect;
|
|
112
116
|
do {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
tempKey = nextKey(tempKey);
|
|
118
|
+
if (tempKey == null) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
itemRect = this.layoutDelegate.getItemRect(tempKey);
|
|
122
|
+
} while (itemRect && shouldSkip(prevRect, itemRect) && tempKey != null);
|
|
116
123
|
|
|
117
|
-
return
|
|
124
|
+
return tempKey;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
private isSameRow(prevRect: Rect, itemRect: Rect) {
|
|
@@ -145,7 +152,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
145
152
|
return right ? this.getPreviousKey(key) : this.getNextKey(key);
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
getKeyRightOf(key: Key) {
|
|
155
|
+
getKeyRightOf?(key: Key) {
|
|
149
156
|
// This is a temporary solution for CardView until we refactor useSelectableCollection.
|
|
150
157
|
// https://github.com/orgs/adobe/projects/19/views/32?pane=issue&itemId=77825042
|
|
151
158
|
let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyRightOf' : 'getKeyLeftOf';
|
|
@@ -167,7 +174,7 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
167
174
|
return null;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
getKeyLeftOf(key: Key) {
|
|
177
|
+
getKeyLeftOf?(key: Key) {
|
|
171
178
|
let layoutDelegateMethod = this.direction === 'ltr' ? 'getKeyLeftOf' : 'getKeyRightOf';
|
|
172
179
|
if (this.layoutDelegate[layoutDelegateMethod]) {
|
|
173
180
|
key = this.layoutDelegate[layoutDelegateMethod](key);
|
|
@@ -204,27 +211,28 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
204
211
|
return null;
|
|
205
212
|
}
|
|
206
213
|
|
|
207
|
-
if (!isScrollable(menu)) {
|
|
214
|
+
if (menu && !isScrollable(menu)) {
|
|
208
215
|
return this.getFirstKey();
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
let nextKey: Key | null = key;
|
|
211
219
|
if (this.orientation === 'horizontal') {
|
|
212
220
|
let pageX = Math.max(0, itemRect.x + itemRect.width - this.layoutDelegate.getVisibleRect().width);
|
|
213
221
|
|
|
214
|
-
while (itemRect && itemRect.x > pageX) {
|
|
215
|
-
|
|
216
|
-
itemRect =
|
|
222
|
+
while (itemRect && itemRect.x > pageX && nextKey != null) {
|
|
223
|
+
nextKey = this.getKeyAbove(nextKey);
|
|
224
|
+
itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey);
|
|
217
225
|
}
|
|
218
226
|
} else {
|
|
219
227
|
let pageY = Math.max(0, itemRect.y + itemRect.height - this.layoutDelegate.getVisibleRect().height);
|
|
220
228
|
|
|
221
|
-
while (itemRect && itemRect.y > pageY) {
|
|
222
|
-
|
|
223
|
-
itemRect =
|
|
229
|
+
while (itemRect && itemRect.y > pageY && nextKey != null) {
|
|
230
|
+
nextKey = this.getKeyAbove(nextKey);
|
|
231
|
+
itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey);
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
|
|
227
|
-
return
|
|
235
|
+
return nextKey ?? this.getFirstKey();
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
getKeyPageBelow(key: Key) {
|
|
@@ -234,27 +242,28 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
234
242
|
return null;
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
if (!isScrollable(menu)) {
|
|
245
|
+
if (menu && !isScrollable(menu)) {
|
|
238
246
|
return this.getLastKey();
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
let nextKey: Key | null = key;
|
|
241
250
|
if (this.orientation === 'horizontal') {
|
|
242
251
|
let pageX = Math.min(this.layoutDelegate.getContentSize().width, itemRect.y - itemRect.width + this.layoutDelegate.getVisibleRect().width);
|
|
243
252
|
|
|
244
|
-
while (itemRect && itemRect.x < pageX) {
|
|
245
|
-
|
|
246
|
-
itemRect =
|
|
253
|
+
while (itemRect && itemRect.x < pageX && nextKey != null) {
|
|
254
|
+
nextKey = this.getKeyBelow(nextKey);
|
|
255
|
+
itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey);
|
|
247
256
|
}
|
|
248
257
|
} else {
|
|
249
258
|
let pageY = Math.min(this.layoutDelegate.getContentSize().height, itemRect.y - itemRect.height + this.layoutDelegate.getVisibleRect().height);
|
|
250
259
|
|
|
251
|
-
while (itemRect && itemRect.y < pageY) {
|
|
252
|
-
|
|
253
|
-
itemRect =
|
|
260
|
+
while (itemRect && itemRect.y < pageY && nextKey != null) {
|
|
261
|
+
nextKey = this.getKeyBelow(nextKey);
|
|
262
|
+
itemRect = nextKey == null ? null : this.layoutDelegate.getItemRect(nextKey);
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
265
|
|
|
257
|
-
return
|
|
266
|
+
return nextKey ?? this.getLastKey();
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
getKeyForSearch(search: string, fromKey?: Key) {
|
|
@@ -266,6 +275,9 @@ export class ListKeyboardDelegate<T> implements KeyboardDelegate {
|
|
|
266
275
|
let key = fromKey || this.getFirstKey();
|
|
267
276
|
while (key != null) {
|
|
268
277
|
let item = collection.getItem(key);
|
|
278
|
+
if (!item) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
269
281
|
let substring = item.textValue.slice(0, search.length);
|
|
270
282
|
if (item.textValue && this.collator.compare(substring, search) === 0) {
|
|
271
283
|
return key;
|
|
@@ -128,7 +128,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
128
128
|
|
|
129
129
|
// Keyboard events bubble through portals. Don't handle keyboard events
|
|
130
130
|
// for elements outside the collection (e.g. menus).
|
|
131
|
-
if (!ref.current
|
|
131
|
+
if (!ref.current?.contains(e.target as Element)) {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -140,9 +140,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
140
140
|
manager.setFocusedKey(key, childFocus);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
let item = scrollRef.current
|
|
143
|
+
let item = scrollRef.current?.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
|
|
144
144
|
let itemProps = manager.getItemProps(key);
|
|
145
|
-
|
|
145
|
+
if (item) {
|
|
146
|
+
router.open(item, e, itemProps.href, itemProps.routerOptions);
|
|
147
|
+
}
|
|
146
148
|
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
@@ -194,7 +196,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
194
196
|
}
|
|
195
197
|
case 'ArrowLeft': {
|
|
196
198
|
if (delegate.getKeyLeftOf) {
|
|
197
|
-
let nextKey = delegate.getKeyLeftOf?.(manager.focusedKey);
|
|
199
|
+
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : null;
|
|
198
200
|
if (nextKey == null && shouldFocusWrap) {
|
|
199
201
|
nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey);
|
|
200
202
|
}
|
|
@@ -207,7 +209,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
207
209
|
}
|
|
208
210
|
case 'ArrowRight': {
|
|
209
211
|
if (delegate.getKeyRightOf) {
|
|
210
|
-
let nextKey = delegate.getKeyRightOf?.(manager.focusedKey);
|
|
212
|
+
let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : null;
|
|
211
213
|
if (nextKey == null && shouldFocusWrap) {
|
|
212
214
|
nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey);
|
|
213
215
|
}
|
|
@@ -221,12 +223,14 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
221
223
|
case 'Home':
|
|
222
224
|
if (delegate.getFirstKey) {
|
|
223
225
|
e.preventDefault();
|
|
224
|
-
let firstKey = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e));
|
|
226
|
+
let firstKey: Key | null = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e));
|
|
225
227
|
manager.setFocusedKey(firstKey);
|
|
226
|
-
if (
|
|
227
|
-
manager.
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
if (firstKey != null) {
|
|
229
|
+
if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') {
|
|
230
|
+
manager.extendSelection(firstKey);
|
|
231
|
+
} else if (selectOnFocus) {
|
|
232
|
+
manager.replaceSelection(firstKey);
|
|
233
|
+
}
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
236
|
break;
|
|
@@ -235,15 +239,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
235
239
|
e.preventDefault();
|
|
236
240
|
let lastKey = delegate.getLastKey(manager.focusedKey, isCtrlKeyPressed(e));
|
|
237
241
|
manager.setFocusedKey(lastKey);
|
|
238
|
-
if (
|
|
239
|
-
manager.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
if (lastKey != null) {
|
|
243
|
+
if (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') {
|
|
244
|
+
manager.extendSelection(lastKey);
|
|
245
|
+
} else if (selectOnFocus) {
|
|
246
|
+
manager.replaceSelection(lastKey);
|
|
247
|
+
}
|
|
242
248
|
}
|
|
243
249
|
}
|
|
244
250
|
break;
|
|
245
251
|
case 'PageDown':
|
|
246
|
-
if (delegate.getKeyPageBelow) {
|
|
252
|
+
if (delegate.getKeyPageBelow && manager.focusedKey != null) {
|
|
247
253
|
let nextKey = delegate.getKeyPageBelow(manager.focusedKey);
|
|
248
254
|
if (nextKey != null) {
|
|
249
255
|
e.preventDefault();
|
|
@@ -252,7 +258,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
252
258
|
}
|
|
253
259
|
break;
|
|
254
260
|
case 'PageUp':
|
|
255
|
-
if (delegate.getKeyPageAbove) {
|
|
261
|
+
if (delegate.getKeyPageAbove && manager.focusedKey != null) {
|
|
256
262
|
let nextKey = delegate.getKeyPageAbove(manager.focusedKey);
|
|
257
263
|
if (nextKey != null) {
|
|
258
264
|
e.preventDefault();
|
|
@@ -285,7 +291,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
285
291
|
ref.current.focus();
|
|
286
292
|
} else {
|
|
287
293
|
let walker = getFocusableTreeWalker(ref.current, {tabbable: true});
|
|
288
|
-
let next: FocusableElement;
|
|
294
|
+
let next: FocusableElement | undefined = undefined;
|
|
289
295
|
let last: FocusableElement;
|
|
290
296
|
do {
|
|
291
297
|
last = walker.lastChild() as FocusableElement;
|
|
@@ -307,10 +313,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
307
313
|
// Store the scroll position so we can restore it later.
|
|
308
314
|
/// TODO: should this happen all the time??
|
|
309
315
|
let scrollPos = useRef({top: 0, left: 0});
|
|
310
|
-
useEvent(scrollRef, 'scroll', isVirtualized ?
|
|
316
|
+
useEvent(scrollRef, 'scroll', isVirtualized ? undefined : () => {
|
|
311
317
|
scrollPos.current = {
|
|
312
|
-
top: scrollRef.current
|
|
313
|
-
left: scrollRef.current
|
|
318
|
+
top: scrollRef.current?.scrollTop ?? 0,
|
|
319
|
+
left: scrollRef.current?.scrollLeft ?? 0
|
|
314
320
|
};
|
|
315
321
|
});
|
|
316
322
|
|
|
@@ -332,7 +338,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
332
338
|
manager.setFocused(true);
|
|
333
339
|
|
|
334
340
|
if (manager.focusedKey == null) {
|
|
335
|
-
let navigateToFirstKey = (key: Key | undefined) => {
|
|
341
|
+
let navigateToFirstKey = (key: Key | undefined | null) => {
|
|
336
342
|
if (key != null) {
|
|
337
343
|
manager.setFocusedKey(key);
|
|
338
344
|
if (selectOnFocus) {
|
|
@@ -345,17 +351,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
345
351
|
// and either focus the first or last item accordingly.
|
|
346
352
|
let relatedTarget = e.relatedTarget as Element;
|
|
347
353
|
if (relatedTarget && (e.currentTarget.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
348
|
-
navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey());
|
|
354
|
+
navigateToFirstKey(manager.lastSelectedKey ?? delegate.getLastKey?.());
|
|
349
355
|
} else {
|
|
350
|
-
navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey());
|
|
356
|
+
navigateToFirstKey(manager.firstSelectedKey ?? delegate.getFirstKey?.());
|
|
351
357
|
}
|
|
352
|
-
} else if (!isVirtualized) {
|
|
358
|
+
} else if (!isVirtualized && scrollRef.current) {
|
|
353
359
|
// Restore the scroll position to what it was before.
|
|
354
360
|
scrollRef.current.scrollTop = scrollPos.current.top;
|
|
355
361
|
scrollRef.current.scrollLeft = scrollPos.current.left;
|
|
356
362
|
}
|
|
357
363
|
|
|
358
|
-
if (manager.focusedKey != null) {
|
|
364
|
+
if (manager.focusedKey != null && scrollRef.current) {
|
|
359
365
|
// Refocus and scroll the focused item into view if it exists within the scrollable region.
|
|
360
366
|
let element = scrollRef.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement;
|
|
361
367
|
if (element) {
|
|
@@ -382,13 +388,13 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
382
388
|
const autoFocusRef = useRef(autoFocus);
|
|
383
389
|
useEffect(() => {
|
|
384
390
|
if (autoFocusRef.current) {
|
|
385
|
-
let focusedKey = null;
|
|
391
|
+
let focusedKey: Key | null = null;
|
|
386
392
|
|
|
387
393
|
// Check focus strategy to determine which item to focus
|
|
388
394
|
if (autoFocus === 'first') {
|
|
389
|
-
focusedKey = delegate.getFirstKey();
|
|
395
|
+
focusedKey = delegate.getFirstKey?.() ?? null;
|
|
390
396
|
} if (autoFocus === 'last') {
|
|
391
|
-
focusedKey = delegate.getLastKey();
|
|
397
|
+
focusedKey = delegate.getLastKey?.() ?? null;
|
|
392
398
|
}
|
|
393
399
|
|
|
394
400
|
// If there are any selected keys, make the first one the new focus target
|
|
@@ -406,7 +412,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
406
412
|
manager.setFocusedKey(focusedKey);
|
|
407
413
|
|
|
408
414
|
// If no default focus key is selected, focus the collection itself.
|
|
409
|
-
if (focusedKey == null && !shouldUseVirtualFocus) {
|
|
415
|
+
if (focusedKey == null && !shouldUseVirtualFocus && ref.current) {
|
|
410
416
|
focusSafely(ref.current);
|
|
411
417
|
}
|
|
412
418
|
}
|
|
@@ -416,7 +422,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
416
422
|
// Scroll the focused element into view when the focusedKey changes.
|
|
417
423
|
let lastFocusedKey = useRef(manager.focusedKey);
|
|
418
424
|
useEffect(() => {
|
|
419
|
-
if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || autoFocusRef.current) && scrollRef
|
|
425
|
+
if (manager.isFocused && manager.focusedKey != null && (manager.focusedKey !== lastFocusedKey.current || autoFocusRef.current) && scrollRef.current && ref.current) {
|
|
420
426
|
let modality = getInteractionModality();
|
|
421
427
|
let element = ref.current.querySelector(`[data-key="${CSS.escape(manager.focusedKey.toString())}"]`) as HTMLElement;
|
|
422
428
|
if (!element) {
|
|
@@ -436,7 +442,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
436
442
|
}
|
|
437
443
|
|
|
438
444
|
// If the focused key becomes null (e.g. the last item is deleted), focus the whole collection.
|
|
439
|
-
if (!shouldUseVirtualFocus && manager.isFocused && manager.focusedKey == null && lastFocusedKey.current != null) {
|
|
445
|
+
if (!shouldUseVirtualFocus && manager.isFocused && manager.focusedKey == null && lastFocusedKey.current != null && ref.current) {
|
|
440
446
|
focusSafely(ref.current);
|
|
441
447
|
}
|
|
442
448
|
|
|
@@ -476,7 +482,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
|
|
|
476
482
|
// This will be marshalled to either the first or last item depending on where focus came from.
|
|
477
483
|
// If using virtual focus, don't set a tabIndex at all so that VoiceOver on iOS 14 doesn't try
|
|
478
484
|
// to move real DOM focus to the element anyway.
|
|
479
|
-
let tabIndex: number;
|
|
485
|
+
let tabIndex: number | undefined = undefined;
|
|
480
486
|
if (!shouldUseVirtualFocus) {
|
|
481
487
|
tabIndex = manager.focusedKey == null ? 0 : -1;
|
|
482
488
|
}
|
package/src/useSelectableItem.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {DOMAttributes, FocusableElement, Key, LongPressEvent, PressEvent, RefObject} from '@react-types/shared';
|
|
13
|
+
import {DOMAttributes, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
|
|
14
14
|
import {focusSafely} from '@react-aria/focus';
|
|
15
15
|
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
|
|
16
16
|
import {mergeProps, openLink, useRouter} from '@react-aria/utils';
|
|
@@ -130,7 +130,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (manager.isLink(key)) {
|
|
133
|
-
if (linkBehavior === 'selection') {
|
|
133
|
+
if (linkBehavior === 'selection' && ref.current) {
|
|
134
134
|
let itemProps = manager.getItemProps(key);
|
|
135
135
|
router.open(ref.current, e, itemProps.href, itemProps.routerOptions);
|
|
136
136
|
// Always set selected keys back to what they were so that select and combobox close.
|
|
@@ -164,7 +164,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
164
164
|
if (isFocused && manager.isFocused && !shouldUseVirtualFocus) {
|
|
165
165
|
if (focus) {
|
|
166
166
|
focus();
|
|
167
|
-
} else if (document.activeElement !== ref.current) {
|
|
167
|
+
} else if (document.activeElement !== ref.current && ref.current) {
|
|
168
168
|
focusSafely(ref.current);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -207,7 +207,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
207
207
|
);
|
|
208
208
|
let hasSecondaryAction = allowsActions && allowsSelection && manager.selectionBehavior === 'replace';
|
|
209
209
|
let hasAction = hasPrimaryAction || hasSecondaryAction;
|
|
210
|
-
let modality = useRef(null);
|
|
210
|
+
let modality = useRef<PointerType | null>(null);
|
|
211
211
|
|
|
212
212
|
let longPressEnabled = hasAction && allowsSelection;
|
|
213
213
|
let longPressEnabledOnPressStart = useRef(false);
|
|
@@ -218,7 +218,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
218
218
|
onAction();
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
if (hasLinkAction) {
|
|
221
|
+
if (hasLinkAction && ref.current) {
|
|
222
222
|
let itemProps = manager.getItemProps(key);
|
|
223
223
|
router.open(ref.current, e, itemProps.href, itemProps.routerOptions);
|
|
224
224
|
}
|
|
@@ -256,13 +256,13 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
|
|
|
256
256
|
}
|
|
257
257
|
};
|
|
258
258
|
} else {
|
|
259
|
-
itemPressProps.onPressUp = hasPrimaryAction ?
|
|
259
|
+
itemPressProps.onPressUp = hasPrimaryAction ? undefined : (e) => {
|
|
260
260
|
if (e.pointerType !== 'keyboard' && allowsSelection) {
|
|
261
261
|
onSelect(e);
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
264
|
|
|
265
|
-
itemPressProps.onPress = hasPrimaryAction ? performAction :
|
|
265
|
+
itemPressProps.onPress = hasPrimaryAction ? performAction : undefined;
|
|
266
266
|
}
|
|
267
267
|
} else {
|
|
268
268
|
itemPressProps.onPressStart = (e) => {
|
package/src/useTypeSelect.ts
CHANGED
|
@@ -46,9 +46,9 @@ export interface TypeSelectAria {
|
|
|
46
46
|
*/
|
|
47
47
|
export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
|
|
48
48
|
let {keyboardDelegate, selectionManager, onTypeSelect} = options;
|
|
49
|
-
let state = useRef({
|
|
49
|
+
let state = useRef<{search: string, timeout: ReturnType<typeof setTimeout> | undefined}>({
|
|
50
50
|
search: '',
|
|
51
|
-
timeout:
|
|
51
|
+
timeout: undefined
|
|
52
52
|
}).current;
|
|
53
53
|
|
|
54
54
|
let onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -70,19 +70,21 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
|
|
|
70
70
|
|
|
71
71
|
state.search += character;
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
if (keyboardDelegate.getKeyForSearch != null) {
|
|
74
|
+
// Use the delegate to find a key to focus.
|
|
75
|
+
// Prioritize items after the currently focused item, falling back to searching the whole list.
|
|
76
|
+
let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// If no key found, search from the top.
|
|
79
|
+
if (key == null) {
|
|
80
|
+
key = keyboardDelegate.getKeyForSearch(state.search);
|
|
81
|
+
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if (key != null) {
|
|
84
|
+
selectionManager.setFocusedKey(key);
|
|
85
|
+
if (onTypeSelect) {
|
|
86
|
+
onTypeSelect(key);
|
|
87
|
+
}
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -96,7 +98,7 @@ export function useTypeSelect(options: AriaTypeSelectOptions): TypeSelectAria {
|
|
|
96
98
|
typeSelectProps: {
|
|
97
99
|
// Using a capturing listener to catch the keydown event before
|
|
98
100
|
// other hooks in order to handle the Spacebar event.
|
|
99
|
-
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown :
|
|
101
|
+
onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined
|
|
100
102
|
}
|
|
101
103
|
};
|
|
102
104
|
}
|