@react-spectrum/combobox 3.13.3 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ComboBox.tsx CHANGED
@@ -33,6 +33,7 @@ import {Popover} from '@react-spectrum/overlays';
33
33
  import {PressResponder, useHover} from '@react-aria/interactions';
34
34
  import {ProgressCircle} from '@react-spectrum/progress';
35
35
  import React, {
36
+ ForwardedRef,
36
37
  InputHTMLAttributes,
37
38
  ReactElement,
38
39
  RefObject,
@@ -69,7 +70,7 @@ function ComboBox<T extends object>(props: SpectrumComboBoxProps<T>, ref: Focusa
69
70
  }
70
71
  }
71
72
 
72
- const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(props: SpectrumComboBoxProps<T>, ref: FocusableRef<HTMLElement>) {
73
+ const ComboBoxBase = React.forwardRef(function ComboBoxBase(props: SpectrumComboBoxProps<any>, ref: FocusableRef<HTMLElement>) {
73
74
  let {
74
75
  menuTrigger = 'input',
75
76
  shouldFlip = true,
@@ -89,14 +90,14 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(pr
89
90
 
90
91
  let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/combobox');
91
92
  let isAsync = loadingState != null;
92
- let popoverRef = useRef<DOMRefValue<HTMLDivElement>>(undefined);
93
+ let popoverRef = useRef<DOMRefValue<HTMLDivElement>>(null);
93
94
  let unwrappedPopoverRef = useUnwrapDOMRef(popoverRef);
94
- let buttonRef = useRef<FocusableRefValue<HTMLElement>>(undefined);
95
+ let buttonRef = useRef<FocusableRefValue<HTMLElement>>(null);
95
96
  let unwrappedButtonRef = useUnwrapDOMRef(buttonRef);
96
- let listBoxRef = useRef(undefined);
97
- let inputRef = useRef<HTMLInputElement>(undefined);
97
+ let listBoxRef = useRef(null);
98
+ let inputRef = useRef<HTMLInputElement>(null);
98
99
  // serve as the new popover `triggerRef` instead of `unwrappedButtonRef` before for better positioning.
99
- let inputGroupRef = useRef<HTMLDivElement>(undefined);
100
+ let inputGroupRef = useRef<HTMLDivElement>(null);
100
101
  let domRef = useFocusableRef(ref, inputRef);
101
102
 
102
103
  let {contains} = useFilter({sensitivity: 'base'});
@@ -124,7 +125,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(pr
124
125
  );
125
126
 
126
127
  // Measure the width of the inputfield and the button to inform the width of the menu (below).
127
- let [menuWidth, setMenuWidth] = useState(null);
128
+ let [menuWidth, setMenuWidth] = useState<number | undefined>(undefined);
128
129
  let {scale} = useProvider();
129
130
 
130
131
  let onResize = useCallback(() => {
@@ -142,11 +143,12 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(pr
142
143
 
143
144
  useLayoutEffect(onResize, [scale, onResize]);
144
145
 
145
- let width = isQuiet ? null : menuWidth;
146
+ let width = isQuiet ? undefined : menuWidth;
146
147
  let style = {
147
148
  width: customMenuWidth ? dimensionValue(customMenuWidth) : width,
148
149
  minWidth: isQuiet ? `calc(${menuWidth}px + calc(2 * var(--spectrum-dropdown-quiet-offset)))` : menuWidth
149
150
  };
151
+ let cbInputProps = {...props, children: null};
150
152
 
151
153
  return (
152
154
  <>
@@ -160,14 +162,14 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(pr
160
162
  labelProps={labelProps}
161
163
  ref={domRef}>
162
164
  <ComboBoxInput
163
- {...props}
165
+ {...cbInputProps}
164
166
  isOpen={state.isOpen}
165
167
  loadingState={loadingState}
166
168
  inputProps={inputProps}
167
169
  inputRef={inputRef}
168
170
  triggerProps={buttonProps}
169
171
  triggerRef={buttonRef}
170
- validationState={props.validationState || (isInvalid ? 'invalid' : null)}
172
+ validationState={props.validationState || (isInvalid ? 'invalid' : undefined)}
171
173
  ref={inputGroupRef} />
172
174
  </Field>
173
175
  {name && formValue === 'key' && <input type="hidden" name={name} value={state.selectedKey ?? ''} />}
@@ -186,7 +188,7 @@ const ComboBoxBase = React.forwardRef(function ComboBoxBase<T extends object>(pr
186
188
  {...listBoxProps}
187
189
  ref={listBoxRef}
188
190
  disallowEmptySelection
189
- autoFocus={state.focusStrategy}
191
+ autoFocus={state.focusStrategy ?? undefined}
190
192
  shouldSelectOnPressUp
191
193
  focusOnPointerEnter
192
194
  layout={layout}
@@ -215,7 +217,7 @@ interface ComboBoxInputProps extends SpectrumComboBoxProps<unknown> {
215
217
  isOpen?: boolean
216
218
  }
217
219
 
218
- const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInputProps, ref: RefObject<HTMLElement | null>) {
220
+ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInputProps, ref: ForwardedRef<HTMLElement | null>) {
219
221
  let {
220
222
  isQuiet,
221
223
  isDisabled,
@@ -233,7 +235,7 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
233
235
  } = props;
234
236
  let {hoverProps, isHovered} = useHover({});
235
237
  let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/combobox');
236
- let timeout = useRef(null);
238
+ let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
237
239
  let [showLoading, setShowLoading] = useState(false);
238
240
 
239
241
  let loadingCircle = (
@@ -272,7 +274,9 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
272
274
  } else if (!isLoading) {
273
275
  // If loading is no longer happening, clear any timers and hide the loading circle
274
276
  setShowLoading(false);
275
- clearTimeout(timeout.current);
277
+ if (timeout.current) {
278
+ clearTimeout(timeout.current);
279
+ }
276
280
  timeout.current = null;
277
281
  }
278
282
 
@@ -281,7 +285,9 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
281
285
 
282
286
  useEffect(() => {
283
287
  return () => {
284
- clearTimeout(timeout.current);
288
+ if (timeout.current) {
289
+ clearTimeout(timeout.current);
290
+ }
285
291
  timeout.current = null;
286
292
  };
287
293
  }, []);
@@ -337,7 +343,7 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
337
343
  // loading circle should only be displayed if menu is open, if menuTrigger is "manual", or first time load (to stop circle from showing up when user selects an option)
338
344
  // TODO: add special case for completionMode: complete as well
339
345
  isLoading={showLoading && (isOpen || menuTrigger === 'manual' || loadingState === 'loading')}
340
- loadingIndicator={loadingState != null && loadingCircle}
346
+ loadingIndicator={loadingState != null ? loadingCircle : undefined}
341
347
  disableFocusRing />
342
348
  <PressResponder preventFocusOnPress isPressed={isOpen}>
343
349
  <FieldButton
@@ -21,15 +21,15 @@ import {ComboBoxState, useComboBoxState} from '@react-stately/combobox';
21
21
  import comboboxStyles from './combobox.css';
22
22
  import {DismissButton, useOverlayTrigger} from '@react-aria/overlays';
23
23
  import {Field} from '@react-spectrum/label';
24
- import {FocusableRef, FocusableRefValue, RefObject, ValidationState} from '@react-types/shared';
24
+ import {FocusableRef, FocusableRefValue, ValidationState} from '@react-types/shared';
25
25
  import {FocusRing, focusSafely, FocusScope} from '@react-aria/focus';
26
26
  // @ts-ignore
27
27
  import intlMessages from '../intl/*.json';
28
28
  import labelStyles from '@adobe/spectrum-css-temp/components/fieldlabel/vars.css';
29
29
  import {ListBoxBase, useListBoxLayout} from '@react-spectrum/listbox';
30
- import {mergeProps, useFormReset, useId} from '@react-aria/utils';
30
+ import {mergeProps, useFormReset, useId, useObjectRef} from '@react-aria/utils';
31
31
  import {ProgressCircle} from '@react-spectrum/progress';
32
- import React, {HTMLAttributes, InputHTMLAttributes, ReactElement, ReactNode, useCallback, useEffect, useRef, useState} from 'react';
32
+ import React, {ForwardedRef, HTMLAttributes, InputHTMLAttributes, ReactElement, ReactNode, useCallback, useEffect, useRef, useState} from 'react';
33
33
  import searchStyles from '@adobe/spectrum-css-temp/components/search/vars.css';
34
34
  import {setInteractionModality, useHover} from '@react-aria/interactions';
35
35
  import {SpectrumComboBoxProps} from '@react-types/combobox';
@@ -45,7 +45,7 @@ import {useFilter, useLocalizedStringFormatter} from '@react-aria/i18n';
45
45
  import {useFormValidation} from '@react-aria/form';
46
46
  import {useProviderProps} from '@react-spectrum/provider';
47
47
 
48
- export const MobileComboBox = React.forwardRef(function MobileComboBox<T extends object>(props: SpectrumComboBoxProps<T>, ref: FocusableRef<HTMLElement>) {
48
+ export const MobileComboBox = React.forwardRef(function MobileComboBox(props: SpectrumComboBoxProps<any>, ref: FocusableRef<HTMLElement>) {
49
49
  props = useProviderProps(props);
50
50
 
51
51
  let {
@@ -73,17 +73,17 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox<T extends
73
73
  shouldCloseOnBlur: false
74
74
  });
75
75
 
76
- let buttonRef = useRef<HTMLElement>(undefined);
76
+ let buttonRef = useRef<HTMLDivElement>(null);
77
77
  let domRef = useFocusableRef(ref, buttonRef);
78
78
  let {triggerProps, overlayProps} = useOverlayTrigger({type: 'listbox'}, state, buttonRef);
79
79
 
80
80
  let inputRef = useRef<HTMLInputElement>(null);
81
81
  useFormValidation({
82
82
  ...props,
83
- focus: () => buttonRef.current.focus()
83
+ focus: () => buttonRef.current?.focus()
84
84
  }, state, inputRef);
85
85
  let {isInvalid, validationErrors, validationDetails} = state.displayValidation;
86
- let validationState = props.validationState || (isInvalid ? 'invalid' : null);
86
+ let validationState = props.validationState || (isInvalid ? 'invalid' : undefined);
87
87
  let errorMessage = props.errorMessage ?? validationErrors.join(' ');
88
88
 
89
89
  let {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField({
@@ -96,7 +96,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox<T extends
96
96
  // Focus the button and show focus ring when clicking on the label
97
97
  labelProps.onClick = () => {
98
98
  if (!props.isDisabled) {
99
- buttonRef.current.focus();
99
+ buttonRef.current?.focus();
100
100
  setInteractionModality('keyboard');
101
101
  }
102
102
  };
@@ -104,7 +104,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox<T extends
104
104
  let inputProps: InputHTMLAttributes<HTMLInputElement> = {
105
105
  type: 'hidden',
106
106
  name,
107
- value: formValue === 'text' ? state.inputValue : state.selectedKey
107
+ value: formValue === 'text' ? state.inputValue : String(state.selectedKey)
108
108
  };
109
109
 
110
110
  if (validationBehavior === 'native') {
@@ -117,7 +117,7 @@ export const MobileComboBox = React.forwardRef(function MobileComboBox<T extends
117
117
  inputProps.onChange = () => {};
118
118
  }
119
119
 
120
- useFormReset(inputRef, inputProps.value, formValue === 'text' ? state.setInputValue : state.setSelectedKey);
120
+ useFormReset(inputRef, String(inputProps.value ?? ''), formValue === 'text' ? state.setInputValue : state.setSelectedKey);
121
121
 
122
122
  return (
123
123
  <>
@@ -166,7 +166,7 @@ interface ComboBoxButtonProps extends AriaButtonProps {
166
166
  className?: string
167
167
  }
168
168
 
169
- const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxButtonProps, ref: RefObject<HTMLElement | null>) {
169
+ export const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxButtonProps, ref: ForwardedRef<HTMLDivElement>) {
170
170
  let {
171
171
  isQuiet,
172
172
  isDisabled,
@@ -194,6 +194,7 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB
194
194
  )
195
195
  });
196
196
 
197
+ let objRef = useObjectRef(ref);
197
198
  let {hoverProps, isHovered} = useHover({});
198
199
  let {buttonProps, isPressed} = useButton({
199
200
  ...props,
@@ -204,7 +205,7 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB
204
205
  validationState === 'invalid' ? invalidId : null
205
206
  ].filter(Boolean).join(' '),
206
207
  elementType: 'div'
207
- }, ref);
208
+ }, objRef);
208
209
 
209
210
  return (
210
211
  (<FocusRing
@@ -213,7 +214,7 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB
213
214
  <div
214
215
  {...mergeProps(hoverProps, buttonProps)}
215
216
  aria-haspopup="dialog"
216
- ref={ref as RefObject<HTMLDivElement | null>}
217
+ ref={objRef}
217
218
  style={{...style, outline: 'none'}}
218
219
  className={
219
220
  classNames(
@@ -307,8 +308,8 @@ const ComboBoxButton = React.forwardRef(function ComboBoxButton(props: ComboBoxB
307
308
  );
308
309
  });
309
310
 
310
- interface ComboBoxTrayProps extends SpectrumComboBoxProps<unknown> {
311
- state: ComboBoxState<unknown>,
311
+ interface ComboBoxTrayProps extends SpectrumComboBoxProps<any> {
312
+ state: ComboBoxState<any>,
312
313
  overlayProps: HTMLAttributes<HTMLElement>,
313
314
  loadingIndicator?: ReactElement,
314
315
  onClose: () => void
@@ -327,12 +328,12 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
327
328
  onClose
328
329
  } = props;
329
330
 
330
- let timeout = useRef(null);
331
+ let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
331
332
  let [showLoading, setShowLoading] = useState(false);
332
- let inputRef = useRef<HTMLInputElement>(undefined);
333
- let buttonRef = useRef<FocusableRefValue<HTMLElement>>(undefined);
334
- let popoverRef = useRef<HTMLDivElement>(undefined);
335
- let listBoxRef = useRef<HTMLDivElement>(undefined);
333
+ let inputRef = useRef<HTMLInputElement>(null);
334
+ let buttonRef = useRef<FocusableRefValue<HTMLElement>>(null);
335
+ let popoverRef = useRef<HTMLDivElement>(null);
336
+ let listBoxRef = useRef<HTMLDivElement>(null);
336
337
  let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
337
338
  let layout = useListBoxLayout();
338
339
  let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/combobox');
@@ -353,7 +354,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
353
354
  );
354
355
 
355
356
  React.useEffect(() => {
356
- focusSafely(inputRef.current);
357
+ if (inputRef.current) {
358
+ focusSafely(inputRef.current);
359
+ }
357
360
  }, []);
358
361
 
359
362
  React.useEffect(() => {
@@ -388,7 +391,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
388
391
  excludeFromTabOrder
389
392
  onPress={() => {
390
393
  state.setInputValue('');
391
- inputRef.current.focus();
394
+ inputRef.current?.focus();
392
395
  }}
393
396
  UNSAFE_className={
394
397
  classNames(
@@ -431,7 +434,7 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
431
434
  return;
432
435
  }
433
436
 
434
- popoverRef.current.focus();
437
+ popoverRef.current?.focus();
435
438
  }, [inputRef, popoverRef, isTouchDown]);
436
439
 
437
440
  let inputValue = inputProps.value;
@@ -454,7 +457,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
454
457
  } else if (loadingState !== 'filtering') {
455
458
  // If loading is no longer happening, clear any timers and hide the loading circle
456
459
  setShowLoading(false);
457
- clearTimeout(timeout.current);
460
+ if (timeout.current) {
461
+ clearTimeout(timeout.current);
462
+ }
458
463
  timeout.current = null;
459
464
  }
460
465
 
@@ -464,9 +469,9 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
464
469
  let onKeyDown = (e) => {
465
470
  // Close virtual keyboard if user hits Enter w/o any focused options
466
471
  if (e.key === 'Enter' && state.selectionManager.focusedKey == null) {
467
- popoverRef.current.focus();
472
+ popoverRef.current?.focus();
468
473
  } else {
469
- inputProps.onKeyDown(e);
474
+ inputProps.onKeyDown?.(e);
470
475
  }
471
476
  };
472
477
 
@@ -489,11 +494,11 @@ function ComboBoxTray(props: ComboBoxTrayProps) {
489
494
  inputRef={inputRef}
490
495
  isDisabled={isDisabled}
491
496
  isLoading={showLoading && loadingState === 'filtering'}
492
- loadingIndicator={loadingState != null && loadingCircle}
497
+ loadingIndicator={loadingState != null ? loadingCircle : undefined}
493
498
  validationState={validationState}
494
499
  labelAlign="start"
495
500
  labelPosition="top"
496
- wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly && clearButton}
501
+ wrapperChildren={(state.inputValue !== '' || loadingState === 'filtering' || validationState != null) && !props.isReadOnly ? clearButton : undefined}
497
502
  UNSAFE_className={
498
503
  classNames(
499
504
  searchStyles,