@telus-uds/components-web 2.17.2 → 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 (88) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/component-docs.json +854 -421
  3. package/lib/Badge/Badge.js +2 -2
  4. package/lib/BlockQuote/BlockQuote.js +9 -0
  5. package/lib/Callout/Callout.js +5 -0
  6. package/lib/DatePicker/DatePicker.js +24 -4
  7. package/lib/Disclaimer/Disclaimer.js +4 -0
  8. package/lib/ExpandCollapseMini/ExpandCollapseMini.js +7 -1
  9. package/lib/NavigationBar/NavigationBar.js +8 -2
  10. package/lib/NavigationBar/NavigationSubMenu.js +2 -4
  11. package/lib/OrderedList/OrderedList.js +4 -2
  12. package/lib/Ribbon/Ribbon.js +2 -1
  13. package/lib/Toast/Toast.js +2 -1
  14. package/lib/Video/Video.js +3 -1
  15. package/lib/WaffleGrid/WaffleGrid.js +20 -20
  16. package/lib/baseExports.js +12 -0
  17. package/lib/index.js +0 -18
  18. package/lib/utils/useOverlaidPosition.js +2 -2
  19. package/lib-module/Badge/Badge.js +1 -1
  20. package/lib-module/BlockQuote/BlockQuote.js +10 -1
  21. package/lib-module/Callout/Callout.js +6 -1
  22. package/lib-module/DatePicker/DatePicker.js +24 -4
  23. package/lib-module/Disclaimer/Disclaimer.js +4 -0
  24. package/lib-module/ExpandCollapseMini/ExpandCollapseMini.js +8 -2
  25. package/lib-module/NavigationBar/NavigationBar.js +8 -2
  26. package/lib-module/NavigationBar/NavigationSubMenu.js +1 -2
  27. package/lib-module/OrderedList/OrderedList.js +5 -3
  28. package/lib-module/Ribbon/Ribbon.js +3 -2
  29. package/lib-module/Toast/Toast.js +3 -2
  30. package/lib-module/Video/Video.js +4 -2
  31. package/lib-module/WaffleGrid/WaffleGrid.js +21 -21
  32. package/lib-module/baseExports.js +1 -1
  33. package/lib-module/index.js +0 -2
  34. package/lib-module/utils/useOverlaidPosition.js +2 -2
  35. package/package.json +3 -3
  36. package/src/Badge/Badge.jsx +1 -1
  37. package/src/BlockQuote/BlockQuote.jsx +10 -1
  38. package/src/Callout/Callout.jsx +11 -1
  39. package/src/DatePicker/DatePicker.jsx +20 -2
  40. package/src/Disclaimer/Disclaimer.jsx +3 -0
  41. package/src/ExpandCollapseMini/ExpandCollapseMini.jsx +7 -2
  42. package/src/NavigationBar/NavigationBar.jsx +7 -2
  43. package/src/NavigationBar/NavigationSubMenu.jsx +1 -2
  44. package/src/OrderedList/OrderedList.jsx +4 -3
  45. package/src/Ribbon/Ribbon.jsx +8 -2
  46. package/src/Toast/Toast.jsx +4 -2
  47. package/src/Video/Video.jsx +9 -2
  48. package/src/WaffleGrid/WaffleGrid.jsx +21 -15
  49. package/src/baseExports.js +2 -0
  50. package/src/index.js +0 -2
  51. package/src/utils/useOverlaidPosition.js +2 -2
  52. package/types/Callout.d.ts +1 -0
  53. package/lib/Autocomplete/Autocomplete.js +0 -407
  54. package/lib/Autocomplete/Loading.js +0 -46
  55. package/lib/Autocomplete/Suggestions.js +0 -81
  56. package/lib/Autocomplete/constants.js +0 -19
  57. package/lib/Autocomplete/dictionary.js +0 -19
  58. package/lib/Autocomplete/index.js +0 -13
  59. package/lib/Listbox/GroupControl.js +0 -110
  60. package/lib/Listbox/Listbox.js +0 -179
  61. package/lib/Listbox/ListboxGroup.js +0 -145
  62. package/lib/Listbox/ListboxItem.js +0 -101
  63. package/lib/Listbox/ListboxOverlay.js +0 -91
  64. package/lib/Listbox/index.js +0 -13
  65. package/lib-module/Autocomplete/Autocomplete.js +0 -383
  66. package/lib-module/Autocomplete/Loading.js +0 -32
  67. package/lib-module/Autocomplete/Suggestions.js +0 -64
  68. package/lib-module/Autocomplete/constants.js +0 -5
  69. package/lib-module/Autocomplete/dictionary.js +0 -12
  70. package/lib-module/Autocomplete/index.js +0 -2
  71. package/lib-module/Listbox/GroupControl.js +0 -96
  72. package/lib-module/Listbox/Listbox.js +0 -158
  73. package/lib-module/Listbox/ListboxGroup.js +0 -122
  74. package/lib-module/Listbox/ListboxItem.js +0 -77
  75. package/lib-module/Listbox/ListboxOverlay.js +0 -69
  76. package/lib-module/Listbox/index.js +0 -2
  77. package/src/Autocomplete/Autocomplete.jsx +0 -357
  78. package/src/Autocomplete/Loading.jsx +0 -15
  79. package/src/Autocomplete/Suggestions.jsx +0 -52
  80. package/src/Autocomplete/constants.js +0 -6
  81. package/src/Autocomplete/dictionary.js +0 -12
  82. package/src/Autocomplete/index.js +0 -3
  83. package/src/Listbox/GroupControl.jsx +0 -82
  84. package/src/Listbox/Listbox.jsx +0 -163
  85. package/src/Listbox/ListboxGroup.jsx +0 -125
  86. package/src/Listbox/ListboxItem.jsx +0 -80
  87. package/src/Listbox/ListboxOverlay.jsx +0 -72
  88. package/src/Listbox/index.js +0 -3
@@ -1,357 +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
-
357
- 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
@@ -1,163 +0,0 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react'
2
- import PropTypes from 'prop-types'
3
- import styled from 'styled-components'
4
- import {
5
- ExpandCollapse,
6
- useThemeTokens,
7
- withLinkRouter,
8
- ListboxContext
9
- } from '@telus-uds/components-base'
10
- import ListboxGroup from './ListboxGroup'
11
- import ListboxItem from './ListboxItem'
12
- import DropdownOverlay from './ListboxOverlay'
13
-
14
- const StyledList = styled.ul({
15
- margin: 0,
16
- padding: 0,
17
- listStyle: 'none'
18
- })
19
-
20
- const getInitialOpen = (items, selectedId) =>
21
- items
22
- .filter(
23
- (item) =>
24
- item.items &&
25
- item.items.some((nestedItem) => (nestedItem.id ?? nestedItem.label) === selectedId)
26
- )
27
- .map((item) => item.id ?? item.label)
28
-
29
- const Listbox = ({
30
- items = [],
31
- firstItemRef = null, // focus will be moved to this one once within the menu
32
- parentRef = null, // to return focus to after leaving the last menu item
33
- selectedId: defaultSelectedId,
34
- LinkRouter,
35
- itemRouterProps,
36
- onClose,
37
- variant,
38
- tokens
39
- }) => {
40
- const initialOpen = getInitialOpen(items, defaultSelectedId)
41
-
42
- const [selectedId, setSelectedId] = useState(defaultSelectedId)
43
-
44
- const { minHeight, minWidth } = useThemeTokens('Listbox', variant, tokens)
45
-
46
- // We need to keep track of each item's ref in order to be able to
47
- // focus on a specific item via keyboard navigation
48
- const itemRefs = useRef([])
49
- if (firstItemRef?.current) itemRefs.current[0] = firstItemRef.current
50
- const [focusedIndex, setFocusedIndex] = useState(0)
51
- const handleKeydown = useCallback(
52
- (event) => {
53
- const nextItemRef = itemRefs.current[focusedIndex + 1]
54
- const prevItemRef = itemRefs.current[focusedIndex - 1]
55
- if (event.key === 'ArrowUp' || (event.shiftKey && event.key === 'Tab')) {
56
- // Move the focus to the previous item or to the parent one if on the first
57
- if (prevItemRef) {
58
- event.preventDefault()
59
- prevItemRef.focus()
60
- } else if (parentRef) parentRef.current?.focus()
61
- setFocusedIndex(focusedIndex - 1)
62
- } else if ((event.key === 'ArrowDown' || event.key === 'Tab') && nextItemRef) {
63
- event.preventDefault()
64
- setFocusedIndex(focusedIndex + 1)
65
- nextItemRef.focus()
66
- } else if (event.key === 'Escape') {
67
- // Close the dropdown
68
- parentRef?.current?.click()
69
- // Return focus to the dropdown control after leaving the last item
70
- parentRef?.current?.focus()
71
- if (onClose) onClose(event)
72
- }
73
- },
74
- [focusedIndex, onClose, parentRef]
75
- )
76
-
77
- // Add listeners for mouse clicks outside and for key presses
78
- useEffect(() => {
79
- window.addEventListener('click', onClose)
80
- window.addEventListener('keydown', handleKeydown)
81
- window.addEventListener('touchstart', onClose)
82
-
83
- return () => {
84
- window.removeEventListener('click', onClose)
85
- window.removeEventListener('keydown', handleKeydown)
86
- window.removeEventListener('touchstart', onClose)
87
- }
88
- }, [onClose, handleKeydown])
89
-
90
- return (
91
- <ListboxContext.Provider value={{ selectedId, setSelectedId }}>
92
- <ExpandCollapse initialOpen={initialOpen} maxOpen={1}>
93
- {(expandProps) => (
94
- <StyledList role="listbox" style={{ minHeight, minWidth }}>
95
- {items.map((item, index) => {
96
- const { id, label, items: nestedItems } = item
97
- const itemId = id ?? label
98
-
99
- // Give `firstItemRef` to the first focusable item
100
- const itemRef =
101
- (index === 0 && !itemId !== selectedId) ||
102
- (index === 1 && items[0].id === selectedId)
103
- ? firstItemRef
104
- : (ref) => {
105
- itemRefs.current[index] = ref
106
- return ref
107
- }
108
-
109
- return nestedItems ? (
110
- <ListboxGroup
111
- {...item}
112
- expandProps={expandProps}
113
- LinkRouter={LinkRouter}
114
- itemRouterProps={itemRouterProps}
115
- prevItemRef={itemRefs.current[index - 1] ?? null}
116
- nextItemRef={itemRefs.current[index + 1] ?? null}
117
- ref={itemRef}
118
- key={itemId}
119
- />
120
- ) : (
121
- <ListboxItem
122
- {...item}
123
- key={itemId}
124
- id={itemId}
125
- LinkRouter={LinkRouter}
126
- itemRouterProps={itemRouterProps}
127
- prevItemRef={itemRefs.current[index - 1] ?? null}
128
- nextItemRef={itemRefs.current[index + 1] ?? null}
129
- ref={itemRef}
130
- />
131
- )
132
- })}
133
- </StyledList>
134
- )}
135
- </ExpandCollapse>
136
- </ListboxContext.Provider>
137
- )
138
- }
139
-
140
- Listbox.propTypes = {
141
- ...withLinkRouter.propTypes,
142
- /**
143
- * Focus will be moved to the item with this ref once within the menu.
144
- */
145
- firstItemRef: PropTypes.object,
146
- /**
147
- * Focus will be returned to the dropdown control with this ref after leaving
148
- * the last menu item.
149
- */
150
- parentRef: PropTypes.object,
151
- /**
152
- * `Listbox` items
153
- */
154
- items: PropTypes.array,
155
- /**
156
- * To select an item by default
157
- */
158
- selectedId: PropTypes.string
159
- }
160
-
161
- Listbox.Overlay = DropdownOverlay
162
-
163
- export default Listbox