@telus-uds/components-base 1.59.2 → 1.61.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 (90) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/component-docs.json +526 -76
  3. package/lib/Autocomplete/Autocomplete.js +483 -0
  4. package/lib/Autocomplete/Loading.js +51 -0
  5. package/lib/Autocomplete/Suggestions.js +85 -0
  6. package/lib/Autocomplete/constants.js +14 -0
  7. package/lib/Autocomplete/dictionary.js +19 -0
  8. package/lib/Autocomplete/index.js +13 -0
  9. package/lib/Button/ButtonLink.js +7 -3
  10. package/lib/ExpandCollapse/Panel.js +7 -0
  11. package/lib/IconButton/IconButton.js +8 -0
  12. package/lib/Link/ChevronLink.js +9 -2
  13. package/lib/Link/LinkBase.js +14 -0
  14. package/lib/Link/TextButton.js +12 -1
  15. package/lib/Listbox/GroupControl.js +121 -0
  16. package/lib/Listbox/Listbox.js +198 -0
  17. package/lib/Listbox/ListboxGroup.js +142 -0
  18. package/lib/Listbox/ListboxItem.js +97 -0
  19. package/lib/Listbox/ListboxOverlay.js +106 -0
  20. package/lib/Listbox/PressableItem.js +0 -2
  21. package/lib/Listbox/index.js +5 -24
  22. package/lib/Pagination/dictionary.js +3 -3
  23. package/lib/Progress/ProgressBarBackground.js +2 -2
  24. package/lib/SideNav/Item.js +15 -5
  25. package/lib/Tags/Tags.js +6 -1
  26. package/lib/TextInput/TextInputBase.js +2 -0
  27. package/lib/Tooltip/Tooltip.js +6 -1
  28. package/lib/Tooltip/Tooltip.native.js +6 -1
  29. package/lib/Tooltip/shared.js +5 -0
  30. package/lib/index.js +17 -13
  31. package/lib/utils/useOverlaidPosition.js +6 -4
  32. package/lib-module/Autocomplete/Autocomplete.js +448 -0
  33. package/lib-module/Autocomplete/Loading.js +36 -0
  34. package/lib-module/Autocomplete/Suggestions.js +66 -0
  35. package/lib-module/Autocomplete/constants.js +4 -0
  36. package/lib-module/Autocomplete/dictionary.js +12 -0
  37. package/lib-module/Autocomplete/index.js +2 -0
  38. package/lib-module/Button/ButtonLink.js +4 -1
  39. package/lib-module/ExpandCollapse/Panel.js +7 -0
  40. package/lib-module/IconButton/IconButton.js +8 -0
  41. package/lib-module/Link/ChevronLink.js +10 -3
  42. package/lib-module/Link/LinkBase.js +14 -0
  43. package/lib-module/Link/TextButton.js +11 -1
  44. package/lib-module/Listbox/GroupControl.js +102 -0
  45. package/lib-module/Listbox/Listbox.js +172 -0
  46. package/lib-module/Listbox/ListboxGroup.js +117 -0
  47. package/lib-module/Listbox/ListboxItem.js +71 -0
  48. package/lib-module/Listbox/ListboxOverlay.js +80 -0
  49. package/lib-module/Listbox/PressableItem.js +0 -2
  50. package/lib-module/Listbox/index.js +2 -2
  51. package/lib-module/Pagination/dictionary.js +3 -3
  52. package/lib-module/Progress/ProgressBarBackground.js +2 -2
  53. package/lib-module/SideNav/Item.js +15 -5
  54. package/lib-module/Tags/Tags.js +6 -1
  55. package/lib-module/TextInput/TextInputBase.js +2 -0
  56. package/lib-module/Tooltip/Tooltip.js +6 -1
  57. package/lib-module/Tooltip/Tooltip.native.js +6 -1
  58. package/lib-module/Tooltip/shared.js +5 -0
  59. package/lib-module/index.js +2 -1
  60. package/lib-module/utils/useOverlaidPosition.js +5 -4
  61. package/package.json +5 -3
  62. package/src/Autocomplete/Autocomplete.jsx +411 -0
  63. package/src/Autocomplete/Loading.jsx +18 -0
  64. package/src/Autocomplete/Suggestions.jsx +54 -0
  65. package/src/Autocomplete/constants.js +4 -0
  66. package/src/Autocomplete/dictionary.js +12 -0
  67. package/src/Autocomplete/index.js +3 -0
  68. package/src/Button/ButtonLink.jsx +4 -1
  69. package/src/ExpandCollapse/Panel.jsx +11 -1
  70. package/src/IconButton/IconButton.jsx +7 -0
  71. package/src/Link/ChevronLink.jsx +10 -3
  72. package/src/Link/LinkBase.jsx +11 -0
  73. package/src/Link/TextButton.jsx +8 -2
  74. package/src/Listbox/GroupControl.jsx +93 -0
  75. package/src/Listbox/Listbox.jsx +165 -0
  76. package/src/Listbox/ListboxGroup.jsx +120 -0
  77. package/src/Listbox/ListboxItem.jsx +76 -0
  78. package/src/Listbox/ListboxOverlay.jsx +82 -0
  79. package/src/Listbox/PressableItem.jsx +0 -2
  80. package/src/Listbox/index.js +3 -2
  81. package/src/Pagination/dictionary.js +3 -3
  82. package/src/Progress/ProgressBarBackground.jsx +2 -2
  83. package/src/SideNav/Item.jsx +13 -5
  84. package/src/Tags/Tags.jsx +5 -1
  85. package/src/TextInput/TextInputBase.jsx +2 -0
  86. package/src/Tooltip/Tooltip.jsx +16 -2
  87. package/src/Tooltip/Tooltip.native.jsx +15 -2
  88. package/src/Tooltip/shared.js +4 -0
  89. package/src/index.js +2 -1
  90. package/src/utils/useOverlaidPosition.js +6 -5
@@ -347,6 +347,8 @@ export default TextInputBase;
347
347
  const staticStyles = StyleSheet.create({
348
348
  buttonsContainer: {
349
349
  position: 'absolute',
350
+ flexDirection: 'row',
351
+ alignItems: 'center',
350
352
  right: 0,
351
353
  top: 0,
352
354
  bottom: 0,
@@ -125,6 +125,7 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref6, ref) => {
125
125
  copy = 'en',
126
126
  tokens,
127
127
  variant,
128
+ inline = false,
128
129
  ...rest
129
130
  } = _ref6;
130
131
  const [isOpen, setIsOpen] = useState(false);
@@ -219,7 +220,11 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref6, ref) => {
219
220
  right: 10
220
221
  } : undefined;
221
222
  return /*#__PURE__*/_jsxs(View, {
222
- style: staticStyles.container,
223
+ style: [staticStyles.container, Platform.select({
224
+ web: {
225
+ display: inline ? 'inline-block' : 'flex'
226
+ }
227
+ })],
223
228
  ...selectProps(rest),
224
229
  ref: ref,
225
230
  children: [/*#__PURE__*/_jsx(Pressable, {
@@ -152,6 +152,7 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref6, ref) => {
152
152
  copy = 'en',
153
153
  tokens,
154
154
  variant,
155
+ inline = false,
155
156
  ...rest
156
157
  } = _ref6;
157
158
  const [isOpen, setIsOpen] = useState(false);
@@ -269,7 +270,11 @@ const Tooltip = /*#__PURE__*/forwardRef((_ref6, ref) => {
269
270
  right: 10
270
271
  } : undefined;
271
272
  return /*#__PURE__*/_jsxs(View, {
272
- style: staticStyles.container,
273
+ style: [staticStyles.container, Platform.select({
274
+ web: {
275
+ display: inline ? 'inline-block' : 'flex'
276
+ }
277
+ })],
273
278
  ...selectProps(rest),
274
279
  children: [/*#__PURE__*/_jsx(Pressable, {
275
280
  onPress: toggleIsOpen,
@@ -21,6 +21,11 @@ const propTypes = {
21
21
  * Use to place the tooltip in a specific location (only if it fits within viewport).
22
22
  */
23
23
  position: PropTypes.oneOf(['auto', 'above', 'right', 'below', 'left']),
24
+
25
+ /**
26
+ * Display tooltip icon button as an inline element.
27
+ */
28
+ inline: PropTypes.bool,
24
29
  tokens: getTokensPropType('Tooltip'),
25
30
  variant: variantProp.propType
26
31
  };
@@ -1,10 +1,11 @@
1
1
  export { default as A11yText } from './A11yText';
2
2
  export { default as ActivityIndicator } from './ActivityIndicator';
3
+ export { default as Autocomplete } from './Autocomplete';
3
4
  export { default as Box } from './Box';
4
5
  export * from './Button';
5
6
  export { default as Card, PressableCardBase } from './Card';
6
7
  export * from './Carousel';
7
- export * from './Listbox';
8
+ export { default as Listbox } from './Listbox';
8
9
  export { default as Checkbox } from './Checkbox';
9
10
  export * from './Checkbox';
10
11
  export { default as Divider } from './Divider';
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import Dimensions from "react-native-web/dist/exports/Dimensions";
3
+ import Platform from "react-native-web/dist/exports/Platform";
3
4
 
4
5
  const adjustHorizontalToFit = (initialOffset, windowWidth, sourceWidth) => {
5
6
  const offset = Math.max(0, initialOffset);
@@ -172,8 +173,8 @@ const useOverlaidPosition = _ref3 => {
172
173
  let {
173
174
  window
174
175
  } = _ref5;
175
- (_sourceRef$current = sourceRef.current) === null || _sourceRef$current === void 0 ? void 0 : _sourceRef$current.measureInWindow((x, y, width, height) => {
176
- // Could add a debouncer here if there's too many rerenders during gradual resizes
176
+ const measurementFunction = Platform.OS === 'web' ? 'measureInWindow' : 'measure';
177
+ (_sourceRef$current = sourceRef.current) === null || _sourceRef$current === void 0 ? void 0 : _sourceRef$current[measurementFunction]((x, y, width, height) => {
177
178
  setWindowDimensions(window);
178
179
  setSourceLayout({
179
180
  x,
@@ -192,9 +193,9 @@ const useOverlaidPosition = _ref3 => {
192
193
  if (typeof ((_subscription = subscription) === null || _subscription === void 0 ? void 0 : _subscription.remove) === 'function') {
193
194
  // React Native >=0.65.0
194
195
  subscription.remove();
195
- } else if (typeof Dimensions.removeEventListener === 'function') {
196
+ } else if (typeof Dimensions.remove === 'function') {
196
197
  // React Native <0.65.0
197
- Dimensions.removeEventListener('change', handleDimensionsChange);
198
+ Dimensions.remove('change', handleDimensionsChange);
198
199
  }
199
200
 
200
201
  setSourceLayout(null);
package/package.json CHANGED
@@ -11,13 +11,15 @@
11
11
  "@floating-ui/react-native": "^0.8.1",
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@telus-uds/system-constants": "^1.3.0",
14
- "@telus-uds/system-theme-tokens": "^2.41.0",
14
+ "@telus-uds/system-theme-tokens": "^2.41.1",
15
15
  "airbnb-prop-types": "^2.16.0",
16
16
  "lodash.debounce": "^4.0.8",
17
17
  "lodash.merge": "^4.6.2",
18
+ "lodash.throttle": "^4.1.1",
18
19
  "prop-types": "^15.7.2",
19
20
  "react-native-picker-select": "^8.0.4",
20
- "semver": "7.5.2"
21
+ "semver": "7.5.2",
22
+ "string.prototype.matchall": "^4.0.9"
21
23
  },
22
24
  "description": "Base components",
23
25
  "devDependencies": {
@@ -72,5 +74,5 @@
72
74
  "standard-engine": {
73
75
  "skip": true
74
76
  },
75
- "version": "1.59.2"
77
+ "version": "1.61.0"
76
78
  }
@@ -0,0 +1,411 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import React, { forwardRef, useRef, useState } from 'react'
3
+ import PropTypes from 'prop-types'
4
+ import { Dimensions, Platform, View, StyleSheet } from 'react-native'
5
+ import throttle from 'lodash.throttle'
6
+ import matchAll from 'string.prototype.matchall'
7
+ import {
8
+ inputSupportsProps,
9
+ selectSystemProps,
10
+ textInputProps,
11
+ textInputHandlerProps,
12
+ useCopy,
13
+ htmlAttrs,
14
+ useOverlaidPosition,
15
+ useSafeLayoutEffect
16
+ } from '../utils'
17
+ import { useThemeTokens } from '../ThemeProvider'
18
+ import Listbox from '../Listbox'
19
+ import Typography from '../Typography'
20
+ import { TextInput } from '../TextInput'
21
+ import InputSupports from '../InputSupports'
22
+ import Loading from './Loading'
23
+ import Suggestions from './Suggestions'
24
+ import {
25
+ DEFAULT_MAX_SUGGESTIONS,
26
+ DEFAULT_MIN_TO_SUGGESTION,
27
+ INPUT_LEFT_PADDING,
28
+ MIN_LISTBOX_WIDTH
29
+ } from './constants'
30
+ import dictionary from './dictionary'
31
+
32
+ const staticStyles = StyleSheet.create({
33
+ container: {
34
+ zIndex: 100,
35
+ flexDirection: 'column',
36
+ justifyContent: 'flex-start',
37
+ flexGrow: 0,
38
+ flexShrink: 0
39
+ }
40
+ })
41
+
42
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
43
+ htmlAttrs,
44
+ inputSupportsProps,
45
+ textInputHandlerProps,
46
+ textInputProps
47
+ ])
48
+
49
+ // Returns JSX to display a bold string `str` with unbolded occurrences of the
50
+ // `substring` based in the array of `matchIndexes` provided
51
+ const highlightAllMatches = (str, substring = '', matchIndexes = [], resultsTextColor) => (
52
+ // Wrapping all in bold
53
+ <Typography variant={{ bold: false }} tokens={{ color: resultsTextColor }}>
54
+ {matchIndexes.reduce(
55
+ (acc, matchIndex, index) => [
56
+ ...acc,
57
+ // Add a piece of the string up to the first occurrence of the substring
58
+ index === 0 && (str.slice(0, matchIndex) ?? ''),
59
+ // Unbold the occurrence of the substring (while keeping the original casing)
60
+ <Typography key={matchIndex} variant={{ bold: true }} tokens={{ color: resultsTextColor }}>
61
+ {str.slice(matchIndex, matchIndex + substring.length)}
62
+ </Typography>,
63
+ // Add the rest of the string until the next occurrence or the end of it
64
+ str.slice(matchIndex + substring.length, matchIndexes[index + 1] ?? str.length)
65
+ ],
66
+ []
67
+ )}
68
+ </Typography>
69
+ )
70
+ const highlight = (items = [], text = '', color) =>
71
+ items.reduce((acc, item) => {
72
+ const matches = Array.from(
73
+ matchAll(
74
+ item.label.toLowerCase(),
75
+ text.toLowerCase().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
76
+ )
77
+ )?.map(({ index }) => index)
78
+
79
+ if (matches?.length) {
80
+ return [...acc, { ...item, label: highlightAllMatches(item.label, text, matches, color) }]
81
+ }
82
+
83
+ return [...acc, item]
84
+ }, [])
85
+
86
+ const Autocomplete = forwardRef(
87
+ (
88
+ {
89
+ children,
90
+ copy = 'en',
91
+ fullWidth = true,
92
+ initialItems,
93
+ initialValue,
94
+ isLoading = false,
95
+ items,
96
+ maxSuggestions = DEFAULT_MAX_SUGGESTIONS,
97
+ minToSuggestion = DEFAULT_MIN_TO_SUGGESTION,
98
+ noResults,
99
+ onChange,
100
+ onClear,
101
+ onSelect,
102
+ readOnly,
103
+ validation,
104
+ value,
105
+ helpText = '',
106
+ ...rest
107
+ },
108
+ ref
109
+ ) => {
110
+ const { color: resultsTextColor } = useThemeTokens('Search', {}, { focus: true })
111
+ // The wrapped input is mostly responsible for controlled vs uncontrolled handling,
112
+ // but we also need to adjust suggestions based on the mode:
113
+ // - in controlled mode we rely entirely on the suggestions passed via the `items` prop,
114
+ // - in uncontrolled mode we filter the suggestions ourselves based on the `initialItems`
115
+ // prop and the text entered
116
+ const isControlled = value !== undefined
117
+
118
+ // We need to store current items for uncontrolled usage
119
+ const [currentItems, setCurrentItems] = useState(initialItems)
120
+
121
+ // We need to store the current value as well to be able to highlight it
122
+ const [currentValue, setCurrentValue] = useState(value ?? initialValue)
123
+ const inputTokens = { paddingLeft: INPUT_LEFT_PADDING }
124
+
125
+ // Setting up the overlay
126
+ const openOverlayRef = useRef()
127
+ const [isExpanded, setIsExpanded] = useState((value ?? initialValue)?.length >= minToSuggestion)
128
+ const [isFocused, setisFocused] = useState(false)
129
+ const [sourceLayout, setSourceLayout] = useState(null)
130
+
131
+ const { supportsProps, ...selectedProps } = selectProps(rest)
132
+ const { hint, label: inputLabel } = supportsProps
133
+ const hintExpansionEnabled = isFocused && helpText && !currentValue
134
+ const {
135
+ overlaidPosition,
136
+ sourceRef: inputRef,
137
+ // targetRef,
138
+ onTargetLayout,
139
+ isReady
140
+ } = useOverlaidPosition({
141
+ isShown: isExpanded || hintExpansionEnabled,
142
+ offsets: {
143
+ vertical: Platform.OS !== 'web' && (hint || inputLabel) ? 28 : 4
144
+ }
145
+ })
146
+ const targetRef = useRef(null)
147
+ // We limit the number of suggestions displayed to avoid huge lists
148
+ // TODO: add a way to make the `Listbox` occupy fixed height and be scrollable
149
+ // within that height, which will unlock similar behaviour for `AutoComplete` as well
150
+ const itemsToSuggest = (data = []) =>
151
+ maxSuggestions ? data.slice(0, maxSuggestions) : [...data]
152
+
153
+ const getCopy = useCopy({ dictionary, copy })
154
+ // Tracking input width changes to resize the listbox overlay accordingly
155
+ const [inputWidth, setInputWidth] = useState()
156
+ useSafeLayoutEffect(() => {
157
+ if (Platform.OS === 'web') {
158
+ const updateInputWidth = () => {
159
+ setInputWidth(inputRef?.current?.clientWidth + 4) // adding back all the input borders / outlines
160
+ setIsExpanded(false) // close the suggestions while the input is changing
161
+ }
162
+
163
+ const throttledUpdateInputWidth = throttle(updateInputWidth, 100, { leading: false })
164
+
165
+ updateInputWidth()
166
+
167
+ Dimensions.addEventListener('change', throttledUpdateInputWidth)
168
+
169
+ return () => {
170
+ Dimensions.removeEventListener('change', throttledUpdateInputWidth)
171
+ }
172
+ }
173
+ setInputWidth(sourceLayout?.width)
174
+ return () => {}
175
+ }, [inputRef, sourceLayout])
176
+
177
+ const handleMeasure = (event) => {
178
+ onTargetLayout(event)
179
+ if (Platform.OS !== 'web') {
180
+ inputRef?.current?.measureInWindow((x, y, width) => {
181
+ setInputWidth(width)
182
+ })
183
+ }
184
+ }
185
+
186
+ const handleChange = (newValue) => {
187
+ onChange?.(newValue || '')
188
+ setCurrentValue(newValue)
189
+ setIsExpanded(newValue?.length >= minToSuggestion)
190
+ if (!isControlled && initialItems !== undefined) {
191
+ setCurrentItems(
192
+ initialItems.filter(({ label }) =>
193
+ label?.toLowerCase()?.includes(newValue?.toLowerCase())
194
+ )
195
+ )
196
+ }
197
+ }
198
+ const handleSelect = (selectedId) => {
199
+ onSelect?.(selectedId)
200
+ const { label: newValue, nested } = (isControlled ? items : currentItems)?.find(
201
+ ({ id }) => id === selectedId
202
+ )
203
+ if (!nested) {
204
+ onChange?.(newValue)
205
+ setIsExpanded(false)
206
+ }
207
+ setCurrentValue(newValue)
208
+ if (!isControlled && inputRef?.current) inputRef.current.value = newValue
209
+ }
210
+
211
+ const handleClose = (event) => {
212
+ if (event.type === 'keydown') {
213
+ if (event.key === 'Escape' || event.key === 27) {
214
+ setIsExpanded(false)
215
+ } else if (event.key === 'ArrowDown' && isExpanded && !isLoading && targetRef?.current) {
216
+ targetRef.current.focus()
217
+ }
218
+ } else if (
219
+ event.type === 'click' &&
220
+ openOverlayRef?.current &&
221
+ event.target &&
222
+ !openOverlayRef?.current?.contains(event.target)
223
+ ) {
224
+ setIsExpanded(false)
225
+ } else if (
226
+ event.type === 'touchstart' &&
227
+ openOverlayRef?.current &&
228
+ event.touches[0].target &&
229
+ !openOverlayRef?.current?.contains(event.touches[0].target)
230
+ ) {
231
+ setIsExpanded(false)
232
+ } else if (Platform.OS === 'web') {
233
+ // needed for dropdown to be collapsed when clicking outside on web
234
+ setIsExpanded(false)
235
+ }
236
+ }
237
+ const itemsToShow = currentValue
238
+ ? itemsToSuggest(
239
+ highlight(isControlled ? items : currentItems, currentValue, resultsTextColor)
240
+ )
241
+ : []
242
+ const helpTextToShow = isFocused && !currentValue ? helpText : noResults ?? getCopy('noResults')
243
+
244
+ return (
245
+ <View style={staticStyles.container}>
246
+ <InputSupports
247
+ {...supportsProps}
248
+ accessibilityAutoComplete="list"
249
+ accessibilityControls="autocomplete"
250
+ accessibilityExpanded={isExpanded}
251
+ accessibilityRole="combobox"
252
+ {...selectedProps}
253
+ validation={validation}
254
+ ref={ref}
255
+ >
256
+ {({ inputId, ...props }) => {
257
+ if (typeof children === 'function')
258
+ return children({
259
+ inputId,
260
+ inputRef,
261
+ onChange: handleChange,
262
+ onKeyPress: handleClose,
263
+ readOnly,
264
+ tokens: inputTokens,
265
+ ...selectedProps,
266
+ ...props,
267
+ ...(isControlled ? { value } : { initialValue })
268
+ })
269
+
270
+ return (
271
+ <TextInput
272
+ onChange={handleChange}
273
+ onFocus={() => {
274
+ setisFocused(true)
275
+ }}
276
+ onBlur={() => {
277
+ setisFocused(false)
278
+ }}
279
+ onClear={onClear}
280
+ onKeyPress={handleClose}
281
+ readOnly={readOnly}
282
+ ref={inputRef}
283
+ {...(Platform.OS !== 'web'
284
+ ? { onLayout: (event) => setSourceLayout(event.nativeEvent.layout) }
285
+ : {})}
286
+ tokens={inputTokens}
287
+ validation={validation}
288
+ {...selectedProps}
289
+ {...props}
290
+ {...(isControlled ? { value } : { initialValue })}
291
+ />
292
+ )
293
+ }}
294
+ </InputSupports>
295
+ {(isExpanded || hintExpansionEnabled) && (
296
+ <>
297
+ <Listbox.Overlay
298
+ overlaidPosition={overlaidPosition}
299
+ isReady={isReady}
300
+ minWidth={fullWidth ? inputWidth : MIN_LISTBOX_WIDTH}
301
+ maxWidth={inputWidth}
302
+ onLayout={handleMeasure}
303
+ ref={openOverlayRef}
304
+ >
305
+ {isLoading ? (
306
+ <Loading label={getCopy('loading')} />
307
+ ) : (
308
+ <Suggestions
309
+ hasResults={getCopy('hasResults')}
310
+ id="autocomplete"
311
+ items={itemsToShow}
312
+ noResults={helpTextToShow}
313
+ onClose={handleClose}
314
+ onSelect={handleSelect}
315
+ parentRef={inputRef}
316
+ ref={targetRef}
317
+ />
318
+ )}
319
+ </Listbox.Overlay>
320
+ {targetRef?.current && (
321
+ <View
322
+ // This catches and shifts focus to other interactive elements.
323
+ onFocus={() => targetRef?.current?.focus()}
324
+ tabIndex={0}
325
+ />
326
+ )}
327
+ </>
328
+ )}
329
+ </View>
330
+ )
331
+ }
332
+ )
333
+ Autocomplete.displayName = 'Autocomplete'
334
+
335
+ // If a language dictionary entry is provided, it must contain every key
336
+ const dictionaryContentShape = PropTypes.shape({
337
+ hasResults: PropTypes.string.isRequired,
338
+ loading: PropTypes.string.isRequired,
339
+ noResults: PropTypes.string.isRequired
340
+ })
341
+
342
+ Autocomplete.propTypes = {
343
+ ...selectedSystemPropTypes,
344
+ /**
345
+ * Can be used to provide a function that renders a custom input:
346
+ * <Autocomplete items={items} value={currentValue}>
347
+ * {({ inputId, inputRef, onChange, onKeyPress, readOnly, tokens, value }) => (
348
+ * <Search
349
+ * nativeID={inputId}
350
+ * ref={inputRef}
351
+ * onChange={onChange}
352
+ * onKeyPress={onKeyPress}
353
+ * readOnly={readOnly}
354
+ * tokens={tokens}
355
+ * value={value}
356
+ * />
357
+ * )}
358
+ * </Autocomplete>
359
+ */
360
+ children: PropTypes.func,
361
+ /**
362
+ * Copy language identifier
363
+ */
364
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
365
+ /**
366
+ * Set to true in order to display the loading indicator instead of results
367
+ */
368
+ isLoading: PropTypes.bool,
369
+ /**
370
+ * List of items to display as suggestions
371
+ */
372
+ items: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, label: PropTypes.string })),
373
+ /**
374
+ * Label to display alongside the spinner when in a loading state
375
+ */
376
+ loadingLabel: PropTypes.string,
377
+ /**
378
+ * Minimum number of characters typed for a list of suggestions to appear
379
+ */
380
+ minToSuggestion: PropTypes.number,
381
+ /**
382
+ * Maximum number of suggestions provided at the same time
383
+ */
384
+ maxSuggestions: PropTypes.number,
385
+ /**
386
+ * Text or JSX to render when no results are available
387
+ */
388
+ noResults: PropTypes.node,
389
+ /**
390
+ * Help text to display when the input is focused and empty
391
+ */
392
+ helpText: PropTypes.string,
393
+ /**
394
+ * Handler function to be called when the input value changes
395
+ */
396
+ onChange: PropTypes.func,
397
+ /**
398
+ * Handler function to be called when the clear button (appears if the handler is passed) is pressed
399
+ */
400
+ onClear: PropTypes.func,
401
+ /**
402
+ * Callback function to be called when an item is selected from the list
403
+ */
404
+ onSelect: PropTypes.func,
405
+ /**
406
+ * Input value for controlled usage
407
+ */
408
+ value: PropTypes.string
409
+ }
410
+
411
+ export default Autocomplete
@@ -0,0 +1,18 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import ActivityIndicator from '../ActivityIndicator'
4
+ import Typography from '../Typography'
5
+ import Box from '../Box'
6
+ import StackView from '../StackView'
7
+
8
+ const Loading = ({ label }) => (
9
+ <Box space={3}>
10
+ <StackView direction="row" space={2} tokens={{ alignItems: 'center' }}>
11
+ <ActivityIndicator variant={{ size: 'large' }} />
12
+ <Typography>{label}</Typography>
13
+ </StackView>
14
+ </Box>
15
+ )
16
+ Loading.propTypes = { label: PropTypes.string }
17
+
18
+ export default Loading
@@ -0,0 +1,54 @@
1
+ import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import A11yText from '../A11yText'
4
+ import Typography from '../Typography'
5
+ import Box from '../Box'
6
+ import Listbox from '../Listbox'
7
+
8
+ const Suggestions = forwardRef(
9
+ ({ hasResults, items = [], noResults, onClose, onSelect, parentRef }, ref) => {
10
+ const pressableItems = items.map(({ id, ...rest }) => ({
11
+ id,
12
+ onPress: () => onSelect(id),
13
+ ...rest
14
+ }))
15
+ if (items?.length)
16
+ return (
17
+ <>
18
+ <A11yText accessibilityLiveRegion="polite" text={hasResults} />
19
+ <Listbox
20
+ items={pressableItems}
21
+ firstItemRef={ref}
22
+ parentRef={parentRef}
23
+ onClose={onClose}
24
+ />
25
+ </>
26
+ )
27
+
28
+ return (
29
+ <Box space={3}>
30
+ {typeof noResults === 'string' ? (
31
+ <>
32
+ <Typography accessibilityLiveRegion="polite" variant={{ size: 'small' }}>
33
+ {noResults}
34
+ </Typography>
35
+ </>
36
+ ) : (
37
+ noResults
38
+ )}
39
+ </Box>
40
+ )
41
+ }
42
+ )
43
+ Suggestions.displayName = 'Suggestions'
44
+ Suggestions.propTypes = {
45
+ hasResults: PropTypes.string.isRequired,
46
+ items: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, label: PropTypes.node }))
47
+ .isRequired,
48
+ noResults: PropTypes.node.isRequired,
49
+ onClose: PropTypes.func.isRequired,
50
+ onSelect: PropTypes.func.isRequired,
51
+ parentRef: PropTypes.object.isRequired
52
+ }
53
+
54
+ export default Suggestions
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_MIN_TO_SUGGESTION = 1
2
+ export const DEFAULT_MAX_SUGGESTIONS = 5
3
+ export const INPUT_LEFT_PADDING = 16
4
+ export const MIN_LISTBOX_WIDTH = 288
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ hasResults: 'Some results are available',
4
+ loading: 'Searching...',
5
+ noResults: 'No results found'
6
+ },
7
+ fr: {
8
+ hasResults: 'Quelques suggestions sont disponible',
9
+ loading: 'Recherche...',
10
+ noResults: 'Aucun résultat trouvé'
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ import Autocomplete from './Autocomplete'
2
+
3
+ export default Autocomplete
@@ -1,4 +1,5 @@
1
1
  import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
2
3
  import ButtonBase from './ButtonBase'
3
4
  import buttonPropTypes, { textAndA11yText } from './propTypes'
4
5
  import { a11yProps, hrefAttrsProp, linkProps } from '../utils/props'
@@ -33,7 +34,9 @@ ButtonLink.propTypes = {
33
34
  ...a11yProps.types,
34
35
  ...buttonPropTypes,
35
36
  ...linkProps.types,
36
- children: textAndA11yText
37
+ children: textAndA11yText,
38
+ dataSet: PropTypes.object,
39
+ accessibilityRole: PropTypes.string
37
40
  }
38
41
 
39
42
  export default ButtonLink
@@ -157,7 +157,17 @@ const ExpandCollapsePanel = forwardRef(
157
157
  />
158
158
  )}
159
159
  <Animated.View ref={animatedRef} style={animatedStyles} {...focusabilityProps}>
160
- <View onLayout={onContainerLayout}>
160
+ <View
161
+ onLayout={onContainerLayout}
162
+ style={{
163
+ ...Platform.select({
164
+ default: {
165
+ flex: 1
166
+ },
167
+ web: {}
168
+ })
169
+ }}
170
+ >
161
171
  <View style={selectContainerStyles(themeTokens)}>{children}</View>
162
172
  </View>
163
173
  </Animated.View>