@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.
@@ -12,34 +12,46 @@
12
12
  import {AriaButtonProps} from '@react-types/button';
13
13
  import {classNames, useFocusableRef, useIsMobileDevice, useResizeObserver, useUnwrapDOMRef} from '@react-spectrum/utils';
14
14
  import {ClearButton} from '@react-spectrum/button';
15
- import {DismissButton, useOverlayPosition} from '@react-aria/overlays';
16
15
  import {DOMRefValue, FocusableRef} from '@react-types/shared';
17
16
  import {Field} from '@react-spectrum/label';
17
+ import {filterDOMProps, useLayoutEffect} from '@react-aria/utils';
18
18
  import {FocusRing} from '@react-aria/focus';
19
19
  // @ts-ignore
20
20
  import intlMessages from '../intl/*.json';
21
21
  import {ListBoxBase, useListBoxLayout} from '@react-spectrum/listbox';
22
22
  import Magnifier from '@spectrum-icons/ui/Magnifier';
23
23
  import {MobileSearchAutocomplete} from './MobileSearchAutocomplete';
24
- import {Placement} from '@react-types/overlays';
25
24
  import {Popover} from '@react-spectrum/overlays';
26
25
  import {ProgressCircle} from '@react-spectrum/progress';
27
- import React, {forwardRef, InputHTMLAttributes, RefObject, useCallback, useEffect, useRef, useState} from 'react';
26
+ import React, {
27
+ forwardRef,
28
+ InputHTMLAttributes,
29
+ ReactElement,
30
+ RefObject,
31
+ useCallback,
32
+ useEffect,
33
+ useRef,
34
+ useState
35
+ } from 'react';
36
+ import searchAutocompleteStyles from './searchautocomplete.css';
28
37
  import searchStyles from '@adobe/spectrum-css-temp/components/search/vars.css';
29
38
  import {SpectrumSearchAutocompleteProps} from '@react-types/autocomplete';
30
39
  import styles from '@adobe/spectrum-css-temp/components/inputgroup/vars.css';
31
40
  import {TextFieldBase} from '@react-spectrum/textfield';
32
41
  import textfieldStyles from '@adobe/spectrum-css-temp/components/textfield/vars.css';
33
42
  import {useComboBoxState} from '@react-stately/combobox';
34
- import {useFilter, useMessageFormatter} from '@react-aria/i18n';
43
+ import {useFilter, useLocalizedStringFormatter} from '@react-aria/i18n';
35
44
  import {useHover} from '@react-aria/interactions';
36
- import {useLayoutEffect} from '@react-aria/utils';
37
45
  import {useProvider, useProviderProps} from '@react-spectrum/provider';
38
46
  import {useSearchAutocomplete} from '@react-aria/autocomplete';
39
47
 
40
48
  function SearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
41
49
  props = useProviderProps(props);
42
50
 
51
+ if (props.placeholder) {
52
+ console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead.');
53
+ }
54
+
43
55
  let isMobile = useIsMobileDevice();
44
56
  if (isMobile) {
45
57
  // menuTrigger=focus/manual don't apply to mobile searchwithin
@@ -49,7 +61,7 @@ function SearchAutocomplete<T extends object>(props: SpectrumSearchAutocompleteP
49
61
  }
50
62
  }
51
63
 
52
- const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
64
+ function _SearchAutocompleteBase<T extends object>(props: SpectrumSearchAutocompleteProps<T>, ref: FocusableRef<HTMLElement>) {
53
65
  props = useProviderProps(props);
54
66
 
55
67
  let {
@@ -62,12 +74,12 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
62
74
  onSubmit = () => {}
63
75
  } = props;
64
76
 
65
- let formatMessage = useMessageFormatter(intlMessages);
77
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
66
78
  let isAsync = loadingState != null;
67
- let popoverRef = useRef<DOMRefValue<HTMLDivElement>>();
79
+ let popoverRef = useRef<DOMRefValue<HTMLDivElement>>(null);
68
80
  let unwrappedPopoverRef = useUnwrapDOMRef(popoverRef);
69
- let listBoxRef = useRef();
70
- let inputRef = useRef<HTMLInputElement>();
81
+ let listBoxRef = useRef(null);
82
+ let inputRef = useRef<HTMLInputElement>(null);
71
83
  let domRef = useFocusableRef(ref, inputRef);
72
84
 
73
85
  let {contains} = useFilter({sensitivity: 'base'});
@@ -82,9 +94,9 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
82
94
  defaultSelectedKey: undefined
83
95
  }
84
96
  );
85
- let layout = useListBoxLayout(state);
86
-
87
- let {inputProps, listBoxProps, labelProps, clearButtonProps} = useSearchAutocomplete(
97
+ let layout = useListBoxLayout(state, loadingState === 'loadingMore');
98
+
99
+ let {inputProps, listBoxProps, labelProps, clearButtonProps, descriptionProps, errorMessageProps} = useSearchAutocomplete(
88
100
  {
89
101
  ...props,
90
102
  keyboardDelegate: layout,
@@ -96,18 +108,8 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
96
108
  state
97
109
  );
98
110
 
99
- let {overlayProps, placement, updatePosition} = useOverlayPosition({
100
- targetRef: inputRef,
101
- overlayRef: unwrappedPopoverRef,
102
- scrollRef: listBoxRef,
103
- placement: `${direction} end` as Placement,
104
- shouldFlip: shouldFlip,
105
- isOpen: state.isOpen,
106
- onClose: state.close
107
- });
108
-
109
111
  // Measure the width of the inputfield to inform the width of the menu (below).
110
- let [menuWidth, setMenuWidth] = useState(null);
112
+ let [menuWidth, setMenuWidth] = useState<number>(0);
111
113
  let {scale} = useProvider();
112
114
 
113
115
  let onResize = useCallback(() => {
@@ -124,26 +126,19 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
124
126
 
125
127
  useLayoutEffect(onResize, [scale, onResize]);
126
128
 
127
- // Update position once the ListBox has rendered. This ensures that
128
- // it flips properly when it doesn't fit in the available space.
129
- // TODO: add ResizeObserver to useOverlayPosition so we don't need this.
130
- useLayoutEffect(() => {
131
- if (state.isOpen) {
132
- requestAnimationFrame(() => {
133
- updatePosition();
134
- });
135
- }
136
- }, [state.isOpen, updatePosition]);
137
-
138
129
  let style = {
139
- ...overlayProps.style,
140
- width: isQuiet ? null : menuWidth,
130
+ width: isQuiet ? undefined : menuWidth,
141
131
  minWidth: isQuiet ? `calc(${menuWidth}px + calc(2 * var(--spectrum-dropdown-quiet-offset)))` : menuWidth
142
132
  };
143
133
 
144
134
  return (
145
135
  <>
146
- <Field {...props} labelProps={labelProps} ref={domRef}>
136
+ <Field
137
+ {...props}
138
+ descriptionProps={descriptionProps}
139
+ errorMessageProps={errorMessageProps}
140
+ labelProps={labelProps}
141
+ ref={domRef}>
147
142
  <SearchAutocompleteInput
148
143
  {...props}
149
144
  isOpen={state.isOpen}
@@ -153,14 +148,15 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
153
148
  clearButtonProps={clearButtonProps} />
154
149
  </Field>
155
150
  <Popover
156
- isOpen={state.isOpen}
151
+ state={state}
157
152
  UNSAFE_style={style}
158
153
  UNSAFE_className={classNames(styles, 'spectrum-InputGroup-popover', {'spectrum-InputGroup-popover--quiet': isQuiet})}
159
154
  ref={popoverRef}
160
- placement={placement}
155
+ triggerRef={inputRef}
156
+ placement={`${direction} end`}
161
157
  hideArrow
162
158
  isNonModal
163
- isDismissable={false}>
159
+ shouldFlip={shouldFlip}>
164
160
  <ListBoxBase
165
161
  {...listBoxProps}
166
162
  ref={listBoxRef}
@@ -171,30 +167,39 @@ const SearchAutocompleteBase = React.forwardRef(function SearchAutocompleteBase<
171
167
  layout={layout}
172
168
  state={state}
173
169
  shouldUseVirtualFocus
174
- isLoading={loadingState === 'loadingMore'}
170
+ isLoading={loadingState === 'loading' || loadingState === 'loadingMore'}
175
171
  onLoadMore={onLoadMore}
176
172
  renderEmptyState={() => isAsync && (
177
- <span>
178
- {formatMessage('noResults')}
173
+ <span className={classNames(searchAutocompleteStyles, 'no-results')}>
174
+ {stringFormatter.format('noResults')}
179
175
  </span>
180
176
  )} />
181
- <DismissButton onDismiss={() => state.close()} />
182
177
  </Popover>
183
178
  </>
184
179
  );
185
- });
180
+ }
186
181
 
187
- interface SearchAutocompleteInputProps extends SpectrumSearchAutocompleteProps<unknown> {
182
+ let SearchAutocompleteBase = React.forwardRef(_SearchAutocompleteBase) as <T>(props: SpectrumSearchAutocompleteProps<T> & {ref?: FocusableRef<HTMLElement>}) => ReactElement;
183
+
184
+
185
+ interface SearchAutocompleteInputProps<T> extends SpectrumSearchAutocompleteProps<T> {
188
186
  inputProps: InputHTMLAttributes<HTMLInputElement>,
189
- inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement>,
187
+ inputRef: RefObject<HTMLInputElement>,
190
188
  style?: React.CSSProperties,
191
189
  className?: string,
192
190
  isOpen?: boolean,
193
191
  clearButtonProps: AriaButtonProps
194
192
  }
195
193
 
196
- const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInput(props: SearchAutocompleteInputProps, ref: RefObject<HTMLElement>) {
194
+ // any type is because we don't want to call useObjectRef because this is an internal component and we know
195
+ // we are always passing an object ref
196
+ function _SearchAutocompleteInput<T>(props: SearchAutocompleteInputProps<T>, ref: any) {
197
+ let searchIcon = (
198
+ <Magnifier data-testid="searchicon" />
199
+ );
200
+
197
201
  let {
202
+ icon = searchIcon,
198
203
  isQuiet,
199
204
  isDisabled,
200
205
  isReadOnly,
@@ -210,13 +215,14 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
210
215
  clearButtonProps
211
216
  } = props;
212
217
  let {hoverProps, isHovered} = useHover({});
213
- let formatMessage = useMessageFormatter(intlMessages);
214
- let timeout = useRef(null);
218
+ let stringFormatter = useLocalizedStringFormatter(intlMessages);
219
+ let domProps = filterDOMProps(props);
220
+ let timeout = useRef<ReturnType<typeof setTimeout> | null>(null);
215
221
  let [showLoading, setShowLoading] = useState(false);
216
222
 
217
223
  let loadingCircle = (
218
224
  <ProgressCircle
219
- aria-label={formatMessage('loading')}
225
+ aria-label={stringFormatter.format('loading')}
220
226
  size="S"
221
227
  isIndeterminate
222
228
  UNSAFE_className={classNames(
@@ -225,14 +231,14 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
225
231
  classNames(
226
232
  styles,
227
233
  'spectrum-InputGroup-input-circleLoader'
234
+ ),
235
+ classNames(
236
+ searchStyles,
237
+ 'spectrum-Search-circleLoader'
228
238
  )
229
239
  )} />
230
240
  );
231
241
 
232
- let searchIcon = (
233
- <Magnifier data-testid="searchicon" />
234
- );
235
-
236
242
  let clearButton = (
237
243
  <ClearButton
238
244
  {...clearButtonProps}
@@ -267,8 +273,10 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
267
273
  } else if (!isLoading) {
268
274
  // If loading is no longer happening, clear any timers and hide the loading circle
269
275
  setShowLoading(false);
270
- clearTimeout(timeout.current);
271
- timeout.current = null;
276
+ if (timeout.current != null) {
277
+ clearTimeout(timeout.current);
278
+ timeout.current = null;
279
+ }
272
280
  }
273
281
 
274
282
  lastInputValue.current = inputValue;
@@ -292,44 +300,63 @@ const SearchAutocompleteInput = React.forwardRef(function SearchAutocompleteInpu
292
300
  {
293
301
  'spectrum-InputGroup--quiet': isQuiet,
294
302
  'is-disabled': isDisabled,
295
- 'spectrum-InputGroup--invalid': validationState === 'invalid',
303
+ 'spectrum-InputGroup--invalid': validationState === 'invalid' && !isDisabled,
296
304
  'is-hovered': isHovered
297
305
  },
306
+ classNames(
307
+ searchAutocompleteStyles,
308
+ 'searchautocomplete'
309
+ ),
298
310
  className
299
311
  )
300
312
  }>
301
313
  <TextFieldBase
314
+ {...domProps}
302
315
  inputProps={inputProps}
303
316
  inputRef={inputRef}
304
317
  UNSAFE_className={
305
318
  classNames(
306
319
  searchStyles,
307
320
  'spectrum-Search',
321
+ 'spectrum-Search--loadable',
308
322
  'spectrum-Textfield',
309
323
  {
310
324
  'is-disabled': isDisabled,
311
325
  'is-quiet': isQuiet,
312
- 'spectrum-Search--invalid': validationState === 'invalid',
313
- 'spectrum-Search--valid': validationState === 'valid'
314
- }
326
+ 'spectrum-Search--invalid': validationState === 'invalid' && !isDisabled,
327
+ 'spectrum-Search--valid': validationState === 'valid' && !isDisabled
328
+ },
329
+ classNames(
330
+ styles,
331
+ 'spectrum-InputGroup-field'
332
+ )
315
333
  )
316
334
  }
317
335
  inputClassName={classNames(searchStyles, 'spectrum-Search-input')}
336
+ validationIconClassName={
337
+ classNames(
338
+ searchStyles,
339
+ 'spectrum-Search-validationIcon'
340
+ )
341
+ }
318
342
  isDisabled={isDisabled}
319
343
  isQuiet={isQuiet}
320
344
  validationState={validationState}
321
345
  isLoading={showLoading && (isOpen || menuTrigger === 'manual' || loadingState === 'loading')}
322
- loadingIndicator={loadingState != null && loadingCircle}
323
- icon={searchIcon}
324
- wrapperChildren={(inputValue !== '' && !isReadOnly) && clearButton} />
346
+ loadingIndicator={loadingState != null ? loadingCircle : undefined}
347
+ icon={icon}
348
+ wrapperChildren={(inputValue !== '' || loadingState === 'filtering' || validationState != null) && !isReadOnly ? clearButton : undefined}
349
+ disableFocusRing />
325
350
  </div>
326
351
  </FocusRing>
327
352
  );
328
- });
353
+ }
354
+
355
+ let SearchAutocompleteInput = React.forwardRef(_SearchAutocompleteInput) as <T>(props: SearchAutocompleteInputProps<T> & {ref?: any}) => ReactElement;
329
356
 
330
357
 
331
358
  /**
332
359
  * A SearchAutocomplete is a searchfield that supports a dynamic list of suggestions.
333
360
  */
334
- let _SearchAutocomplete = forwardRef(SearchAutocomplete);
361
+ let _SearchAutocomplete = forwardRef(SearchAutocomplete) as <T>(props: SpectrumSearchAutocompleteProps<T> & {ref?: FocusableRef<HTMLElement>}) => ReactElement;
335
362
  export {_SearchAutocomplete as SearchAutocomplete};
package/src/index.ts CHANGED
@@ -9,8 +9,7 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
-
13
12
  /// <reference types="css-module-types" />
14
-
15
- export * from './SearchAutocomplete';
13
+ export {SearchAutocomplete} from './SearchAutocomplete';
16
14
  export {Item, Section} from '@react-stately/collections';
15
+ export type {SpectrumSearchAutocompleteProps} from '@react-types/autocomplete';
@@ -23,18 +23,25 @@
23
23
  font-style: italic;
24
24
  }
25
25
 
26
+ /* override .spectrum-InputGroup */
27
+ .searchautocomplete.searchautocomplete {
28
+ border-radius: var(--spectrum-search-border-radius);
29
+ --spectrum-focus-ring-border-radius: var(--spectrum-search-border-radius);
30
+ }
31
+
26
32
  .mobile-searchautocomplete {
27
33
  outline: none;
28
34
  }
29
35
 
30
36
  .mobile-input {
31
- white-space: nowrap;
32
- overflow: hidden;
33
- text-overflow: ellipsis;
37
+ display: flex;
38
+ align-items: center;
34
39
  }
35
40
 
36
41
  .mobile-value {
37
- vertical-align: middle;
42
+ white-space: nowrap;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
38
45
  }
39
46
 
40
47
  .tray-dialog {