@react-aria/autocomplete 3.0.0-alpha.9 → 3.0.0-beta.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 (196) hide show
  1. package/dist/ar-AE.main.js +6 -0
  2. package/dist/ar-AE.main.js.map +1 -0
  3. package/dist/ar-AE.mjs +8 -0
  4. package/dist/ar-AE.module.js +8 -0
  5. package/dist/ar-AE.module.js.map +1 -0
  6. package/dist/bg-BG.main.js +6 -0
  7. package/dist/bg-BG.main.js.map +1 -0
  8. package/dist/bg-BG.mjs +8 -0
  9. package/dist/bg-BG.module.js +8 -0
  10. package/dist/bg-BG.module.js.map +1 -0
  11. package/dist/cs-CZ.main.js +6 -0
  12. package/dist/cs-CZ.main.js.map +1 -0
  13. package/dist/cs-CZ.mjs +8 -0
  14. package/dist/cs-CZ.module.js +8 -0
  15. package/dist/cs-CZ.module.js.map +1 -0
  16. package/dist/da-DK.main.js +6 -0
  17. package/dist/da-DK.main.js.map +1 -0
  18. package/dist/da-DK.mjs +8 -0
  19. package/dist/da-DK.module.js +8 -0
  20. package/dist/da-DK.module.js.map +1 -0
  21. package/dist/de-DE.main.js +6 -0
  22. package/dist/de-DE.main.js.map +1 -0
  23. package/dist/de-DE.mjs +8 -0
  24. package/dist/de-DE.module.js +8 -0
  25. package/dist/de-DE.module.js.map +1 -0
  26. package/dist/el-GR.main.js +6 -0
  27. package/dist/el-GR.main.js.map +1 -0
  28. package/dist/el-GR.mjs +8 -0
  29. package/dist/el-GR.module.js +8 -0
  30. package/dist/el-GR.module.js.map +1 -0
  31. package/dist/en-US.main.js +6 -0
  32. package/dist/en-US.main.js.map +1 -0
  33. package/dist/en-US.mjs +8 -0
  34. package/dist/en-US.module.js +8 -0
  35. package/dist/en-US.module.js.map +1 -0
  36. package/dist/es-ES.main.js +6 -0
  37. package/dist/es-ES.main.js.map +1 -0
  38. package/dist/es-ES.mjs +8 -0
  39. package/dist/es-ES.module.js +8 -0
  40. package/dist/es-ES.module.js.map +1 -0
  41. package/dist/et-EE.main.js +6 -0
  42. package/dist/et-EE.main.js.map +1 -0
  43. package/dist/et-EE.mjs +8 -0
  44. package/dist/et-EE.module.js +8 -0
  45. package/dist/et-EE.module.js.map +1 -0
  46. package/dist/fi-FI.main.js +6 -0
  47. package/dist/fi-FI.main.js.map +1 -0
  48. package/dist/fi-FI.mjs +8 -0
  49. package/dist/fi-FI.module.js +8 -0
  50. package/dist/fi-FI.module.js.map +1 -0
  51. package/dist/fr-FR.main.js +6 -0
  52. package/dist/fr-FR.main.js.map +1 -0
  53. package/dist/fr-FR.mjs +8 -0
  54. package/dist/fr-FR.module.js +8 -0
  55. package/dist/fr-FR.module.js.map +1 -0
  56. package/dist/he-IL.main.js +6 -0
  57. package/dist/he-IL.main.js.map +1 -0
  58. package/dist/he-IL.mjs +8 -0
  59. package/dist/he-IL.module.js +8 -0
  60. package/dist/he-IL.module.js.map +1 -0
  61. package/dist/hr-HR.main.js +6 -0
  62. package/dist/hr-HR.main.js.map +1 -0
  63. package/dist/hr-HR.mjs +8 -0
  64. package/dist/hr-HR.module.js +8 -0
  65. package/dist/hr-HR.module.js.map +1 -0
  66. package/dist/hu-HU.main.js +6 -0
  67. package/dist/hu-HU.main.js.map +1 -0
  68. package/dist/hu-HU.mjs +8 -0
  69. package/dist/hu-HU.module.js +8 -0
  70. package/dist/hu-HU.module.js.map +1 -0
  71. package/dist/import.mjs +19 -0
  72. package/dist/intlStrings.main.js +108 -0
  73. package/dist/intlStrings.main.js.map +1 -0
  74. package/dist/intlStrings.mjs +110 -0
  75. package/dist/intlStrings.module.js +110 -0
  76. package/dist/intlStrings.module.js.map +1 -0
  77. package/dist/it-IT.main.js +6 -0
  78. package/dist/it-IT.main.js.map +1 -0
  79. package/dist/it-IT.mjs +8 -0
  80. package/dist/it-IT.module.js +8 -0
  81. package/dist/it-IT.module.js.map +1 -0
  82. package/dist/ja-JP.main.js +6 -0
  83. package/dist/ja-JP.main.js.map +1 -0
  84. package/dist/ja-JP.mjs +8 -0
  85. package/dist/ja-JP.module.js +8 -0
  86. package/dist/ja-JP.module.js.map +1 -0
  87. package/dist/ko-KR.main.js +6 -0
  88. package/dist/ko-KR.main.js.map +1 -0
  89. package/dist/ko-KR.mjs +8 -0
  90. package/dist/ko-KR.module.js +8 -0
  91. package/dist/ko-KR.module.js.map +1 -0
  92. package/dist/lt-LT.main.js +6 -0
  93. package/dist/lt-LT.main.js.map +1 -0
  94. package/dist/lt-LT.mjs +8 -0
  95. package/dist/lt-LT.module.js +8 -0
  96. package/dist/lt-LT.module.js.map +1 -0
  97. package/dist/lv-LV.main.js +6 -0
  98. package/dist/lv-LV.main.js.map +1 -0
  99. package/dist/lv-LV.mjs +8 -0
  100. package/dist/lv-LV.module.js +8 -0
  101. package/dist/lv-LV.module.js.map +1 -0
  102. package/dist/main.js +16 -42
  103. package/dist/main.js.map +1 -1
  104. package/dist/module.js +14 -42
  105. package/dist/module.js.map +1 -1
  106. package/dist/nb-NO.main.js +6 -0
  107. package/dist/nb-NO.main.js.map +1 -0
  108. package/dist/nb-NO.mjs +8 -0
  109. package/dist/nb-NO.module.js +8 -0
  110. package/dist/nb-NO.module.js.map +1 -0
  111. package/dist/nl-NL.main.js +6 -0
  112. package/dist/nl-NL.main.js.map +1 -0
  113. package/dist/nl-NL.mjs +8 -0
  114. package/dist/nl-NL.module.js +8 -0
  115. package/dist/nl-NL.module.js.map +1 -0
  116. package/dist/pl-PL.main.js +6 -0
  117. package/dist/pl-PL.main.js.map +1 -0
  118. package/dist/pl-PL.mjs +8 -0
  119. package/dist/pl-PL.module.js +8 -0
  120. package/dist/pl-PL.module.js.map +1 -0
  121. package/dist/pt-BR.main.js +6 -0
  122. package/dist/pt-BR.main.js.map +1 -0
  123. package/dist/pt-BR.mjs +8 -0
  124. package/dist/pt-BR.module.js +8 -0
  125. package/dist/pt-BR.module.js.map +1 -0
  126. package/dist/pt-PT.main.js +6 -0
  127. package/dist/pt-PT.main.js.map +1 -0
  128. package/dist/pt-PT.mjs +8 -0
  129. package/dist/pt-PT.module.js +8 -0
  130. package/dist/pt-PT.module.js.map +1 -0
  131. package/dist/ro-RO.main.js +6 -0
  132. package/dist/ro-RO.main.js.map +1 -0
  133. package/dist/ro-RO.mjs +8 -0
  134. package/dist/ro-RO.module.js +8 -0
  135. package/dist/ro-RO.module.js.map +1 -0
  136. package/dist/ru-RU.main.js +6 -0
  137. package/dist/ru-RU.main.js.map +1 -0
  138. package/dist/ru-RU.mjs +8 -0
  139. package/dist/ru-RU.module.js +8 -0
  140. package/dist/ru-RU.module.js.map +1 -0
  141. package/dist/sk-SK.main.js +6 -0
  142. package/dist/sk-SK.main.js.map +1 -0
  143. package/dist/sk-SK.mjs +8 -0
  144. package/dist/sk-SK.module.js +8 -0
  145. package/dist/sk-SK.module.js.map +1 -0
  146. package/dist/sl-SI.main.js +6 -0
  147. package/dist/sl-SI.main.js.map +1 -0
  148. package/dist/sl-SI.mjs +8 -0
  149. package/dist/sl-SI.module.js +8 -0
  150. package/dist/sl-SI.module.js.map +1 -0
  151. package/dist/sr-SP.main.js +6 -0
  152. package/dist/sr-SP.main.js.map +1 -0
  153. package/dist/sr-SP.mjs +8 -0
  154. package/dist/sr-SP.module.js +8 -0
  155. package/dist/sr-SP.module.js.map +1 -0
  156. package/dist/sv-SE.main.js +6 -0
  157. package/dist/sv-SE.main.js.map +1 -0
  158. package/dist/sv-SE.mjs +8 -0
  159. package/dist/sv-SE.module.js +8 -0
  160. package/dist/sv-SE.module.js.map +1 -0
  161. package/dist/tr-TR.main.js +6 -0
  162. package/dist/tr-TR.main.js.map +1 -0
  163. package/dist/tr-TR.mjs +8 -0
  164. package/dist/tr-TR.module.js +8 -0
  165. package/dist/tr-TR.module.js.map +1 -0
  166. package/dist/types.d.ts +69 -15
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/uk-UA.main.js +6 -0
  169. package/dist/uk-UA.main.js.map +1 -0
  170. package/dist/uk-UA.mjs +8 -0
  171. package/dist/uk-UA.module.js +8 -0
  172. package/dist/uk-UA.module.js.map +1 -0
  173. package/dist/useAutocomplete.main.js +273 -0
  174. package/dist/useAutocomplete.main.js.map +1 -0
  175. package/dist/useAutocomplete.mjs +268 -0
  176. package/dist/useAutocomplete.module.js +268 -0
  177. package/dist/useAutocomplete.module.js.map +1 -0
  178. package/dist/useSearchAutocomplete.main.js +73 -0
  179. package/dist/useSearchAutocomplete.main.js.map +1 -0
  180. package/dist/useSearchAutocomplete.mjs +68 -0
  181. package/dist/useSearchAutocomplete.module.js +68 -0
  182. package/dist/useSearchAutocomplete.module.js.map +1 -0
  183. package/dist/zh-CN.main.js +6 -0
  184. package/dist/zh-CN.main.js.map +1 -0
  185. package/dist/zh-CN.mjs +8 -0
  186. package/dist/zh-CN.module.js +8 -0
  187. package/dist/zh-CN.module.js.map +1 -0
  188. package/dist/zh-TW.main.js +6 -0
  189. package/dist/zh-TW.main.js.map +1 -0
  190. package/dist/zh-TW.mjs +8 -0
  191. package/dist/zh-TW.module.js +8 -0
  192. package/dist/zh-TW.module.js.map +1 -0
  193. package/package.json +23 -21
  194. package/src/index.ts +5 -1
  195. package/src/useAutocomplete.ts +364 -0
  196. package/src/useSearchAutocomplete.ts +62 -27
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "@react-aria/autocomplete",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.0-beta.0",
4
4
  "description": "Spectrum UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/main.js",
7
7
  "module": "dist/module.js",
8
+ "exports": {
9
+ "types": "./dist/types.d.ts",
10
+ "import": "./dist/import.mjs",
11
+ "require": "./dist/main.js"
12
+ },
8
13
  "types": "dist/types.d.ts",
9
14
  "source": "src/index.ts",
10
15
  "files": [
@@ -17,30 +22,27 @@
17
22
  "url": "https://github.com/adobe/react-spectrum"
18
23
  },
19
24
  "dependencies": {
20
- "@babel/runtime": "^7.6.2",
21
- "@react-aria/combobox": "^3.4.0",
22
- "@react-aria/i18n": "^3.5.0",
23
- "@react-aria/listbox": "^3.6.0",
24
- "@react-aria/live-announcer": "^3.1.1",
25
- "@react-aria/menu": "^3.6.0",
26
- "@react-aria/overlays": "^3.10.0",
27
- "@react-aria/searchfield": "^3.4.0",
28
- "@react-aria/selection": "^3.10.0",
29
- "@react-aria/textfield": "^3.7.0",
30
- "@react-aria/utils": "^3.13.2",
31
- "@react-stately/collections": "^3.4.2",
32
- "@react-stately/combobox": "^3.2.0",
33
- "@react-types/autocomplete": "3.0.0-alpha.7",
34
- "@react-types/button": "^3.6.0",
35
- "@react-types/combobox": "^3.5.2",
36
- "@react-types/searchfield": "^3.3.2",
37
- "@react-types/shared": "^3.14.0"
25
+ "@react-aria/combobox": "^3.12.0",
26
+ "@react-aria/focus": "^3.20.0",
27
+ "@react-aria/i18n": "^3.12.6",
28
+ "@react-aria/interactions": "^3.24.0",
29
+ "@react-aria/listbox": "^3.14.1",
30
+ "@react-aria/searchfield": "^3.8.1",
31
+ "@react-aria/textfield": "^3.17.0",
32
+ "@react-aria/utils": "^3.28.0",
33
+ "@react-stately/autocomplete": "3.0.0-beta.0",
34
+ "@react-stately/combobox": "^3.10.3",
35
+ "@react-types/autocomplete": "3.0.0-alpha.29",
36
+ "@react-types/button": "^3.11.0",
37
+ "@react-types/shared": "^3.28.0",
38
+ "@swc/helpers": "^0.5.0"
38
39
  },
39
40
  "peerDependencies": {
40
- "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
41
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
42
+ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
41
43
  },
42
44
  "publishConfig": {
43
45
  "access": "public"
44
46
  },
45
- "gitHead": "cd7c0ec917122c7612f653c22f8ed558f8b66ecd"
47
+ "gitHead": "4d3c72c94eea2d72eb3a0e7d56000c6ef7e39726"
46
48
  }
package/src/index.ts CHANGED
@@ -10,4 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  export {useSearchAutocomplete} from './useSearchAutocomplete';
13
- export type {AriaSearchAutocompleteProps, SearchAutocompleteAria} from './useSearchAutocomplete';
13
+ export {useAutocomplete} from './useAutocomplete';
14
+
15
+ export type {AriaSearchAutocompleteOptions, SearchAutocompleteAria} from './useSearchAutocomplete';
16
+ export type {AriaSearchAutocompleteProps} from '@react-types/autocomplete';
17
+ export type {AriaAutocompleteProps, AriaAutocompleteOptions, AutocompleteAria, CollectionOptions} from './useAutocomplete';
@@ -0,0 +1,364 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaLabelingProps, BaseEvent, DOMProps, RefObject} from '@react-types/shared';
14
+ import {AriaTextFieldProps} from '@react-aria/textfield';
15
+ import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
16
+ import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isCtrlKeyPressed, mergeProps, mergeRefs, useEffectEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
17
+ import {dispatchVirtualBlur, dispatchVirtualFocus, moveVirtualFocus} from '@react-aria/focus';
18
+ import {getInteractionModality} from '@react-aria/interactions';
19
+ // @ts-ignore
20
+ import intlMessages from '../intl/*.json';
21
+ import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef} from 'react';
22
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
23
+
24
+ export interface CollectionOptions extends DOMProps, AriaLabelingProps {
25
+ /** Whether the collection items should use virtual focus instead of being focused directly. */
26
+ shouldUseVirtualFocus: boolean,
27
+ /** Whether typeahead is disabled. */
28
+ disallowTypeAhead: boolean
29
+ }
30
+ export interface AriaAutocompleteProps extends AutocompleteProps {
31
+ /**
32
+ * An optional filter function used to determine if a option should be included in the autocomplete list.
33
+ * Include this if the items you are providing to your wrapped collection aren't filtered by default.
34
+ */
35
+ filter?: (textValue: string, inputValue: string) => boolean,
36
+
37
+ /**
38
+ * Whether or not to focus the first item in the collection after a filter is performed.
39
+ * @default false
40
+ */
41
+ disableAutoFocusFirst?: boolean
42
+ }
43
+
44
+ export interface AriaAutocompleteOptions extends Omit<AriaAutocompleteProps, 'children'> {
45
+ /** The ref for the wrapped collection element. */
46
+ inputRef: RefObject<HTMLInputElement | null>,
47
+ /** The ref for the wrapped collection element. */
48
+ collectionRef: RefObject<HTMLElement | null>
49
+ }
50
+
51
+ export interface AutocompleteAria {
52
+ /** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. */
53
+ textFieldProps: AriaTextFieldProps,
54
+ /** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */
55
+ collectionProps: CollectionOptions,
56
+ /** Ref to attach to the wrapped collection. */
57
+ collectionRef: RefObject<HTMLElement | null>,
58
+ /** A filter function that returns if the provided collection node should be filtered out of the collection. */
59
+ filter?: (nodeTextValue: string) => boolean
60
+ }
61
+
62
+ /**
63
+ * Provides the behavior and accessibility implementation for an autocomplete component.
64
+ * An autocomplete combines a text input with a collection, allowing users to filter the collection's contents match a query.
65
+ * @param props - Props for the autocomplete.
66
+ * @param state - State for the autocomplete, as returned by `useAutocompleteState`.
67
+ */
68
+ export function useAutocomplete(props: AriaAutocompleteOptions, state: AutocompleteState): AutocompleteAria {
69
+ let {
70
+ inputRef,
71
+ collectionRef,
72
+ filter,
73
+ disableAutoFocusFirst = false
74
+ } = props;
75
+
76
+ let collectionId = useId();
77
+ let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
78
+ let delayNextActiveDescendant = useRef(false);
79
+ let queuedActiveDescendant = useRef<string | null>(null);
80
+ let lastCollectionNode = useRef<HTMLElement>(null);
81
+
82
+ // For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
83
+ // moving focus back to the subtriggers
84
+ let shouldUseVirtualFocus = getInteractionModality() !== 'virtual';
85
+
86
+ useEffect(() => {
87
+ return () => clearTimeout(timeout.current);
88
+ }, []);
89
+
90
+ let updateActiveDescendant = useEffectEvent((e: Event) => {
91
+ // Ensure input is focused if the user clicks on the collection directly.
92
+ if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
93
+ inputRef.current.focus();
94
+ }
95
+
96
+ let target = e.target as Element | null;
97
+ if (e.isTrusted || !target || queuedActiveDescendant.current === target.id) {
98
+ return;
99
+ }
100
+
101
+ clearTimeout(timeout.current);
102
+ if (target !== collectionRef.current) {
103
+ if (delayNextActiveDescendant.current) {
104
+ queuedActiveDescendant.current = target.id;
105
+ timeout.current = setTimeout(() => {
106
+ state.setFocusedNodeId(target.id);
107
+ }, 500);
108
+ } else {
109
+ queuedActiveDescendant.current = target.id;
110
+ state.setFocusedNodeId(target.id);
111
+ }
112
+ } else {
113
+ queuedActiveDescendant.current = null;
114
+ state.setFocusedNodeId(null);
115
+ }
116
+
117
+ delayNextActiveDescendant.current = false;
118
+ });
119
+
120
+ let callbackRef = useCallback((collectionNode) => {
121
+ if (collectionNode != null) {
122
+ // When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
123
+ // of the letter you just typed. If we recieve another focus event then we clear the queued update
124
+ // We track lastCollectionNode to do proper cleanup since callbackRefs just pass null when unmounting. This also handles
125
+ // React 19's extra call of the callback ref in strict mode
126
+ lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
127
+ lastCollectionNode.current = collectionNode;
128
+ collectionNode.addEventListener('focusin', updateActiveDescendant);
129
+ } else {
130
+ lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
131
+ }
132
+ }, [updateActiveDescendant]);
133
+
134
+ // Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
135
+ let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef]));
136
+
137
+ let focusFirstItem = useEffectEvent(() => {
138
+ delayNextActiveDescendant.current = true;
139
+ collectionRef.current?.dispatchEvent(
140
+ new CustomEvent(FOCUS_EVENT, {
141
+ cancelable: true,
142
+ bubbles: true,
143
+ detail: {
144
+ focusStrategy: 'first'
145
+ }
146
+ })
147
+ );
148
+ });
149
+
150
+ let clearVirtualFocus = useEffectEvent((clearFocusKey?: boolean) => {
151
+ moveVirtualFocus(getActiveElement());
152
+ queuedActiveDescendant.current = null;
153
+ state.setFocusedNodeId(null);
154
+ let clearFocusEvent = new CustomEvent(CLEAR_FOCUS_EVENT, {
155
+ cancelable: true,
156
+ bubbles: true,
157
+ detail: {
158
+ clearFocusKey
159
+ }
160
+ });
161
+ clearTimeout(timeout.current);
162
+ delayNextActiveDescendant.current = false;
163
+ collectionRef.current?.dispatchEvent(clearFocusEvent);
164
+ });
165
+
166
+ // TODO: update to see if we can tell what kind of event (paste vs backspace vs typing) is happening instead
167
+ let onChange = (value: string) => {
168
+ // Tell wrapped collection to focus the first element in the list when typing forward and to clear focused key when deleting text
169
+ // for screen reader announcements
170
+ if (state.inputValue !== value && state.inputValue.length <= value.length && !disableAutoFocusFirst) {
171
+ focusFirstItem();
172
+ } else {
173
+ // Fully clear focused key when backspacing since the list may change and thus we'd want to start fresh again
174
+ clearVirtualFocus(true);
175
+ }
176
+
177
+ state.setInputValue(value);
178
+ };
179
+
180
+ let keyDownTarget = useRef<Element | null>(null);
181
+ // For textfield specific keydown operations
182
+ let onKeyDown = (e: BaseEvent<ReactKeyboardEvent<any>>) => {
183
+ keyDownTarget.current = e.target as Element;
184
+ if (e.nativeEvent.isComposing) {
185
+ return;
186
+ }
187
+
188
+ let focusedNodeId = queuedActiveDescendant.current;
189
+ switch (e.key) {
190
+ case 'a':
191
+ if (isCtrlKeyPressed(e)) {
192
+ return;
193
+ }
194
+ break;
195
+ case 'Escape':
196
+ // Early return for Escape here so it doesn't leak the Escape event from the simulated collection event below and
197
+ // close the dialog prematurely. Ideally that should be up to the discretion of the input element hence the check
198
+ // for isPropagationStopped
199
+ if (e.isDefaultPrevented()) {
200
+ return;
201
+ }
202
+ break;
203
+ case ' ':
204
+ // Space shouldn't trigger onAction so early return.
205
+ return;
206
+ case 'Tab':
207
+ // Don't propogate Tab down to the collection, otherwise we will try to focus the collection via useSelectableCollection's Tab handler (aka shift tab logic)
208
+ // We want FocusScope to handle Tab if one exists (aka sub dialog), so special casepropogate
209
+ if ('continuePropagation' in e) {
210
+ e.continuePropagation();
211
+ }
212
+ return;
213
+ case 'Home':
214
+ case 'End':
215
+ case 'PageDown':
216
+ case 'PageUp':
217
+ case 'ArrowUp':
218
+ case 'ArrowDown': {
219
+ if ((e.key === 'Home' || e.key === 'End') && focusedNodeId == null && e.shiftKey) {
220
+ return;
221
+ }
222
+
223
+ // Prevent these keys from moving the text cursor in the input
224
+ e.preventDefault();
225
+ // Move virtual focus into the wrapped collection
226
+ let focusCollection = new CustomEvent(FOCUS_EVENT, {
227
+ cancelable: true,
228
+ bubbles: true
229
+ });
230
+
231
+ collectionRef.current?.dispatchEvent(focusCollection);
232
+ break;
233
+ }
234
+ }
235
+
236
+ // Emulate the keyboard events that happen in the input field in the wrapped collection. This is for triggering things like onAction via Enter
237
+ // or moving focus from one item to another. Stop propagation on the input event if it isn't already stopped so it doesn't leak out. For events
238
+ // like ESC, the dispatched event below will bubble out of the collection and be stopped if handled by useSelectableCollection, otherwise will bubble
239
+ // as expected
240
+ if (!e.isPropagationStopped()) {
241
+ e.stopPropagation();
242
+ }
243
+
244
+ let shouldPerformDefaultAction = true;
245
+ if (focusedNodeId == null) {
246
+ shouldPerformDefaultAction = collectionRef.current?.dispatchEvent(
247
+ new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
248
+ ) || false;
249
+ } else {
250
+ let item = document.getElementById(focusedNodeId);
251
+ shouldPerformDefaultAction = item?.dispatchEvent(
252
+ new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
253
+ ) || false;
254
+ }
255
+
256
+ if (shouldPerformDefaultAction) {
257
+ switch (e.key) {
258
+ case 'ArrowLeft':
259
+ case 'ArrowRight': {
260
+ // Clear the activedescendant so NVDA announcements aren't interrupted but retain the focused key in the collection so the
261
+ // user's keyboard navigation restarts from where they left off
262
+ clearVirtualFocus();
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ };
268
+
269
+ let onKeyUpCapture = useEffectEvent((e) => {
270
+ // Dispatch simulated key up events for things like triggering links in listbox
271
+ // Make sure to stop the propagation of the input keyup event so that the simulated keyup/down pair
272
+ // is detected by usePress instead of the original keyup originating from the input
273
+ if (e.target === keyDownTarget.current) {
274
+ e.stopImmediatePropagation();
275
+ let focusedNodeId = queuedActiveDescendant.current;
276
+ if (focusedNodeId == null) {
277
+ collectionRef.current?.dispatchEvent(
278
+ new KeyboardEvent(e.type, e)
279
+ );
280
+ } else {
281
+ let item = document.getElementById(focusedNodeId);
282
+ item?.dispatchEvent(
283
+ new KeyboardEvent(e.type, e)
284
+ );
285
+ }
286
+ }
287
+ });
288
+
289
+ useEffect(() => {
290
+ document.addEventListener('keyup', onKeyUpCapture, true);
291
+ return () => {
292
+ document.removeEventListener('keyup', onKeyUpCapture, true);
293
+ };
294
+ }, [onKeyUpCapture]);
295
+
296
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete');
297
+ let collectionProps = useLabels({
298
+ id: collectionId,
299
+ 'aria-label': stringFormatter.format('collectionLabel')
300
+ });
301
+
302
+ let filterFn = useCallback((nodeTextValue: string) => {
303
+ if (filter) {
304
+ return filter(nodeTextValue, state.inputValue);
305
+ }
306
+
307
+ return true;
308
+ }, [state.inputValue, filter]);
309
+
310
+ // Be sure to clear/restore the virtual + collection focus when blurring/refocusing the field so we only show the
311
+ // focus ring on the virtually focused collection when are actually interacting with the Autocomplete
312
+ let onBlur = (e: ReactFocusEvent) => {
313
+ if (!e.isTrusted) {
314
+ return;
315
+ }
316
+
317
+ let lastFocusedNode = queuedActiveDescendant.current ? document.getElementById(queuedActiveDescendant.current) : null;
318
+ if (lastFocusedNode) {
319
+ dispatchVirtualBlur(lastFocusedNode, e.relatedTarget);
320
+ }
321
+ };
322
+
323
+ let onFocus = (e: ReactFocusEvent) => {
324
+ if (!e.isTrusted) {
325
+ return;
326
+ }
327
+
328
+ let curFocusedNode = queuedActiveDescendant.current ? document.getElementById(queuedActiveDescendant.current) : null;
329
+ if (curFocusedNode) {
330
+ let target = e.target;
331
+ queueMicrotask(() => {
332
+ dispatchVirtualBlur(target, curFocusedNode);
333
+ dispatchVirtualFocus(curFocusedNode, target);
334
+ });
335
+ }
336
+ };
337
+
338
+ return {
339
+ textFieldProps: {
340
+ value: state.inputValue,
341
+ onChange,
342
+ onKeyDown,
343
+ autoComplete: 'off',
344
+ 'aria-haspopup': 'listbox',
345
+ 'aria-controls': collectionId,
346
+ // TODO: readd proper logic for completionMode = complete (aria-autocomplete: both)
347
+ 'aria-autocomplete': 'list',
348
+ 'aria-activedescendant': state.focusedNodeId ?? undefined,
349
+ // This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions.
350
+ autoCorrect: 'off',
351
+ // This disable's the macOS Safari spell check auto corrections.
352
+ spellCheck: 'false',
353
+ enterKeyHint: 'go',
354
+ onBlur,
355
+ onFocus
356
+ },
357
+ collectionProps: mergeProps(collectionProps, {
358
+ shouldUseVirtualFocus,
359
+ disallowTypeAhead: true
360
+ }),
361
+ collectionRef: mergedCollectionRef,
362
+ filter: filter != null ? filterFn : undefined
363
+ };
364
+ }
@@ -12,27 +12,15 @@
12
12
 
13
13
  import {AriaButtonProps} from '@react-types/button';
14
14
  import {AriaListBoxOptions} from '@react-aria/listbox';
15
+ import {AriaSearchAutocompleteProps} from '@react-types/autocomplete';
15
16
  import {ComboBoxState} from '@react-stately/combobox';
16
- import {DOMAttributes} from '@react-types/shared';
17
- import {InputHTMLAttributes, RefObject} from 'react';
18
- import {KeyboardDelegate} from '@react-types/shared';
17
+ import {DOMAttributes, KeyboardDelegate, LayoutDelegate, RefObject, ValidationResult} from '@react-types/shared';
18
+ import {InputHTMLAttributes} from 'react';
19
19
  import {mergeProps} from '@react-aria/utils';
20
- import {SearchAutocompleteProps} from '@react-types/autocomplete';
21
20
  import {useComboBox} from '@react-aria/combobox';
22
21
  import {useSearchField} from '@react-aria/searchfield';
23
22
 
24
- export interface AriaSearchAutocompleteProps<T> extends SearchAutocompleteProps<T> {
25
- /** The ref for the input element. */
26
- inputRef: RefObject<HTMLInputElement>,
27
- /** The ref for the list box popover. */
28
- popoverRef: RefObject<HTMLDivElement>,
29
- /** The ref for the list box. */
30
- listBoxRef: RefObject<HTMLElement>,
31
- /** An optional keyboard delegate implementation, to override the default. */
32
- keyboardDelegate?: KeyboardDelegate
33
- }
34
-
35
- export interface SearchAutocompleteAria<T> {
23
+ export interface SearchAutocompleteAria<T> extends ValidationResult {
36
24
  /** Props for the label element. */
37
25
  labelProps: DOMAttributes,
38
26
  /** Props for the search input element. */
@@ -40,7 +28,28 @@ export interface SearchAutocompleteAria<T> {
40
28
  /** Props for the list box, to be passed to [useListBox](useListBox.html). */
41
29
  listBoxProps: AriaListBoxOptions<T>,
42
30
  /** Props for the search input's clear button. */
43
- clearButtonProps: AriaButtonProps
31
+ clearButtonProps: AriaButtonProps,
32
+ /** Props for the search autocomplete description element, if any. */
33
+ descriptionProps: DOMAttributes,
34
+ /** Props for the search autocomplete error message element, if any. */
35
+ errorMessageProps: DOMAttributes
36
+ }
37
+
38
+ export interface AriaSearchAutocompleteOptions<T> extends AriaSearchAutocompleteProps<T> {
39
+ /** The ref for the input element. */
40
+ inputRef: RefObject<HTMLInputElement | null>,
41
+ /** The ref for the list box popover. */
42
+ popoverRef: RefObject<HTMLDivElement | null>,
43
+ /** The ref for the list box. */
44
+ listBoxRef: RefObject<HTMLElement | null>,
45
+ /** An optional keyboard delegate implementation, to override the default. */
46
+ keyboardDelegate?: KeyboardDelegate,
47
+ /**
48
+ * A delegate object that provides layout information for items in the collection.
49
+ * By default this uses the DOM, but this can be overridden to implement things like
50
+ * virtualized scrolling.
51
+ */
52
+ layoutDelegate?: LayoutDelegate
44
53
  }
45
54
 
46
55
  /**
@@ -49,42 +58,67 @@ export interface SearchAutocompleteAria<T> {
49
58
  * @param props - Props for the search autocomplete.
50
59
  * @param state - State for the search autocomplete, as returned by `useSearchAutocomplete`.
51
60
  */
52
- export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteProps<T>, state: ComboBoxState<T>): SearchAutocompleteAria<T> {
61
+ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteOptions<T>, state: ComboBoxState<T>): SearchAutocompleteAria<T> {
53
62
  let {
54
63
  popoverRef,
55
64
  inputRef,
56
65
  listBoxRef,
57
66
  keyboardDelegate,
58
- onSubmit = () => {}
67
+ layoutDelegate,
68
+ onSubmit = () => {},
69
+ onClear,
70
+ onKeyDown,
71
+ onKeyUp,
72
+ isInvalid,
73
+ validationState,
74
+ validationBehavior,
75
+ isRequired,
76
+ ...otherProps
59
77
  } = props;
60
78
 
61
79
  let {inputProps, clearButtonProps} = useSearchField({
62
- ...props,
80
+ ...otherProps,
63
81
  value: state.inputValue,
64
82
  onChange: state.setInputValue,
65
83
  autoComplete: 'off',
66
- onClear: () => state.setInputValue(''),
84
+ onClear: () => {
85
+ state.setInputValue('');
86
+ if (onClear) {
87
+ onClear();
88
+ }
89
+ },
67
90
  onSubmit: (value) => {
68
91
  // Prevent submission from search field if menu item was selected
69
92
  if (state.selectionManager.focusedKey === null) {
70
93
  onSubmit(value, null);
71
94
  }
72
- }
95
+ },
96
+ onKeyDown,
97
+ onKeyUp
73
98
  }, {
74
99
  value: state.inputValue,
75
100
  setValue: state.setInputValue
76
101
  }, inputRef);
77
-
78
102
 
79
- let {listBoxProps, labelProps, inputProps: comboBoxInputProps} = useComboBox(
103
+
104
+ let {listBoxProps, labelProps, inputProps: comboBoxInputProps, ...validation} = useComboBox(
80
105
  {
81
- ...props,
106
+ ...otherProps,
82
107
  keyboardDelegate,
108
+ layoutDelegate,
83
109
  popoverRef,
84
110
  listBoxRef,
85
111
  inputRef,
86
112
  onFocus: undefined,
87
- onBlur: undefined
113
+ onFocusChange: undefined,
114
+ onBlur: undefined,
115
+ onKeyDown: undefined,
116
+ onKeyUp: undefined,
117
+ isInvalid,
118
+ validationState,
119
+ validationBehavior,
120
+ isRequired,
121
+ validate: undefined
88
122
  },
89
123
  state
90
124
  );
@@ -93,6 +127,7 @@ export function useSearchAutocomplete<T>(props: AriaSearchAutocompleteProps<T>,
93
127
  labelProps,
94
128
  inputProps: mergeProps(inputProps, comboBoxInputProps),
95
129
  listBoxProps,
96
- clearButtonProps
130
+ clearButtonProps,
131
+ ...validation
97
132
  };
98
133
  }