@telus-uds/components-web 2.18.0 → 2.19.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 (51) hide show
  1. package/CHANGELOG.md +11 -2
  2. package/component-docs.json +381 -471
  3. package/lib/NavigationBar/NavigationSubMenu.js +2 -4
  4. package/lib/baseExports.js +12 -0
  5. package/lib/index.js +0 -18
  6. package/lib/utils/useOverlaidPosition.js +2 -2
  7. package/lib-module/NavigationBar/NavigationSubMenu.js +1 -2
  8. package/lib-module/baseExports.js +1 -1
  9. package/lib-module/index.js +0 -2
  10. package/lib-module/utils/useOverlaidPosition.js +2 -2
  11. package/package.json +2 -2
  12. package/src/NavigationBar/NavigationSubMenu.jsx +1 -2
  13. package/src/baseExports.js +2 -0
  14. package/src/index.js +0 -2
  15. package/src/utils/useOverlaidPosition.js +2 -2
  16. package/lib/Autocomplete/Autocomplete.js +0 -430
  17. package/lib/Autocomplete/Loading.js +0 -46
  18. package/lib/Autocomplete/Suggestions.js +0 -81
  19. package/lib/Autocomplete/constants.js +0 -19
  20. package/lib/Autocomplete/dictionary.js +0 -19
  21. package/lib/Autocomplete/index.js +0 -13
  22. package/lib/Listbox/GroupControl.js +0 -110
  23. package/lib/Listbox/Listbox.js +0 -185
  24. package/lib/Listbox/ListboxGroup.js +0 -145
  25. package/lib/Listbox/ListboxItem.js +0 -101
  26. package/lib/Listbox/ListboxOverlay.js +0 -91
  27. package/lib/Listbox/index.js +0 -13
  28. package/lib-module/Autocomplete/Autocomplete.js +0 -406
  29. package/lib-module/Autocomplete/Loading.js +0 -32
  30. package/lib-module/Autocomplete/Suggestions.js +0 -64
  31. package/lib-module/Autocomplete/constants.js +0 -5
  32. package/lib-module/Autocomplete/dictionary.js +0 -12
  33. package/lib-module/Autocomplete/index.js +0 -2
  34. package/lib-module/Listbox/GroupControl.js +0 -96
  35. package/lib-module/Listbox/Listbox.js +0 -164
  36. package/lib-module/Listbox/ListboxGroup.js +0 -122
  37. package/lib-module/Listbox/ListboxItem.js +0 -77
  38. package/lib-module/Listbox/ListboxOverlay.js +0 -69
  39. package/lib-module/Listbox/index.js +0 -2
  40. package/src/Autocomplete/Autocomplete.jsx +0 -375
  41. package/src/Autocomplete/Loading.jsx +0 -15
  42. package/src/Autocomplete/Suggestions.jsx +0 -52
  43. package/src/Autocomplete/constants.js +0 -6
  44. package/src/Autocomplete/dictionary.js +0 -12
  45. package/src/Autocomplete/index.js +0 -3
  46. package/src/Listbox/GroupControl.jsx +0 -82
  47. package/src/Listbox/Listbox.jsx +0 -169
  48. package/src/Listbox/ListboxGroup.jsx +0 -125
  49. package/src/Listbox/ListboxItem.jsx +0 -80
  50. package/src/Listbox/ListboxOverlay.jsx +0 -72
  51. package/src/Listbox/index.js +0 -3
@@ -1,375 +0,0 @@
1
- /* eslint-disable react/require-default-props */
2
- import React, { forwardRef, useRef, useState } from 'react'
3
- import PropTypes from 'prop-types'
4
- import { throttle } from 'lodash'
5
- import {
6
- InputSupports,
7
- inputSupportsProps,
8
- selectSystemProps,
9
- TextInput,
10
- textInputProps,
11
- textInputHandlerProps,
12
- Typography,
13
- useCopy,
14
- useSafeLayoutEffect,
15
- useThemeTokens
16
- } from '@telus-uds/components-base'
17
- import Listbox from '../Listbox'
18
- import { htmlAttrs, useOverlaidPosition } from '../utils'
19
- import Loading from './Loading'
20
- import Suggestions from './Suggestions'
21
- import {
22
- DEFAULT_MAX_SUGGESTIONS,
23
- DEFAULT_MIN_TO_SUGGESTION,
24
- INPUT_LEFT_PADDING,
25
- MIN_LISTBOX_WIDTH
26
- } from './constants'
27
- import dictionary from './dictionary'
28
-
29
- const [selectProps, selectedSystemPropTypes] = selectSystemProps([
30
- htmlAttrs,
31
- inputSupportsProps,
32
- textInputHandlerProps,
33
- textInputProps
34
- ])
35
-
36
- // Returns JSX to display a bold string `str` with unbolded occurrences of the
37
- // `substring` based in the array of `matchIndexes` provided
38
- const highlightAllMatches = (str, substring = '', matchIndexes = [], resultsTextColor) => (
39
- // Wrapping all in bold
40
- <Typography variant={{ bold: false }} tokens={{ color: resultsTextColor }}>
41
- {matchIndexes.reduce(
42
- (acc, matchIndex, index) => [
43
- ...acc,
44
- // Add a piece of the string up to the first occurrence of the substring
45
- index === 0 && (str.slice(0, matchIndex) ?? ''),
46
- // Unbold the occurrence of the substring (while keeping the original casing)
47
- <Typography key={matchIndex} variant={{ bold: true }} tokens={{ color: resultsTextColor }}>
48
- {str.slice(matchIndex, matchIndex + substring.length)}
49
- </Typography>,
50
- // Add the rest of the string until the next occurrence or the end of it
51
- str.slice(matchIndex + substring.length, matchIndexes[index + 1] ?? str.length)
52
- ],
53
- []
54
- )}
55
- </Typography>
56
- )
57
- const highlight = (items = [], text = '', color) =>
58
- items.reduce((acc, item) => {
59
- const matches = Array.from(item.label.toLowerCase().matchAll(text.toLowerCase()))?.map(
60
- ({ index }) => index
61
- )
62
-
63
- if (matches?.length) {
64
- return [...acc, { ...item, label: highlightAllMatches(item.label, text, matches, color) }]
65
- }
66
-
67
- return [...acc, item]
68
- }, [])
69
-
70
- const Autocomplete = forwardRef(
71
- (
72
- {
73
- children,
74
- copy = 'en',
75
- fullWidth = true,
76
- initialItems,
77
- initialValue,
78
- isLoading = false,
79
- items,
80
- maxSuggestions = DEFAULT_MAX_SUGGESTIONS,
81
- minToSuggestion = DEFAULT_MIN_TO_SUGGESTION,
82
- noResults,
83
- onChange,
84
- onClear,
85
- onSelect,
86
- readOnly,
87
- validation,
88
- value,
89
- ...rest
90
- },
91
- ref
92
- ) => {
93
- const { color: resultsTextColor } = useThemeTokens('Search', {}, { focus: true })
94
- // The wrapped input is mostly responsible for controlled vs uncontrolled handling,
95
- // but we also need to adjust suggestions based on the mode:
96
- // - in controlled mode we rely entirely on the suggestions passed via the `items` prop,
97
- // - in uncontrolled mode we filter the suggestions ourselves based on the `initialItems`
98
- // prop and the text entered
99
- const isControlled = value !== undefined
100
-
101
- // We need to store current items for uncontrolled usage
102
- const [currentItems, setCurrentItems] = useState(initialItems)
103
-
104
- // We need to store the current value as well to be able to highlight it
105
- const [currentValue, setCurrentValue] = useState(value ?? initialValue)
106
-
107
- const inputTokens = { paddingLeft: INPUT_LEFT_PADDING }
108
-
109
- // Setting up the overlay
110
- const openOverlayRef = useRef()
111
- const [isExpanded, setIsExpanded] = useState((value ?? initialValue)?.length >= minToSuggestion)
112
- const {
113
- overlaidPosition,
114
- sourceRef: inputRef,
115
- targetRef,
116
- onTargetLayout,
117
- isReady
118
- } = useOverlaidPosition({
119
- isShown: isExpanded,
120
- offsets: { vertical: 4 }
121
- })
122
-
123
- // We limit the number of suggestions displayed to avoid huge lists
124
- // TODO: add a way to make the `Listbox` occupy fixed height and be scrollable
125
- // within that height, which will unlock similar behaviour for `AutoComplete` as well
126
- const itemsToSuggest = (data = []) =>
127
- maxSuggestions ? data.slice(0, maxSuggestions) : [...data]
128
-
129
- const getCopy = useCopy({ dictionary, copy })
130
-
131
- // Tracking input width changes to resize the listbox overlay accordingly
132
- const [inputWidth, setInputWidth] = useState()
133
- useSafeLayoutEffect(() => {
134
- const updateInputWidth = () => {
135
- setInputWidth(inputRef?.current?.clientWidth + 4) // adding back all the input borders / outlines
136
- setIsExpanded(false) // close the suggestions while the input is changing
137
- }
138
-
139
- const throttledUpdateInputWidth = throttle(updateInputWidth, 100, { leading: false })
140
-
141
- updateInputWidth()
142
-
143
- window.addEventListener('load', updateInputWidth)
144
- window.addEventListener('resize', throttledUpdateInputWidth)
145
-
146
- return () => {
147
- window.removeEventListener('load', updateInputWidth)
148
- window.removeEventListener('resize', throttledUpdateInputWidth)
149
- }
150
- }, [inputRef])
151
-
152
- const handleChange = (newValue) => {
153
- onChange?.(newValue)
154
- setCurrentValue(newValue)
155
- setIsExpanded(newValue?.length >= minToSuggestion)
156
- if (!isControlled && initialItems !== undefined) {
157
- setCurrentItems(
158
- initialItems.filter(({ label }) =>
159
- label?.toLowerCase()?.includes(newValue?.toLowerCase())
160
- )
161
- )
162
- }
163
- }
164
- const handleSelect = (selectedId) => {
165
- onSelect?.(selectedId)
166
- const { label: newValue } = (isControlled ? items : currentItems)?.find(
167
- ({ id }) => id === selectedId
168
- )
169
- onChange?.(newValue)
170
- setCurrentValue(newValue)
171
- if (!isControlled && inputRef?.current) inputRef.current.value = newValue
172
- setIsExpanded(false)
173
- }
174
- const handleClose = (event) => {
175
- if (event.type === 'keydown') {
176
- if (event.key === 'Escape' || event.key === 27) {
177
- setIsExpanded(false)
178
- } else if (event.key === 'ArrowDown' && isExpanded && !isLoading && targetRef?.current) {
179
- targetRef.current.focus()
180
- }
181
- } else if (
182
- event.type === 'click' &&
183
- openOverlayRef?.current &&
184
- event.target &&
185
- !openOverlayRef?.current?.contains(event.target)
186
- ) {
187
- setIsExpanded(false)
188
- } else if (
189
- event.type === 'touchstart' &&
190
- openOverlayRef?.current &&
191
- event.touches[0].target &&
192
- !openOverlayRef?.current?.contains(event.touches[0].target)
193
- ) {
194
- setIsExpanded(false)
195
- }
196
- }
197
-
198
- const { supportsProps, ...selectedProps } = selectProps(rest)
199
-
200
- return (
201
- <>
202
- <InputSupports
203
- {...supportsProps}
204
- accessibilityAutoComplete="list"
205
- accessibilityControls="autocomplete"
206
- accessibilityExpanded={isExpanded}
207
- accessibilityRole="combobox"
208
- {...selectedProps}
209
- validation={validation}
210
- ref={ref}
211
- >
212
- {({ inputId, ...props }) => {
213
- if (typeof children === 'function')
214
- return children({
215
- inputId,
216
- inputRef,
217
- onChange: handleChange,
218
- onKeyPress: handleClose,
219
- readOnly,
220
- tokens: inputTokens,
221
- ...selectedProps,
222
- ...props,
223
- ...(isControlled ? { value } : { initialValue })
224
- })
225
-
226
- return (
227
- <TextInput
228
- onChange={handleChange}
229
- onClear={onClear}
230
- onKeyPress={handleClose}
231
- readOnly={readOnly}
232
- ref={inputRef}
233
- tokens={inputTokens}
234
- validation={validation}
235
- {...selectedProps}
236
- {...props}
237
- {...(isControlled ? { value } : { initialValue })}
238
- />
239
- )
240
- }}
241
- </InputSupports>
242
- {isExpanded && (
243
- <>
244
- <Listbox.Overlay
245
- overlaidPosition={overlaidPosition}
246
- isReady={isReady}
247
- minWidth={fullWidth ? inputWidth : MIN_LISTBOX_WIDTH}
248
- maxWidth={inputWidth}
249
- onLayout={onTargetLayout}
250
- ref={openOverlayRef}
251
- >
252
- {isLoading ? (
253
- <Loading label={getCopy('loading')} />
254
- ) : (
255
- <Suggestions
256
- hasResults={getCopy('hasResults')}
257
- id="autocomplete"
258
- items={itemsToSuggest(
259
- highlight(isControlled ? items : currentItems, currentValue, resultsTextColor)
260
- )}
261
- noResults={noResults ?? getCopy('noResults')}
262
- onClose={handleClose}
263
- onSelect={handleSelect}
264
- parentRef={inputRef}
265
- ref={targetRef}
266
- />
267
- )}
268
- </Listbox.Overlay>
269
- {targetRef?.current && (
270
- <div
271
- // This catches and shifts focus to other interactive elements.
272
- onFocus={() => targetRef?.current?.focus()}
273
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
274
- tabIndex={0}
275
- />
276
- )}
277
- </>
278
- )}
279
- </>
280
- )
281
- }
282
- )
283
- Autocomplete.displayName = 'Autocomplete'
284
-
285
- // If a language dictionary entry is provided, it must contain every key
286
- const dictionaryContentShape = PropTypes.shape({
287
- hasResults: PropTypes.string.isRequired,
288
- loading: PropTypes.string.isRequired,
289
- noResults: PropTypes.string.isRequired
290
- })
291
-
292
- Autocomplete.propTypes = {
293
- ...selectedSystemPropTypes,
294
- /**
295
- * Can be used to provide a function that renders a custom input:
296
- * <Autocomplete items={items} value={currentValue}>
297
- * {({ inputId, inputRef, onChange, onKeyPress, readOnly, tokens, value }) => (
298
- * <Search
299
- * nativeID={inputId}
300
- * ref={inputRef}
301
- * onChange={onChange}
302
- * onKeyPress={onKeyPress}
303
- * readOnly={readOnly}
304
- * tokens={tokens}
305
- * value={value}
306
- * />
307
- * )}
308
- * </Autocomplete>
309
- */
310
- children: PropTypes.func,
311
- /**
312
- * Copy language identifier
313
- */
314
- copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
315
- /**
316
- * Set to true in order to display the loading indicator instead of results
317
- */
318
- isLoading: PropTypes.bool,
319
- /**
320
- * List of items to display as suggestions
321
- */
322
- items: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, label: PropTypes.string })),
323
- /**
324
- * Label to display alongside the spinner when in a loading state
325
- */
326
- loadingLabel: PropTypes.string,
327
- /**
328
- * Minimum number of characters typed for a list of suggestions to appear
329
- */
330
- minToSuggestion: PropTypes.number,
331
- /**
332
- * Maximum number of suggestions provided at the same time
333
- */
334
- maxSuggestions: PropTypes.number,
335
- /**
336
- * Text or JSX to render when no results are available
337
- */
338
- noResults: PropTypes.node,
339
- /**
340
- * Handler function to be called when the input value changes
341
- */
342
- onChange: PropTypes.func,
343
- /**
344
- * Handler function to be called when the clear button (appears if the handler is passed) is pressed
345
- */
346
- onClear: PropTypes.func,
347
- /**
348
- * Callback function to be called when an item is selected from the list
349
- */
350
- onSelect: PropTypes.func,
351
- /**
352
- * Input value for controlled usage
353
- */
354
- value: PropTypes.string,
355
- /**
356
- * Can be used to set the initial items of the component
357
- */
358
- initialItems: PropTypes.arrayOf(
359
- PropTypes.shape({ id: PropTypes.string, label: PropTypes.string })
360
- ),
361
- /**
362
- * Initial value for the component
363
- */
364
- initialValue: PropTypes.string,
365
- /**
366
- * Boolean to set if it's readonly or not
367
- */
368
- readOnly: PropTypes.bool,
369
- /**
370
- * Use to visually mark an input as valid or invalid.
371
- */
372
- validation: PropTypes.oneOf(['error', 'success'])
373
- }
374
-
375
- export default Autocomplete
@@ -1,15 +0,0 @@
1
- import React from 'react'
2
- import PropTypes from 'prop-types'
3
- import { Box, StackView } from '@telus-uds/components-base'
4
- import Spinner from '../Spinner'
5
-
6
- const Loading = ({ label }) => (
7
- <Box space={3}>
8
- <StackView direction="row" space={2} tokens={{ alignItems: 'center' }}>
9
- <Spinner inline={true} show={true} label={label} labelPosition="right" />
10
- </StackView>
11
- </Box>
12
- )
13
- Loading.propTypes = { label: PropTypes.string }
14
-
15
- export default Loading
@@ -1,52 +0,0 @@
1
- import React, { forwardRef } from 'react'
2
- import PropTypes from 'prop-types'
3
- import { A11yText, Box, Typography } from '@telus-uds/components-base'
4
- import Listbox from '../Listbox'
5
-
6
- const Suggestions = forwardRef(
7
- ({ hasResults, items = [], noResults, onClose, onSelect, parentRef }, ref) => {
8
- const pressableItems = items.map(({ id, ...rest }) => ({
9
- id,
10
- onPress: () => onSelect(id),
11
- ...rest
12
- }))
13
- if (items?.length)
14
- return (
15
- <>
16
- <A11yText accessibilityLiveRegion="polite" text={hasResults} />
17
- <Listbox
18
- items={pressableItems}
19
- firstItemRef={ref}
20
- parentRef={parentRef}
21
- onClose={onClose}
22
- />
23
- </>
24
- )
25
-
26
- return (
27
- <Box space={3}>
28
- {typeof noResults === 'string' ? (
29
- <>
30
- <Typography accessibilityLiveRegion="polite" variant={{ size: 'small' }}>
31
- {noResults}
32
- </Typography>
33
- </>
34
- ) : (
35
- noResults
36
- )}
37
- </Box>
38
- )
39
- }
40
- )
41
- Suggestions.displayName = 'Suggestions'
42
- Suggestions.propTypes = {
43
- hasResults: PropTypes.string.isRequired,
44
- items: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, label: PropTypes.node }))
45
- .isRequired,
46
- noResults: PropTypes.node.isRequired,
47
- onClose: PropTypes.func.isRequired,
48
- onSelect: PropTypes.func.isRequired,
49
- parentRef: PropTypes.object.isRequired
50
- }
51
-
52
- export default Suggestions
@@ -1,6 +0,0 @@
1
- import palette from '@telus-uds/palette-allium/build/web/palette'
2
-
3
- export const DEFAULT_MIN_TO_SUGGESTION = 1
4
- export const DEFAULT_MAX_SUGGESTIONS = 5
5
- export const INPUT_LEFT_PADDING = palette.size.size16
6
- export const MIN_LISTBOX_WIDTH = palette.size.size288
@@ -1,12 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- import Autocomplete from './Autocomplete'
2
-
3
- export default Autocomplete
@@ -1,82 +0,0 @@
1
- import React from 'react'
2
- import styled from 'styled-components'
3
- import PropTypes from 'prop-types'
4
- import { Icon, Spacer, useThemeTokens, useListboxContext } from '@telus-uds/components-base'
5
-
6
- const StyledControlWrapper = styled.div(
7
- ({
8
- groupFontName,
9
- groupFontWeight,
10
- groupFontSize,
11
- groupColor,
12
- groupBackgroundColor,
13
- groupBorderColor,
14
- groupBorderWidth,
15
- groupBorderRadius,
16
- groupPaddingLeft,
17
- groupPaddingRight,
18
- groupPaddingTop,
19
- groupPaddingBottom,
20
- itemTextDecoration,
21
- itemOutline,
22
- groupHeight
23
- }) => ({
24
- fontFamily: `${groupFontName}${groupFontWeight}normal`,
25
- fontSize: groupFontSize,
26
- color: groupColor,
27
- textDecoration: itemTextDecoration,
28
- backgroundColor: groupBackgroundColor,
29
- outline: itemOutline,
30
- width: '100%',
31
- height: groupHeight,
32
- display: 'flex',
33
- alignItems: 'center',
34
- justifyContent: 'space-between',
35
- boxSizing: 'border-box',
36
- border: `${groupBorderWidth}px solid ${groupBorderColor}`,
37
- borderRadius: groupBorderRadius,
38
- paddingLeft: groupPaddingLeft - groupBorderWidth,
39
- paddingRight: groupPaddingRight - groupBorderWidth,
40
- paddingTop: groupPaddingTop - groupBorderWidth,
41
- paddingBottom: groupPaddingBottom - groupBorderWidth
42
- })
43
- )
44
-
45
- const GroupControl = ({ expanded, pressed, hover, focus, label, id }) => {
46
- const { selectedId, setSelectedId } = useListboxContext()
47
- const tokens = useThemeTokens(
48
- 'Listbox',
49
- {},
50
- {},
51
- {
52
- expanded,
53
- pressed,
54
- hover,
55
- current: selectedId === id && id !== undefined,
56
- focus
57
- }
58
- )
59
-
60
- return (
61
- <StyledControlWrapper onClick={() => setSelectedId(id)} {...tokens}>
62
- {label}
63
- <Spacer space={1} direction="row" />
64
- <Icon
65
- icon={tokens.groupIcon}
66
- tokens={{ color: tokens.groupColor }}
67
- variant={{ size: 'micro' }}
68
- />
69
- </StyledControlWrapper>
70
- )
71
- }
72
-
73
- GroupControl.propTypes = {
74
- id: PropTypes.string,
75
- expanded: PropTypes.bool,
76
- pressed: PropTypes.bool,
77
- hover: PropTypes.bool,
78
- focus: PropTypes.bool,
79
- label: PropTypes.string
80
- }
81
-
82
- export default GroupControl