@react-spectrum/autocomplete 3.0.0-alpha.2 → 3.0.0-alpha.20

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.
@@ -13,21 +13,28 @@
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, FocusScope, useFocusRing} from '@react-aria/focus';
24
23
  // @ts-ignore
25
24
  import intlMessages from '../intl/*.json';
26
25
  import {ListBoxBase, useListBoxLayout} from '@react-spectrum/listbox';
27
26
  import Magnifier from '@spectrum-icons/ui/Magnifier';
28
27
  import {mergeProps, useId} from '@react-aria/utils';
29
28
  import {ProgressCircle} from '@react-spectrum/progress';
30
- import React, {HTMLAttributes, ReactElement, ReactNode, RefObject, useCallback, useEffect, useRef, useState} from 'react';
29
+ import React, {
30
+ HTMLAttributes,
31
+ ReactElement,
32
+ ReactNode,
33
+ useCallback,
34
+ useEffect,
35
+ useRef,
36
+ useState
37
+ } from 'react';
31
38
  import searchAutocompleteStyles from './searchautocomplete.css';
32
39
  import searchStyles from '@adobe/spectrum-css-temp/components/search/vars.css';
33
40
  import {setInteractionModality, useHover} from '@react-aria/interactions';
@@ -38,14 +45,12 @@ import textfieldStyles from '@adobe/spectrum-css-temp/components/textfield/vars.
38
45
  import {Tray} from '@react-spectrum/overlays';
39
46
  import {useButton} from '@react-aria/button';
40
47
  import {useDialog} from '@react-aria/dialog';
41
- import {useFilter, useMessageFormatter} from '@react-aria/i18n';
42
- import {useFocusableRef} from '@react-spectrum/utils';
48
+ import {useFilter, useLocalizedStringFormatter} from '@react-aria/i18n';
43
49
  import {useLabel} from '@react-aria/label';
44
- import {useOverlayTrigger} from '@react-aria/overlays';
45
50
  import {useProviderProps} from '@react-spectrum/provider';
46
51
  import {useSearchAutocomplete} from '@react-aria/autocomplete';
47
52
 
48
- export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
53
+ function _MobileSearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
49
54
  props = useProviderProps(props);
50
55
 
51
56
  let {
@@ -70,8 +75,8 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
70
75
  selectedKey: undefined,
71
76
  defaultSelectedKey: undefined
72
77
  });
73
-
74
- let buttonRef = useRef<HTMLElement>();
78
+
79
+ let buttonRef = useRef<HTMLDivElement>(null);
75
80
  let domRef = useFocusableRef(ref, buttonRef);
76
81
  let {triggerProps, overlayProps} = useOverlayTrigger({type: 'listbox'}, state, buttonRef);
77
82
 
@@ -82,14 +87,12 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
82
87
 
83
88
  // Focus the button and show focus ring when clicking on the label
84
89
  labelProps.onClick = () => {
85
- if (!props.isDisabled) {
90
+ if (!props.isDisabled && buttonRef.current) {
86
91
  buttonRef.current.focus();
87
92
  setInteractionModality('keyboard');
88
93
  }
89
94
  };
90
95
 
91
- let onClose = () => state.commit();
92
-
93
96
  return (
94
97
  <>
95
98
  <Field
@@ -99,7 +102,7 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
99
102
  ref={domRef}
100
103
  includeNecessityIndicatorInAccessibilityName>
101
104
  <SearchAutocompleteButton
102
- {...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus})}
105
+ {...mergeProps(triggerProps, fieldProps, {autoFocus: props.autoFocus, icon: props.icon})}
103
106
  ref={buttonRef}
104
107
  isQuiet={isQuiet}
105
108
  isDisabled={isDisabled}
@@ -112,18 +115,22 @@ export const MobileSearchAutocomplete = React.forwardRef(function MobileSearchAu
112
115
  {state.inputValue || props.placeholder || ''}
113
116
  </SearchAutocompleteButton>
114
117
  </Field>
115
- <Tray isOpen={state.isOpen} onClose={onClose} isFixedHeight isNonModal {...overlayProps}>
118
+ <Tray state={state} isFixedHeight {...overlayProps}>
116
119
  <SearchAutocompleteTray
117
120
  {...props}
118
- onClose={onClose}
121
+ onClose={state.close}
119
122
  overlayProps={overlayProps}
120
123
  state={state} />
121
124
  </Tray>
122
125
  </>
123
126
  );
124
- });
127
+ }
128
+
129
+ export let MobileSearchAutocomplete = React.forwardRef(_MobileSearchAutocomplete) as <T>(props: SpectrumSearchAutocompleteProps<T> & {ref?: FocusableRef<HTMLElement>}) => ReactElement;
130
+
125
131
 
126
132
  interface SearchAutocompleteButtonProps extends AriaButtonProps {
133
+ icon?: ReactElement | null,
127
134
  isQuiet?: boolean,
128
135
  isDisabled?: boolean,
129
136
  isReadOnly?: boolean,
@@ -136,8 +143,15 @@ interface SearchAutocompleteButtonProps extends AriaButtonProps {
136
143
  className?: string
137
144
  }
138
145
 
139
- const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteButton(props: SearchAutocompleteButtonProps, ref: RefObject<HTMLElement>) {
146
+ // any type is because we don't want to call useObjectRef because this is an internal component and we know
147
+ // we are always passing an object ref
148
+ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteButton(props: SearchAutocompleteButtonProps, ref: any) {
149
+ let searchIcon = (
150
+ <Magnifier data-testid="searchicon" />
151
+ );
152
+
140
153
  let {
154
+ icon = searchIcon,
141
155
  isQuiet,
142
156
  isDisabled,
143
157
  isReadOnly,
@@ -149,33 +163,31 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
149
163
  style,
150
164
  className
151
165
  } = props;
152
- let formatMessage = useMessageFormatter(intlMessages);
166
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
153
167
  let valueId = useId();
154
168
  let invalidId = useId();
155
169
  let validationIcon = validationState === 'invalid'
156
- ? <AlertMedium id={invalidId} aria-label={formatMessage('invalid')} />
170
+ ? <AlertMedium id={invalidId} aria-label={stringFormatter.format('invalid')} />
157
171
  : <CheckmarkMedium />;
158
172
 
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
- });
173
+ if (icon) {
174
+ icon = React.cloneElement(icon, {
175
+ UNSAFE_className: classNames(
176
+ textfieldStyles,
177
+ 'spectrum-Textfield-icon'
178
+ ),
179
+ size: 'S'
180
+ });
181
+ }
170
182
 
171
183
  let clearButton = (
172
184
  <ClearButton
173
185
  onPress={(e) => {
174
- clearInput();
175
- props.onPress(e);
186
+ clearInput?.();
187
+ props?.onPress?.(e);
176
188
  }}
177
189
  preventFocus
178
- aria-label={formatMessage('clear')}
190
+ aria-label={stringFormatter.format('clear')}
179
191
  excludeFromTabOrder
180
192
  UNSAFE_className={
181
193
  classNames(
@@ -186,7 +198,6 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
186
198
  isDisabled={isDisabled} />
187
199
  );
188
200
 
189
-
190
201
  let validation = React.cloneElement(validationIcon, {
191
202
  UNSAFE_className: classNames(
192
203
  textfieldStyles,
@@ -194,11 +205,16 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
194
205
  classNames(
195
206
  styles,
196
207
  'spectrum-InputGroup-input-validationIcon'
208
+ ),
209
+ classNames(
210
+ searchStyles,
211
+ 'spectrum-Search-validationIcon'
197
212
  )
198
213
  )
199
214
  });
200
215
 
201
216
  let {hoverProps, isHovered} = useHover({});
217
+ let {isFocused, isFocusVisible, focusProps} = useFocusRing();
202
218
  let {buttonProps} = useButton({
203
219
  ...props,
204
220
  'aria-labelledby': [
@@ -211,102 +227,116 @@ const SearchAutocompleteButton = React.forwardRef(function SearchAutocompleteBut
211
227
  }, ref);
212
228
 
213
229
  return (
214
- <FocusRing
215
- focusClass={classNames(styles, 'is-focused')}
216
- focusRingClass={classNames(styles, 'focus-ring')}>
230
+ <div
231
+ {...mergeProps(hoverProps, focusProps, buttonProps)}
232
+ aria-haspopup="dialog"
233
+ ref={ref}
234
+ style={{...style, outline: 'none'}}
235
+ className={
236
+ classNames(
237
+ styles,
238
+ 'spectrum-InputGroup',
239
+ {
240
+ 'spectrum-InputGroup--quiet': isQuiet,
241
+ 'is-disabled': isDisabled,
242
+ 'spectrum-InputGroup--invalid': validationState === 'invalid' && !isDisabled,
243
+ 'is-hovered': isHovered,
244
+ 'is-focused': isFocused,
245
+ 'focus-ring': isFocusVisible
246
+ },
247
+ classNames(
248
+ searchAutocompleteStyles,
249
+ 'searchautocomplete',
250
+ 'mobile-searchautocomplete'
251
+ ),
252
+ className
253
+ )
254
+ }>
217
255
  <div
218
- {...mergeProps(hoverProps, buttonProps)}
219
- aria-haspopup="dialog"
220
- ref={ref as RefObject<HTMLDivElement>}
221
- style={{...style, outline: 'none'}}
222
256
  className={
223
257
  classNames(
224
- styles,
225
- 'spectrum-InputGroup',
258
+ textfieldStyles,
259
+ 'spectrum-Textfield',
226
260
  {
227
- 'spectrum-InputGroup--quiet': isQuiet,
228
- 'is-disabled': isDisabled,
229
- 'spectrum-InputGroup--invalid': validationState === 'invalid',
230
- 'is-hovered': isHovered
261
+ 'spectrum-Textfield--invalid': validationState === 'invalid' && !isDisabled,
262
+ 'spectrum-Textfield--valid': validationState === 'valid' && !isDisabled,
263
+ 'spectrum-Textfield--quiet': isQuiet
231
264
  },
232
265
  classNames(
233
- searchAutocompleteStyles,
234
- 'mobile-searchautocomplete'
266
+ searchStyles,
267
+ 'spectrum-Search',
268
+ 'spectrum-Search--loadable',
269
+ {
270
+ 'is-disabled': isDisabled,
271
+ 'is-quiet': isQuiet,
272
+ 'spectrum-Search--invalid': validationState === 'invalid' && !isDisabled,
273
+ 'spectrum-Search--valid': validationState === 'valid' && !isDisabled
274
+ }
235
275
  ),
236
- className
276
+ classNames(
277
+ styles,
278
+ 'spectrum-InputGroup-field'
279
+ )
237
280
  )
238
281
  }>
239
282
  <div
240
283
  className={
241
284
  classNames(
242
285
  textfieldStyles,
243
- 'spectrum-Textfield',
286
+ 'spectrum-Textfield-input',
244
287
  {
245
- 'spectrum-Textfield--invalid': validationState === 'invalid',
246
- 'spectrum-Textfield--valid': validationState === 'valid',
247
- 'spectrum-Textfield--quiet': isQuiet
288
+ 'spectrum-Textfield-inputIcon': !!icon,
289
+ 'is-hovered': isHovered,
290
+ 'is-placeholder': isPlaceholder,
291
+ 'is-disabled': isDisabled,
292
+ 'is-quiet': isQuiet,
293
+ 'is-focused': isFocused
248
294
  },
249
295
  classNames(
250
296
  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
- }
297
+ 'spectrum-Search-input'
298
+ ),
299
+ classNames(
300
+ searchAutocompleteStyles,
301
+ 'mobile-input'
258
302
  )
259
303
  )
260
304
  }>
261
- <div
305
+ {icon}
306
+ <span
307
+ id={valueId}
262
308
  className={
263
309
  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
- )
310
+ searchAutocompleteStyles,
311
+ 'mobile-value'
277
312
  )
278
313
  }>
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}
314
+ {children}
315
+ </span>
293
316
  </div>
317
+ {validationState && !isDisabled ? validation : null}
318
+ {(inputValue !== '' || validationState != null) && !isReadOnly && clearButton}
294
319
  </div>
295
- </FocusRing>
320
+ </div>
296
321
  );
297
322
  });
298
323
 
299
- interface SearchAutocompleteTrayProps extends SpectrumSearchAutocompleteProps<unknown> {
300
- state: ComboBoxState<unknown>,
324
+ interface SearchAutocompleteTrayProps<T> extends SpectrumSearchAutocompleteProps<T> {
325
+ state: ComboBoxState<T>,
301
326
  overlayProps: HTMLAttributes<HTMLElement>,
302
327
  loadingIndicator?: ReactElement,
303
328
  onClose: () => void
304
329
  }
305
330
 
306
- function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
331
+ function SearchAutocompleteTray<T>(props: SearchAutocompleteTrayProps<T>) {
332
+ let searchIcon = (
333
+ <Magnifier data-testid="searchicon" />
334
+ );
335
+
307
336
  let {
308
337
  // completionMode = 'suggest',
309
338
  state,
339
+ icon = searchIcon,
310
340
  isDisabled,
311
341
  validationState,
312
342
  label,
@@ -317,15 +347,16 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
317
347
  onSubmit
318
348
  } = props;
319
349
 
320
- let timeout = useRef(null);
350
+ let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
321
351
  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(
352
+ let inputRef = useRef<HTMLInputElement>(null);
353
+ let popoverRef = useRef<HTMLDivElement>(null);
354
+ let listBoxRef = useRef<HTMLDivElement>(null);
355
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
356
+ let layout = useListBoxLayout(state, isLoading);
357
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
358
+
359
+ let {inputProps, listBoxProps, labelProps, clearButtonProps} = useSearchAutocomplete<T>(
329
360
  {
330
361
  ...props,
331
362
  keyboardDelegate: layout,
@@ -337,7 +368,9 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
337
368
  );
338
369
 
339
370
  React.useEffect(() => {
340
- focusSafely(inputRef.current);
371
+ if (inputRef.current) {
372
+ focusSafely(inputRef.current);
373
+ }
341
374
 
342
375
  // When the tray unmounts, set state.isFocused (i.e. the tray input's focus tracker) to false.
343
376
  // This is to prevent state.isFocused from being set to true when the tray closes via tapping on the underlay
@@ -366,7 +399,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
366
399
  <ClearButton
367
400
  {...clearButtonProps}
368
401
  preventFocus
369
- aria-label={formatMessage('clear')}
402
+ aria-label={stringFormatter.format('clear')}
370
403
  excludeFromTabOrder
371
404
  UNSAFE_className={
372
405
  classNames(
@@ -379,7 +412,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
379
412
 
380
413
  let loadingCircle = (
381
414
  <ProgressCircle
382
- aria-label={formatMessage('loading')}
415
+ aria-label={stringFormatter.format('loading')}
383
416
  size="S"
384
417
  isIndeterminate
385
418
  UNSAFE_className={classNames(
@@ -409,7 +442,9 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
409
442
  return;
410
443
  }
411
444
 
412
- popoverRef.current.focus();
445
+ if (popoverRef.current) {
446
+ popoverRef.current.focus();
447
+ }
413
448
  }, [inputRef, popoverRef, isTouchDown]);
414
449
 
415
450
  let inputValue = inputProps.value;
@@ -432,8 +467,10 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
432
467
  } else if (loadingState !== 'filtering') {
433
468
  // If loading is no longer happening, clear any timers and hide the loading circle
434
469
  setShowLoading(false);
435
- clearTimeout(timeout.current);
436
- timeout.current = null;
470
+ if (timeout.current !== null) {
471
+ clearTimeout(timeout.current);
472
+ timeout.current = null;
473
+ }
437
474
  }
438
475
 
439
476
  lastInputValue.current = inputValue;
@@ -442,25 +479,29 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
442
479
  let onKeyDown = (e) => {
443
480
  // Close virtual keyboard, close tray, and fire onSubmit if user hits Enter w/o any focused options
444
481
  if (e.key === 'Enter' && state.selectionManager.focusedKey == null) {
445
- popoverRef.current.focus();
446
- onClose();
447
- onSubmit(inputValue.toString(), null);
482
+ popoverRef.current?.focus();
483
+ if (onClose) {
484
+ onClose();
485
+ }
486
+ if (onSubmit) {
487
+ onSubmit(inputValue == null ? null : inputValue.toString(), null);
488
+ }
448
489
  } else {
449
- inputProps.onKeyDown(e);
490
+ if (inputProps.onKeyDown) {
491
+ inputProps.onKeyDown(e);
492
+ }
450
493
  }
451
494
  };
452
495
 
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
- });
496
+ if (icon) {
497
+ icon = React.cloneElement(icon, {
498
+ UNSAFE_className: classNames(
499
+ textfieldStyles,
500
+ 'spectrum-Textfield-icon'
501
+ ),
502
+ size: 'S'
503
+ });
504
+ }
464
505
 
465
506
  return (
466
507
  <FocusScope restoreFocus contain>
@@ -481,9 +522,9 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
481
522
  inputRef={inputRef}
482
523
  isDisabled={isDisabled}
483
524
  isLoading={showLoading && loadingState === 'filtering'}
484
- loadingIndicator={loadingState != null && loadingCircle}
525
+ loadingIndicator={loadingState != null ? loadingCircle : undefined}
485
526
  validationState={validationState}
486
- wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly && clearButton}
527
+ wrapperChildren={((state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly) ? clearButton : undefined}
487
528
  icon={icon}
488
529
  UNSAFE_className={
489
530
  classNames(
@@ -492,8 +533,8 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
492
533
  'spectrum-Textfield',
493
534
  'spectrum-Search--loadable',
494
535
  {
495
- 'spectrum-Search--invalid': validationState === 'invalid',
496
- 'spectrum-Search--valid': validationState === 'valid'
536
+ 'spectrum-Search--invalid': validationState === 'invalid' && !isDisabled,
537
+ 'spectrum-Search--valid': validationState === 'valid' && !isDisabled
497
538
  },
498
539
  classNames(
499
540
  searchAutocompleteStyles,
@@ -527,7 +568,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
527
568
  shouldUseVirtualFocus
528
569
  renderEmptyState={() => loadingState !== 'loading' && (
529
570
  <span className={classNames(searchAutocompleteStyles, 'no-results')}>
530
- {formatMessage('noResults')}
571
+ {stringFormatter.format('noResults')}
531
572
  </span>
532
573
  )}
533
574
  UNSAFE_className={
@@ -539,7 +580,7 @@ function SearchAutocompleteTray(props: SearchAutocompleteTrayProps) {
539
580
  ref={listBoxRef}
540
581
  onScroll={onScroll}
541
582
  onLoadMore={onLoadMore}
542
- isLoading={loadingState === 'loading' || loadingState === 'loadingMore'} />
583
+ isLoading={isLoading} />
543
584
  <DismissButton onDismiss={onClose} />
544
585
  </div>
545
586
  </FocusScope>