@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.
Files changed (34) hide show
  1. package/LICENSE +201 -0
  2. package/dist/DOMLayoutDelegate.main.js +9 -6
  3. package/dist/DOMLayoutDelegate.main.js.map +1 -1
  4. package/dist/DOMLayoutDelegate.mjs +9 -6
  5. package/dist/DOMLayoutDelegate.module.js +9 -6
  6. package/dist/DOMLayoutDelegate.module.js.map +1 -1
  7. package/dist/ListKeyboardDelegate.main.js +38 -30
  8. package/dist/ListKeyboardDelegate.main.js.map +1 -1
  9. package/dist/ListKeyboardDelegate.mjs +38 -30
  10. package/dist/ListKeyboardDelegate.module.js +38 -30
  11. package/dist/ListKeyboardDelegate.module.js.map +1 -1
  12. package/dist/types.d.ts +12 -12
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/useSelectableCollection.main.js +37 -25
  15. package/dist/useSelectableCollection.main.js.map +1 -1
  16. package/dist/useSelectableCollection.mjs +37 -25
  17. package/dist/useSelectableCollection.module.js +37 -25
  18. package/dist/useSelectableCollection.module.js.map +1 -1
  19. package/dist/useSelectableItem.main.js +5 -5
  20. package/dist/useSelectableItem.main.js.map +1 -1
  21. package/dist/useSelectableItem.mjs +5 -5
  22. package/dist/useSelectableItem.module.js +5 -5
  23. package/dist/useSelectableItem.module.js.map +1 -1
  24. package/dist/useTypeSelect.main.js +12 -10
  25. package/dist/useTypeSelect.main.js.map +1 -1
  26. package/dist/useTypeSelect.mjs +12 -10
  27. package/dist/useTypeSelect.module.js +12 -10
  28. package/dist/useTypeSelect.module.js.map +1 -1
  29. package/package.json +12 -11
  30. package/src/DOMLayoutDelegate.ts +11 -8
  31. package/src/ListKeyboardDelegate.ts +46 -34
  32. package/src/useSelectableCollection.ts +38 -32
  33. package/src/useSelectableItem.ts +7 -7
  34. 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: null
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
- // Use the delegate to find a key to focus.
36
- // Prioritize items after the currently focused item, falling back to searching the whole list.
37
- let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);
38
- // If no key found, search from the top.
39
- if (key == null) key = keyboardDelegate.getKeyForSearch(state.search);
40
- if (key != null) {
41
- selectionManager.setFocusedKey(key);
42
- if (onTypeSelect) onTypeSelect(key);
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 : null
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,EAAE;QACjB,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,2CAA2C;QAC3C,+FAA+F;QAC/F,IAAI,MAAM,iBAAiB,eAAe,CAAC,MAAM,MAAM,EAAE,iBAAiB,UAAU;QAEpF,wCAAwC;QACxC,IAAI,OAAO,MACT,MAAM,iBAAiB,eAAe,CAAC,MAAM,MAAM;QAGrD,IAAI,OAAO,MAAM;YACf,iBAAiB,aAAa,CAAC;YAC/B,IAAI,cACF,aAAa;QAEjB;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({\n search: '',\n timeout: null\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 // 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 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 : null\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"}
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.20.0",
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.18.3",
26
- "@react-aria/i18n": "^3.12.3",
27
- "@react-aria/interactions": "^3.22.3",
28
- "@react-aria/utils": "^3.25.3",
29
- "@react-stately/selection": "^3.17.0",
30
- "@react-types/shared": "^3.25.0",
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
+ }
@@ -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.scrollWidth,
44
- height: container.scrollHeight
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.scrollLeft,
52
- y: container.scrollTop,
53
- width: container.offsetWidth,
54
- height: container.offsetHeight
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
- while (key != null) {
79
- let item = this.collection.getItem(key);
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 key;
82
+ return nextKey;
82
83
  }
83
84
 
84
- key = getNext(key);
85
+ nextKey = getNext(nextKey);
85
86
  }
86
87
 
87
88
  return null;
88
89
  }
89
90
 
90
91
  getNextKey(key: Key) {
91
- key = this.collection.getKeyAfter(key);
92
- return this.findNextNonDisabled(key, key => this.collection.getKeyAfter(key));
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
- key = this.collection.getKeyBefore(key);
97
- return this.findNextNonDisabled(key, key => this.collection.getKeyBefore(key));
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 itemRect = this.layoutDelegate.getItemRect(key);
106
- if (!itemRect) {
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
- key = nextKey(key);
114
- itemRect = this.layoutDelegate.getItemRect(key);
115
- } while (itemRect && shouldSkip(prevRect, itemRect));
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 key;
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
- key = this.getKeyAbove(key);
216
- itemRect = key == null ? null : this.layoutDelegate.getItemRect(key);
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
- key = this.getKeyAbove(key);
223
- itemRect = key == null ? null : this.layoutDelegate.getItemRect(key);
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 key ?? this.getFirstKey();
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
- key = this.getKeyBelow(key);
246
- itemRect = key == null ? null : this.layoutDelegate.getItemRect(key);
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
- key = this.getKeyBelow(key);
253
- itemRect = key == null ? null : this.layoutDelegate.getItemRect(key);
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 key ?? this.getLastKey();
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.contains(e.target as Element)) {
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.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
143
+ let item = scrollRef.current?.querySelector(`[data-key="${CSS.escape(key.toString())}"]`);
144
144
  let itemProps = manager.getItemProps(key);
145
- router.open(item, e, itemProps.href, itemProps.routerOptions);
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 (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') {
227
- manager.extendSelection(firstKey);
228
- } else if (selectOnFocus) {
229
- manager.replaceSelection(firstKey);
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 (isCtrlKeyPressed(e) && e.shiftKey && manager.selectionMode === 'multiple') {
239
- manager.extendSelection(lastKey);
240
- } else if (selectOnFocus) {
241
- manager.replaceSelection(lastKey);
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 ? null : () => {
316
+ useEvent(scrollRef, 'scroll', isVirtualized ? undefined : () => {
311
317
  scrollPos.current = {
312
- top: scrollRef.current.scrollTop,
313
- left: scrollRef.current.scrollLeft
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?.current) {
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
  }
@@ -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 ? null : (e) => {
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 : null;
265
+ itemPressProps.onPress = hasPrimaryAction ? performAction : undefined;
266
266
  }
267
267
  } else {
268
268
  itemPressProps.onPressStart = (e) => {
@@ -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: null
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
- // Use the delegate to find a key to focus.
74
- // Prioritize items after the currently focused item, falling back to searching the whole list.
75
- let key = keyboardDelegate.getKeyForSearch(state.search, selectionManager.focusedKey);
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
- // If no key found, search from the top.
78
- if (key == null) {
79
- key = keyboardDelegate.getKeyForSearch(state.search);
80
- }
78
+ // If no key found, search from the top.
79
+ if (key == null) {
80
+ key = keyboardDelegate.getKeyForSearch(state.search);
81
+ }
81
82
 
82
- if (key != null) {
83
- selectionManager.setFocusedKey(key);
84
- if (onTypeSelect) {
85
- onTypeSelect(key);
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 : null
101
+ onKeyDownCapture: keyboardDelegate.getKeyForSearch ? onKeyDown : undefined
100
102
  }
101
103
  };
102
104
  }