@react-spectrum/autocomplete 3.0.0-alpha.4 → 3.0.0-alpha.40

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 (226) hide show
  1. package/dist/MobileSearchAutocomplete.main.js +449 -0
  2. package/dist/MobileSearchAutocomplete.main.js.map +1 -0
  3. package/dist/MobileSearchAutocomplete.mjs +444 -0
  4. package/dist/MobileSearchAutocomplete.module.js +444 -0
  5. package/dist/MobileSearchAutocomplete.module.js.map +1 -0
  6. package/dist/SearchAutocomplete.main.js +293 -0
  7. package/dist/SearchAutocomplete.main.js.map +1 -0
  8. package/dist/SearchAutocomplete.mjs +288 -0
  9. package/dist/SearchAutocomplete.module.js +288 -0
  10. package/dist/SearchAutocomplete.module.js.map +1 -0
  11. package/dist/ar-AE.main.js +9 -0
  12. package/dist/ar-AE.main.js.map +1 -0
  13. package/dist/ar-AE.mjs +11 -0
  14. package/dist/ar-AE.module.js +11 -0
  15. package/dist/ar-AE.module.js.map +1 -0
  16. package/dist/bg-BG.main.js +9 -0
  17. package/dist/bg-BG.main.js.map +1 -0
  18. package/dist/bg-BG.mjs +11 -0
  19. package/dist/bg-BG.module.js +11 -0
  20. package/dist/bg-BG.module.js.map +1 -0
  21. package/dist/cs-CZ.main.js +9 -0
  22. package/dist/cs-CZ.main.js.map +1 -0
  23. package/dist/cs-CZ.mjs +11 -0
  24. package/dist/cs-CZ.module.js +11 -0
  25. package/dist/cs-CZ.module.js.map +1 -0
  26. package/dist/da-DK.main.js +9 -0
  27. package/dist/da-DK.main.js.map +1 -0
  28. package/dist/da-DK.mjs +11 -0
  29. package/dist/da-DK.module.js +11 -0
  30. package/dist/da-DK.module.js.map +1 -0
  31. package/dist/de-DE.main.js +9 -0
  32. package/dist/de-DE.main.js.map +1 -0
  33. package/dist/de-DE.mjs +11 -0
  34. package/dist/de-DE.module.js +11 -0
  35. package/dist/de-DE.module.js.map +1 -0
  36. package/dist/el-GR.main.js +9 -0
  37. package/dist/el-GR.main.js.map +1 -0
  38. package/dist/el-GR.mjs +11 -0
  39. package/dist/el-GR.module.js +11 -0
  40. package/dist/el-GR.module.js.map +1 -0
  41. package/dist/en-US.main.js +9 -0
  42. package/dist/en-US.main.js.map +1 -0
  43. package/dist/en-US.mjs +11 -0
  44. package/dist/en-US.module.js +11 -0
  45. package/dist/en-US.module.js.map +1 -0
  46. package/dist/es-ES.main.js +9 -0
  47. package/dist/es-ES.main.js.map +1 -0
  48. package/dist/es-ES.mjs +11 -0
  49. package/dist/es-ES.module.js +11 -0
  50. package/dist/es-ES.module.js.map +1 -0
  51. package/dist/et-EE.main.js +9 -0
  52. package/dist/et-EE.main.js.map +1 -0
  53. package/dist/et-EE.mjs +11 -0
  54. package/dist/et-EE.module.js +11 -0
  55. package/dist/et-EE.module.js.map +1 -0
  56. package/dist/fi-FI.main.js +9 -0
  57. package/dist/fi-FI.main.js.map +1 -0
  58. package/dist/fi-FI.mjs +11 -0
  59. package/dist/fi-FI.module.js +11 -0
  60. package/dist/fi-FI.module.js.map +1 -0
  61. package/dist/fr-FR.main.js +9 -0
  62. package/dist/fr-FR.main.js.map +1 -0
  63. package/dist/fr-FR.mjs +11 -0
  64. package/dist/fr-FR.module.js +11 -0
  65. package/dist/fr-FR.module.js.map +1 -0
  66. package/dist/he-IL.main.js +9 -0
  67. package/dist/he-IL.main.js.map +1 -0
  68. package/dist/he-IL.mjs +11 -0
  69. package/dist/he-IL.module.js +11 -0
  70. package/dist/he-IL.module.js.map +1 -0
  71. package/dist/hr-HR.main.js +9 -0
  72. package/dist/hr-HR.main.js.map +1 -0
  73. package/dist/hr-HR.mjs +11 -0
  74. package/dist/hr-HR.module.js +11 -0
  75. package/dist/hr-HR.module.js.map +1 -0
  76. package/dist/hu-HU.main.js +9 -0
  77. package/dist/hu-HU.main.js.map +1 -0
  78. package/dist/hu-HU.mjs +11 -0
  79. package/dist/hu-HU.module.js +11 -0
  80. package/dist/hu-HU.module.js.map +1 -0
  81. package/dist/import.mjs +20 -0
  82. package/dist/inputgroup_vars_css.main.js +86 -0
  83. package/dist/inputgroup_vars_css.main.js.map +1 -0
  84. package/dist/inputgroup_vars_css.mjs +88 -0
  85. package/dist/inputgroup_vars_css.module.js +88 -0
  86. package/dist/inputgroup_vars_css.module.js.map +1 -0
  87. package/dist/intlStrings.main.js +108 -0
  88. package/dist/intlStrings.main.js.map +1 -0
  89. package/dist/intlStrings.mjs +110 -0
  90. package/dist/intlStrings.module.js +110 -0
  91. package/dist/intlStrings.module.js.map +1 -0
  92. package/dist/it-IT.main.js +9 -0
  93. package/dist/it-IT.main.js.map +1 -0
  94. package/dist/it-IT.mjs +11 -0
  95. package/dist/it-IT.module.js +11 -0
  96. package/dist/it-IT.module.js.map +1 -0
  97. package/dist/ja-JP.main.js +9 -0
  98. package/dist/ja-JP.main.js.map +1 -0
  99. package/dist/ja-JP.mjs +11 -0
  100. package/dist/ja-JP.module.js +11 -0
  101. package/dist/ja-JP.module.js.map +1 -0
  102. package/dist/ko-KR.main.js +9 -0
  103. package/dist/ko-KR.main.js.map +1 -0
  104. package/dist/ko-KR.mjs +11 -0
  105. package/dist/ko-KR.module.js +11 -0
  106. package/dist/ko-KR.module.js.map +1 -0
  107. package/dist/lt-LT.main.js +9 -0
  108. package/dist/lt-LT.main.js.map +1 -0
  109. package/dist/lt-LT.mjs +11 -0
  110. package/dist/lt-LT.module.js +11 -0
  111. package/dist/lt-LT.module.js.map +1 -0
  112. package/dist/lv-LV.main.js +9 -0
  113. package/dist/lv-LV.main.js.map +1 -0
  114. package/dist/lv-LV.mjs +11 -0
  115. package/dist/lv-LV.module.js +11 -0
  116. package/dist/lv-LV.module.js.map +1 -0
  117. package/dist/main.js +14 -997
  118. package/dist/main.js.map +1 -1
  119. package/dist/module.js +13 -984
  120. package/dist/module.js.map +1 -1
  121. package/dist/nb-NO.main.js +9 -0
  122. package/dist/nb-NO.main.js.map +1 -0
  123. package/dist/nb-NO.mjs +11 -0
  124. package/dist/nb-NO.module.js +11 -0
  125. package/dist/nb-NO.module.js.map +1 -0
  126. package/dist/nl-NL.main.js +9 -0
  127. package/dist/nl-NL.main.js.map +1 -0
  128. package/dist/nl-NL.mjs +11 -0
  129. package/dist/nl-NL.module.js +11 -0
  130. package/dist/nl-NL.module.js.map +1 -0
  131. package/dist/pl-PL.main.js +9 -0
  132. package/dist/pl-PL.main.js.map +1 -0
  133. package/dist/pl-PL.mjs +11 -0
  134. package/dist/pl-PL.module.js +11 -0
  135. package/dist/pl-PL.module.js.map +1 -0
  136. package/dist/pt-BR.main.js +9 -0
  137. package/dist/pt-BR.main.js.map +1 -0
  138. package/dist/pt-BR.mjs +11 -0
  139. package/dist/pt-BR.module.js +11 -0
  140. package/dist/pt-BR.module.js.map +1 -0
  141. package/dist/pt-PT.main.js +9 -0
  142. package/dist/pt-PT.main.js.map +1 -0
  143. package/dist/pt-PT.mjs +11 -0
  144. package/dist/pt-PT.module.js +11 -0
  145. package/dist/pt-PT.module.js.map +1 -0
  146. package/dist/ro-RO.main.js +9 -0
  147. package/dist/ro-RO.main.js.map +1 -0
  148. package/dist/ro-RO.mjs +11 -0
  149. package/dist/ro-RO.module.js +11 -0
  150. package/dist/ro-RO.module.js.map +1 -0
  151. package/dist/ru-RU.main.js +9 -0
  152. package/dist/ru-RU.main.js.map +1 -0
  153. package/dist/ru-RU.mjs +11 -0
  154. package/dist/ru-RU.module.js +11 -0
  155. package/dist/ru-RU.module.js.map +1 -0
  156. package/dist/search_vars_css.main.js +53 -0
  157. package/dist/search_vars_css.main.js.map +1 -0
  158. package/dist/search_vars_css.mjs +55 -0
  159. package/dist/search_vars_css.module.js +55 -0
  160. package/dist/search_vars_css.module.js.map +1 -0
  161. package/dist/searchautocomplete.af2b88b2.css +55 -0
  162. package/dist/searchautocomplete.af2b88b2.css.map +1 -0
  163. package/dist/searchautocomplete_css.main.js +38 -0
  164. package/dist/searchautocomplete_css.main.js.map +1 -0
  165. package/dist/searchautocomplete_css.mjs +40 -0
  166. package/dist/searchautocomplete_css.module.js +40 -0
  167. package/dist/searchautocomplete_css.module.js.map +1 -0
  168. package/dist/sk-SK.main.js +9 -0
  169. package/dist/sk-SK.main.js.map +1 -0
  170. package/dist/sk-SK.mjs +11 -0
  171. package/dist/sk-SK.module.js +11 -0
  172. package/dist/sk-SK.module.js.map +1 -0
  173. package/dist/sl-SI.main.js +9 -0
  174. package/dist/sl-SI.main.js.map +1 -0
  175. package/dist/sl-SI.mjs +11 -0
  176. package/dist/sl-SI.module.js +11 -0
  177. package/dist/sl-SI.module.js.map +1 -0
  178. package/dist/sr-SP.main.js +9 -0
  179. package/dist/sr-SP.main.js.map +1 -0
  180. package/dist/sr-SP.mjs +11 -0
  181. package/dist/sr-SP.module.js +11 -0
  182. package/dist/sr-SP.module.js.map +1 -0
  183. package/dist/sv-SE.main.js +9 -0
  184. package/dist/sv-SE.main.js.map +1 -0
  185. package/dist/sv-SE.mjs +11 -0
  186. package/dist/sv-SE.module.js +11 -0
  187. package/dist/sv-SE.module.js.map +1 -0
  188. package/dist/textfield_vars_css.main.js +74 -0
  189. package/dist/textfield_vars_css.main.js.map +1 -0
  190. package/dist/textfield_vars_css.mjs +76 -0
  191. package/dist/textfield_vars_css.module.js +76 -0
  192. package/dist/textfield_vars_css.module.js.map +1 -0
  193. package/dist/tr-TR.main.js +9 -0
  194. package/dist/tr-TR.main.js.map +1 -0
  195. package/dist/tr-TR.mjs +11 -0
  196. package/dist/tr-TR.module.js +11 -0
  197. package/dist/tr-TR.module.js.map +1 -0
  198. package/dist/types.d.ts +6 -2
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/uk-UA.main.js +9 -0
  201. package/dist/uk-UA.main.js.map +1 -0
  202. package/dist/uk-UA.mjs +11 -0
  203. package/dist/uk-UA.module.js +11 -0
  204. package/dist/uk-UA.module.js.map +1 -0
  205. package/dist/vars.02beaa3c.css +649 -0
  206. package/dist/vars.02beaa3c.css.map +1 -0
  207. package/dist/vars.3a478514.css +223 -0
  208. package/dist/vars.3a478514.css.map +1 -0
  209. package/dist/vars.8b5c28ea.css +550 -0
  210. package/dist/vars.8b5c28ea.css.map +1 -0
  211. package/dist/zh-CN.main.js +9 -0
  212. package/dist/zh-CN.main.js.map +1 -0
  213. package/dist/zh-CN.mjs +11 -0
  214. package/dist/zh-CN.module.js +11 -0
  215. package/dist/zh-CN.module.js.map +1 -0
  216. package/dist/zh-TW.main.js +9 -0
  217. package/dist/zh-TW.main.js.map +1 -0
  218. package/dist/zh-TW.mjs +11 -0
  219. package/dist/zh-TW.module.js +11 -0
  220. package/dist/zh-TW.module.js.map +1 -0
  221. package/package.json +34 -29
  222. package/src/MobileSearchAutocomplete.tsx +228 -143
  223. package/src/SearchAutocomplete.tsx +116 -77
  224. package/src/index.ts +2 -3
  225. package/src/searchautocomplete.css +11 -5
  226. package/dist/main.css +0 -1
@@ -13,24 +13,32 @@
13
13
  import AlertMedium from '@spectrum-icons/ui/AlertMedium';
14
14
  import {AriaButtonProps} from '@react-types/button';
15
15
  import CheckmarkMedium from '@spectrum-icons/ui/CheckmarkMedium';
16
- import {classNames} from '@react-spectrum/utils';
16
+ import {classNames, useFocusableRef} from '@react-spectrum/utils';
17
17
  import {ClearButton} from '@react-spectrum/button';
18
18
  import {ComboBoxState, useComboBoxState} from '@react-stately/combobox';
19
- import {DismissButton} from '@react-aria/overlays';
19
+ import {DismissButton, useOverlayTrigger} from '@react-aria/overlays';
20
20
  import {Field} from '@react-spectrum/label';
21
21
  import {FocusableRef, ValidationState} from '@react-types/shared';
22
- import {FocusRing, FocusScope} from '@react-aria/focus';
23
- import {focusSafely} from '@react-aria/focus';
22
+ import {focusSafely, setInteractionModality, useHover} from '@react-aria/interactions';
23
+ import {FocusScope, useFocusRing} from '@react-aria/focus';
24
24
  // @ts-ignore
25
25
  import intlMessages from '../intl/*.json';
26
26
  import {ListBoxBase, useListBoxLayout} from '@react-spectrum/listbox';
27
27
  import Magnifier from '@spectrum-icons/ui/Magnifier';
28
- import {mergeProps, useId} from '@react-aria/utils';
28
+ import {mergeProps, useFormReset, useId} from '@react-aria/utils';
29
29
  import {ProgressCircle} from '@react-spectrum/progress';
30
- import React, {HTMLAttributes, ReactElement, ReactNode, RefObject, useCallback, useEffect, useRef, useState} from 'react';
30
+ import React, {
31
+ HTMLAttributes,
32
+ InputHTMLAttributes,
33
+ ReactElement,
34
+ ReactNode,
35
+ useCallback,
36
+ useEffect,
37
+ useRef,
38
+ useState
39
+ } from 'react';
31
40
  import searchAutocompleteStyles from './searchautocomplete.css';
32
41
  import searchStyles from '@adobe/spectrum-css-temp/components/search/vars.css';
33
- import {setInteractionModality, useHover} from '@react-aria/interactions';
34
42
  import {SpectrumSearchAutocompleteProps} from '@react-types/autocomplete';
35
43
  import styles from '@adobe/spectrum-css-temp/components/inputgroup/vars.css';
36
44
  import {TextFieldBase} from '@react-spectrum/textfield';
@@ -38,20 +46,22 @@ import textfieldStyles from '@adobe/spectrum-css-temp/components/textfield/vars.
38
46
  import {Tray} from '@react-spectrum/overlays';
39
47
  import {useButton} from '@react-aria/button';
40
48
  import {useDialog} from '@react-aria/dialog';
41
- import {useFilter, useMessageFormatter} from '@react-aria/i18n';
42
- import {useFocusableRef} from '@react-spectrum/utils';
43
- import {useLabel} from '@react-aria/label';
44
- import {useOverlayTrigger} from '@react-aria/overlays';
49
+ import {useField} from '@react-aria/label';
50
+ import {useFilter, useLocalizedStringFormatter} from '@react-aria/i18n';
51
+ import {useFormValidation} from '@react-aria/form';
45
52
  import {useProviderProps} from '@react-spectrum/provider';
46
53
  import {useSearchAutocomplete} from '@react-aria/autocomplete';
47
54
 
48
- export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
55
+ function ForwardMobileSearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
49
56
  props = useProviderProps(props);
50
57
 
51
58
  let {
52
59
  isQuiet,
53
60
  isDisabled,
54
- validationState,
61
+ isRequired,
62
+ validationBehavior,
63
+ validate,
64
+ name,
55
65
  isReadOnly,
56
66
  onSubmit = () => {}
57
67
  } = props;
@@ -68,38 +78,71 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
68
78
  allowsCustomValue: true,
69
79
  onSelectionChange: (key) => key !== null && onSubmit(null, key),
70
80
  selectedKey: undefined,
71
- defaultSelectedKey: undefined
81
+ defaultSelectedKey: undefined,
82
+ validate: useCallback(v => validate?.(v.inputValue), [validate])
72
83
  });
73
-
74
- let buttonRef = useRef<HTMLElement>();
84
+
85
+ let buttonRef = useRef<HTMLDivElement>(null);
75
86
  let domRef = useFocusableRef(ref, buttonRef);
76
87
  let {triggerProps, overlayProps} = useOverlayTrigger({type: 'listbox'}, state, buttonRef);
77
88
 
78
- let {labelProps, fieldProps} = useLabel({
89
+ let inputRef = useRef<HTMLInputElement>(null);
90
+ useFormValidation({
79
91
  ...props,
80
- labelElementType: 'span'
92
+ focus: () => buttonRef.current?.focus()
93
+ }, state, inputRef);
94
+ let {isInvalid, validationErrors, validationDetails} = state.displayValidation;
95
+ let validationState = props.validationState || (isInvalid ? 'invalid' : undefined);
96
+ let errorMessage = props.errorMessage ?? validationErrors.join(' ');
97
+
98
+ let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
99
+ ...props,
100
+ labelElementType: 'span',
101
+ isInvalid,
102
+ errorMessage
81
103
  });
82
104
 
83
105
  // Focus the button and show focus ring when clicking on the label
84
106
  labelProps.onClick = () => {
85
- if (!props.isDisabled) {
107
+ if (!props.isDisabled && buttonRef.current) {
86
108
  buttonRef.current.focus();
87
109
  setInteractionModality('keyboard');
88
110
  }
89
111
  };
90
112
 
91
- let onClose = () => state.commit();
113
+ let inputProps: InputHTMLAttributes<HTMLInputElement> = {
114
+ type: 'hidden',
115
+ name,
116
+ value: state.inputValue
117
+ };
118
+
119
+ if (validationBehavior === 'native') {
120
+ // Use a hidden <input type="text"> rather than <input type="hidden">
121
+ // so that an empty value blocks HTML form submission when the field is required.
122
+ inputProps.type = 'text';
123
+ inputProps.hidden = true;
124
+ inputProps.required = isRequired;
125
+ // Ignore react warning.
126
+ inputProps.onChange = () => {};
127
+ }
128
+
129
+ useFormReset(inputRef, state.inputValue, state.setInputValue);
92
130
 
93
131
  return (
94
132
  <>
95
133
  <Field
96
134
  {...props}
97
135
  labelProps={labelProps}
136
+ descriptionProps={descriptionProps}
137
+ errorMessageProps={errorMessageProps}
138
+ isInvalid={isInvalid}
139
+ validationErrors={validationErrors}
140
+ validationDetails={validationDetails}
98
141
  elementType="span"
99
142
  ref={domRef}
100
143
  includeNecessityIndicatorInAccessibilityName>
101
144
  <SearchAutocompleteButton
102
- {...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus})}
145
+ {...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus, icon: props.icon})}
103
146
  ref={buttonRef}
104
147
  isQuiet={isQuiet}
105
148
  isDisabled={isDisabled}
@@ -112,18 +155,23 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
112
155
  {state.inputValue || props.placeholder || ''}
113
156
  </SearchAutocompleteButton>
114
157
  </Field>
115
- <Tray isOpen={state.isOpen} onClose={onClose} isFixedHeight isNonModal {...overlayProps}>
158
+ <input {...inputProps} ref={inputRef} />
159
+ <Tray state={state} isFixedHeight {...overlayProps}>
116
160
  <SearchAutocompleteTray
117
161
  {...props}
118
- onClose={onClose}
162
+ onClose={state.close}
119
163
  overlayProps={overlayProps}
120
164
  state={state} />
121
165
  </Tray>
122
166
  </>
123
167
  );
124
- });
168
+ }
169
+
170
+ export let MobileSearchAutocomplete = React.forwardRef(ForwardMobileSearchAutocomplete) as <T>(props: SpectrumSearchAutocompleteProps<T> & {ref?: FocusableRef<HTMLElement>}) => ReactElement;
171
+
125
172
 
126
173
  interface SearchAutocompleteButtonProps extends AriaButtonProps {
174
+ icon?: ReactElement | null,
127
175
  isQuiet?: boolean,
128
176
  isDisabled?: boolean,
129
177
  isReadOnly?: boolean,
@@ -136,8 +184,15 @@ interface SearchAutocompleteButtonProps extends AriaButtonProps {
136
184
  className?: string
137
185
  }
138
186
 
139
- const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteButton(props: SearchAutocompleteButtonProps, ref: RefObject<HTMLElement>) {
187
+ // any type is because we don't want to call useObjectRef because this is an internal component and we know
188
+ // we are always passing an object ref
189
+ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteButton(props: SearchAutocompleteButtonProps, ref: any) {
190
+ let searchIcon = (
191
+ <Magnifier data-testid="searchicon" />
192
+ );
193
+
140
194
  let {
195
+ icon = searchIcon,
141
196
  isQuiet,
142
197
  isDisabled,
143
198
  isReadOnly,
@@ -149,33 +204,31 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
149
204
  style,
150
205
  className
151
206
  } = props;
152
- let formatMessage = useMessageFormatter(intlMessages);
207
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/autocomplete');
153
208
  let valueId = useId();
154
209
  let invalidId = useId();
155
210
  let validationIcon = validationState === 'invalid'
156
- ? <AlertMedium id={invalidId} aria-label={formatMessage('invalid')} />
211
+ ? <AlertMedium id={invalidId} aria-label={stringFormatter.format('invalid')} />
157
212
  : <CheckmarkMedium />;
158
213
 
159
- let searchIcon = (
160
- <Magnifier data-testid="searchicon" />
161
- );
162
-
163
- let icon = React.cloneElement(searchIcon, {
164
- UNSAFE_className: classNames(
165
- textfieldStyles,
166
- 'spectrum-Textfield-icon'
167
- ),
168
- size: 'S'
169
- });
214
+ if (icon) {
215
+ icon = React.cloneElement(icon, {
216
+ UNSAFE_className: classNames(
217
+ textfieldStyles,
218
+ 'spectrum-Textfield-icon'
219
+ ),
220
+ size: 'S'
221
+ });
222
+ }
170
223
 
171
224
  let clearButton = (
172
225
  <ClearButton
173
226
  onPress={(e) => {
174
- clearInput();
175
- props.onPress(e);
227
+ clearInput?.();
228
+ props?.onPress?.(e);
176
229
  }}
177
230
  preventFocus
178
- aria-label={formatMessage('clear')}
231
+ aria-label={stringFormatter.format('clear')}
179
232
  excludeFromTabOrder
180
233
  UNSAFE_className={
181
234
  classNames(
@@ -186,7 +239,6 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
186
239
  isDisabled={isDisabled} />
187
240
  );
188
241
 
189
-
190
242
  let validation = React.cloneElement(validationIcon, {
191
243
  UNSAFE_className: classNames(
192
244
  textfieldStyles,
@@ -194,11 +246,16 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
194
246
  classNames(
195
247
  styles,
196
248
  'spectrum-InputGroup-input-validationIcon'
249
+ ),
250
+ classNames(
251
+ searchStyles,
252
+ 'spectrum-Search-validationIcon'
197
253
  )
198
254
  )
199
255
  });
200
256
 
201
257
  let {hoverProps, isHovered} = useHover({});
258
+ let {isFocused, isFocusVisible, focusProps} = useFocusRing();
202
259
  let {buttonProps} = useButton({
203
260
  ...props,
204
261
  'aria-labelledby': [
@@ -211,102 +268,116 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
211
268
  }, ref);
212
269
 
213
270
  return (
214
- <FocusRing
215
- focusClass={classNames(styles, 'is-focused')}
216
- focusRingClass={classNames(styles, 'focus-ring')}>
271
+ <div
272
+ {...mergeProps(hoverProps, focusProps, buttonProps)}
273
+ aria-haspopup="dialog"
274
+ ref={ref}
275
+ style={{...style, outline: 'none'}}
276
+ className={
277
+ classNames(
278
+ styles,
279
+ 'spectrum-InputGroup',
280
+ {
281
+ 'spectrum-InputGroup--quiet': isQuiet,
282
+ 'is-disabled': isDisabled,
283
+ 'spectrum-InputGroup--invalid': validationState === 'invalid' && !isDisabled,
284
+ 'is-hovered': isHovered,
285
+ 'is-focused': isFocused,
286
+ 'focus-ring': isFocusVisible
287
+ },
288
+ classNames(
289
+ searchAutocompleteStyles,
290
+ 'searchautocomplete',
291
+ 'mobile-searchautocomplete'
292
+ ),
293
+ className
294
+ )
295
+ }>
217
296
  <div
218
- {...mergeProps(hoverProps, buttonProps)}
219
- aria-haspopup="dialog"
220
- ref={ref as RefObject<HTMLDivElement>}
221
- style={{...style, outline: 'none'}}
222
297
  className={
223
298
  classNames(
224
- styles,
225
- 'spectrum-InputGroup',
299
+ textfieldStyles,
300
+ 'spectrum-Textfield',
226
301
  {
227
- 'spectrum-InputGroup--quiet': isQuiet,
228
- 'is-disabled': isDisabled,
229
- 'spectrum-InputGroup--invalid': validationState === 'invalid',
230
- 'is-hovered': isHovered
302
+ 'spectrum-Textfield--invalid': validationState === 'invalid' && !isDisabled,
303
+ 'spectrum-Textfield--valid': validationState === 'valid' && !isDisabled,
304
+ 'spectrum-Textfield--quiet': isQuiet
231
305
  },
232
306
  classNames(
233
- searchAutocompleteStyles,
234
- 'mobile-searchautocomplete'
307
+ searchStyles,
308
+ 'spectrum-Search',
309
+ 'spectrum-Search--loadable',
310
+ {
311
+ 'is-disabled': isDisabled,
312
+ 'is-quiet': isQuiet,
313
+ 'spectrum-Search--invalid': validationState === 'invalid' && !isDisabled,
314
+ 'spectrum-Search--valid': validationState === 'valid' && !isDisabled
315
+ }
235
316
  ),
236
- className
317
+ classNames(
318
+ styles,
319
+ 'spectrum-InputGroup-field'
320
+ )
237
321
  )
238
322
  }>
239
323
  <div
240
324
  className={
241
325
  classNames(
242
326
  textfieldStyles,
243
- 'spectrum-Textfield',
327
+ 'spectrum-Textfield-input',
244
328
  {
245
- 'spectrum-Textfield--invalid': validationState === 'invalid',
246
- 'spectrum-Textfield--valid': validationState === 'valid',
247
- 'spectrum-Textfield--quiet': isQuiet
329
+ 'spectrum-Textfield-inputIcon': !!icon,
330
+ 'is-hovered': isHovered,
331
+ 'is-placeholder': isPlaceholder,
332
+ 'is-disabled': isDisabled,
333
+ 'is-quiet': isQuiet,
334
+ 'is-focused': isFocused
248
335
  },
249
336
  classNames(
250
337
  searchStyles,
251
- 'spectrum-Search',
252
- {
253
- 'is-disabled': isDisabled,
254
- 'is-quiet': isQuiet,
255
- 'spectrum-Search--invalid': validationState === 'invalid',
256
- 'spectrum-Search--valid': validationState === 'valid'
257
- }
338
+ 'spectrum-Search-input'
339
+ ),
340
+ classNames(
341
+ searchAutocompleteStyles,
342
+ 'mobile-input'
258
343
  )
259
344
  )
260
345
  }>
261
- <div
346
+ {icon}
347
+ <span
348
+ id={valueId}
262
349
  className={
263
350
  classNames(
264
- textfieldStyles,
265
- 'spectrum-Textfield-input',
266
- 'spectrum-Textfield-inputIcon',
267
- {
268
- 'is-hovered': isHovered,
269
- 'is-placeholder': isPlaceholder,
270
- 'is-disabled': isDisabled,
271
- 'is-quiet': isQuiet
272
- },
273
- classNames(
274
- searchStyles,
275
- 'spectrum-Search-input'
276
- )
351
+ searchAutocompleteStyles,
352
+ 'mobile-value'
277
353
  )
278
354
  }>
279
- {icon}
280
- <span
281
- id={valueId}
282
- className={
283
- classNames(
284
- searchAutocompleteStyles,
285
- 'mobile-value'
286
- )
287
- }>
288
- {children}
289
- </span>
290
- </div>
291
- {validationState ? validation : null}
292
- {(inputValue !== '' || validationState != null) && !isReadOnly && clearButton}
355
+ {children}
356
+ </span>
293
357
  </div>
358
+ {validationState && !isDisabled ? validation : null}
359
+ {(inputValue !== '' || validationState != null) && !isReadOnly && clearButton}
294
360
  </div>
295
- </FocusRing>
361
+ </div>
296
362
  );
297
363
  });
298
364
 
299
- interface SearchAutocompleteTrayProps extends SpectrumSearchAutocompleteProps<unknown> {
300
- state: ComboBoxState<unknown>,
365
+ interface SearchAutocompleteTrayProps<T> extends SpectrumSearchAutocompleteProps<T> {
366
+ state: ComboBoxState<T>,
301
367
  overlayProps: HTMLAttributes<HTMLElement>,
302
368
  loadingIndicator?: ReactElement,
303
369
  onClose: () => void
304
370
  }
305
371
 
306
- function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
372
+ function SearchAutocompleteTray<T>(props: SearchAutocompleteTrayProps<T>) {
373
+ let searchIcon = (
374
+ <Magnifier data-testid="searchicon" />
375
+ );
376
+
307
377
  let {
308
378
  // completionMode = 'suggest',
309
379
  state,
380
+ icon = searchIcon,
310
381
  isDisabled,
311
382
  validationState,
312
383
  label,
@@ -317,37 +388,43 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
317
388
  onSubmit
318
389
  } = props;
319
390
 
320
- let timeout = useRef(null);
391
+ let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
321
392
  let [showLoading, setShowLoading] = useState(false);
322
- let inputRef = useRef<HTMLInputElement>();
323
- let popoverRef = useRef<HTMLDivElement>();
324
- let listBoxRef = useRef<HTMLDivElement>();
325
- let layout = useListBoxLayout(state);
326
- let formatMessage = useMessageFormatter(intlMessages);
327
-
328
- let {inputProps, listBoxProps, labelProps, clearButtonProps} = useSearchAutocomplete(
393
+ let inputRef = useRef<HTMLInputElement>(null);
394
+ let popoverRef = useRef<HTMLDivElement>(null);
395
+ let listBoxRef = useRef<HTMLDivElement>(null);
396
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
397
+ let layout = useListBoxLayout();
398
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/autocomplete');
399
+
400
+ let {inputProps, listBoxProps, labelProps, clearButtonProps} = useSearchAutocomplete<T>(
329
401
  {
330
402
  ...props,
331
- keyboardDelegate: layout,
403
+ layoutDelegate: layout,
332
404
  popoverRef: popoverRef,
333
405
  listBoxRef,
334
- inputRef
406
+ inputRef,
407
+ // Handled outside the tray.
408
+ name: undefined
335
409
  },
336
410
  state
337
411
  );
338
412
 
339
413
  React.useEffect(() => {
340
- focusSafely(inputRef.current);
414
+ if (inputRef.current) {
415
+ focusSafely(inputRef.current);
416
+ }
417
+ }, []);
341
418
 
342
- // When the tray unmounts, set state.isFocused (i.e. the tray input's focus tracker) to false.
419
+ React.useEffect(() => {
420
+ // When the tray closes, set state.isFocused (i.e. the tray input's focus tracker) to false.
343
421
  // This is to prevent state.isFocused from being set to true when the tray closes via tapping on the underlay
344
422
  // (FocusScope attempts to restore focus to the tray input when tapping outside the tray due to "contain")
345
423
  // Have to do this manually since React doesn't call onBlur when a component is unmounted: https://github.com/facebook/react/issues/12363
346
- return () => {
424
+ if (!state.isOpen && state.isFocused) {
347
425
  state.setFocused(false);
348
- };
349
- // eslint-disable-next-line react-hooks/exhaustive-deps
350
- }, []);
426
+ }
427
+ });
351
428
 
352
429
  let {dialogProps} = useDialog({
353
430
  'aria-labelledby': useId(labelProps.id)
@@ -366,7 +443,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
366
443
  <ClearButton
367
444
  {...clearButtonProps}
368
445
  preventFocus
369
- aria-label={formatMessage('clear')}
446
+ aria-label={stringFormatter.format('clear')}
370
447
  excludeFromTabOrder
371
448
  UNSAFE_className={
372
449
  classNames(
@@ -379,7 +456,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
379
456
 
380
457
  let loadingCircle = (
381
458
  <ProgressCircle
382
- aria-label={formatMessage('loading')}
459
+ aria-label={stringFormatter.format('loading')}
383
460
  size="S"
384
461
  isIndeterminate
385
462
  UNSAFE_className={classNames(
@@ -409,7 +486,9 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
409
486
  return;
410
487
  }
411
488
 
412
- popoverRef.current.focus();
489
+ if (popoverRef.current) {
490
+ popoverRef.current.focus();
491
+ }
413
492
  }, [inputRef, popoverRef, isTouchDown]);
414
493
 
415
494
  let inputValue = inputProps.value;
@@ -432,8 +511,10 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
432
511
  } else if (loadingState !== 'filtering') {
433
512
  // If loading is no longer happening, clear any timers and hide the loading circle
434
513
  setShowLoading(false);
435
- clearTimeout(timeout.current);
436
- timeout.current = null;
514
+ if (timeout.current !== null) {
515
+ clearTimeout(timeout.current);
516
+ timeout.current = null;
517
+ }
437
518
  }
438
519
 
439
520
  lastInputValue.current = inputValue;
@@ -442,25 +523,29 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
442
523
  let onKeyDown = (e) => {
443
524
  // Close virtual keyboard, close tray, and fire onSubmit if user hits Enter w/o any focused options
444
525
  if (e.key === 'Enter' && state.selectionManager.focusedKey == null) {
445
- popoverRef.current.focus();
446
- onClose();
447
- onSubmit(inputValue.toString(), null);
526
+ popoverRef.current?.focus();
527
+ if (onClose) {
528
+ onClose();
529
+ }
530
+ if (onSubmit) {
531
+ onSubmit(inputValue == null ? null : inputValue.toString(), null);
532
+ }
448
533
  } else {
449
- inputProps.onKeyDown(e);
534
+ if (inputProps.onKeyDown) {
535
+ inputProps.onKeyDown(e);
536
+ }
450
537
  }
451
538
  };
452
539
 
453
- let searchIcon = (
454
- <Magnifier data-testid="searchicon" />
455
- );
456
-
457
- let icon = React.cloneElement(searchIcon, {
458
- UNSAFE_className: classNames(
459
- textfieldStyles,
460
- 'spectrum-Textfield-icon'
461
- ),
462
- size: 'S'
463
- });
540
+ if (icon) {
541
+ icon = React.cloneElement(icon, {
542
+ UNSAFE_className: classNames(
543
+ textfieldStyles,
544
+ 'spectrum-Textfield-icon'
545
+ ),
546
+ size: 'S'
547
+ });
548
+ }
464
549
 
465
550
  return (
466
551
  <FocusScope restoreFocus contain>
@@ -481,9 +566,9 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
481
566
  inputRef={inputRef}
482
567
  isDisabled={isDisabled}
483
568
  isLoading={showLoading && loadingState === 'filtering'}
484
- loadingIndicator={loadingState != null && loadingCircle}
569
+ loadingIndicator={loadingState != null ? loadingCircle : undefined}
485
570
  validationState={validationState}
486
- wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly && clearButton}
571
+ wrapperChildren={((state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly) ? clearButton : undefined}
487
572
  icon={icon}
488
573
  UNSAFE_className={
489
574
  classNames(
@@ -492,8 +577,8 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
492
577
  'spectrum-Textfield',
493
578
  'spectrum-Search--loadable',
494
579
  {
495
- 'spectrum-Search--invalid': validationState === 'invalid',
496
- 'spectrum-Search--valid': validationState === 'valid'
580
+ 'spectrum-Search--invalid': validationState === 'invalid' && !isDisabled,
581
+ 'spectrum-Search--valid': validationState === 'valid' && !isDisabled
497
582
  },
498
583
  classNames(
499
584
  searchAutocompleteStyles,
@@ -527,7 +612,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
527
612
  shouldUseVirtualFocus
528
613
  renderEmptyState={() => loadingState !== 'loading' && (
529
614
  <span className={classNames(searchAutocompleteStyles, 'no-results')}>
530
- {formatMessage('noResults')}
615
+ {stringFormatter.format('noResults')}
531
616
  </span>
532
617
  )}
533
618
  UNSAFE_className={
@@ -539,7 +624,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
539
624
  ref={listBoxRef}
540
625
  onScroll={onScroll}
541
626
  onLoadMore={onLoadMore}
542
- isLoading={loadingState === 'loading' || loadingState === 'loadingMore'} />
627
+ isLoading={isLoading} />
543
628
  <DismissButton onDismiss={onClose} />
544
629
  </div>
545
630
  </FocusScope>