@telus-uds/components-base 1.18.1 → 1.20.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 (112) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/__tests17__/ThemeProvider/ThemeProvider.test.jsx +2 -1
  3. package/component-docs.json +1035 -231
  4. package/jest.config-android.js +17 -0
  5. package/jest.config-ios.js +18 -0
  6. package/jest.config-web.js +31 -0
  7. package/lib/BaseProvider/index.js +2 -1
  8. package/lib/Box/Box.js +14 -1
  9. package/lib/Button/ButtonBase.js +6 -2
  10. package/lib/Button/ButtonDropdown.js +207 -0
  11. package/lib/Button/index.js +8 -0
  12. package/lib/Carousel/Carousel.js +34 -6
  13. package/lib/Carousel/CarouselItem/CarouselItem.js +7 -1
  14. package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +22 -14
  15. package/lib/FlexGrid/Col/Col.js +1 -3
  16. package/lib/FlexGrid/FlexGrid.js +3 -5
  17. package/lib/FlexGrid/Row/Row.js +3 -3
  18. package/lib/IconButton/IconButton.js +12 -4
  19. package/lib/MultiSelectFilter/MultiSelectFilter.js +276 -0
  20. package/lib/MultiSelectFilter/dictionary.js +19 -0
  21. package/lib/MultiSelectFilter/index.js +13 -0
  22. package/lib/Pagination/SideButton.js +6 -4
  23. package/lib/Responsive/Responsive.js +58 -0
  24. package/lib/Responsive/index.js +13 -0
  25. package/lib/Search/Search.js +33 -63
  26. package/lib/Select/Picker.native.js +16 -13
  27. package/lib/Select/Select.js +7 -1
  28. package/lib/Select/constants.js +15 -0
  29. package/lib/StepTracker/Step.js +2 -1
  30. package/lib/Tags/Tags.js +10 -4
  31. package/lib/TextInput/TextInput.js +9 -2
  32. package/lib/TextInput/TextInputBase.js +98 -20
  33. package/lib/TextInput/dictionary.js +15 -0
  34. package/lib/ThemeProvider/ThemeProvider.js +6 -1
  35. package/lib/index.js +18 -0
  36. package/lib/utils/BaseView/BaseView.js +64 -0
  37. package/lib/utils/BaseView/BaseView.native.js +16 -0
  38. package/lib/utils/BaseView/index.js +13 -0
  39. package/lib/utils/index.js +10 -1
  40. package/lib/utils/input.js +11 -3
  41. package/lib/utils/props/handlerProps.js +5 -0
  42. package/lib-module/BaseProvider/index.js +2 -1
  43. package/lib-module/Box/Box.js +14 -1
  44. package/lib-module/Button/ButtonBase.js +6 -2
  45. package/lib-module/Button/ButtonDropdown.js +181 -0
  46. package/lib-module/Button/index.js +2 -1
  47. package/lib-module/Carousel/Carousel.js +34 -6
  48. package/lib-module/Carousel/CarouselItem/CarouselItem.js +8 -2
  49. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +24 -16
  50. package/lib-module/FlexGrid/Col/Col.js +2 -3
  51. package/lib-module/FlexGrid/FlexGrid.js +2 -3
  52. package/lib-module/FlexGrid/Row/Row.js +2 -2
  53. package/lib-module/IconButton/IconButton.js +14 -4
  54. package/lib-module/MultiSelectFilter/MultiSelectFilter.js +248 -0
  55. package/lib-module/MultiSelectFilter/dictionary.js +12 -0
  56. package/lib-module/MultiSelectFilter/index.js +2 -0
  57. package/lib-module/Pagination/SideButton.js +6 -4
  58. package/lib-module/Responsive/Responsive.js +45 -0
  59. package/lib-module/Responsive/index.js +2 -0
  60. package/lib-module/Search/Search.js +33 -61
  61. package/lib-module/Select/Picker.native.js +15 -13
  62. package/lib-module/Select/Select.js +6 -1
  63. package/lib-module/Select/constants.js +5 -0
  64. package/lib-module/StepTracker/Step.js +2 -1
  65. package/lib-module/Tags/Tags.js +10 -4
  66. package/lib-module/TextInput/TextInput.js +6 -0
  67. package/lib-module/TextInput/TextInputBase.js +96 -21
  68. package/lib-module/TextInput/dictionary.js +8 -0
  69. package/lib-module/ThemeProvider/ThemeProvider.js +6 -1
  70. package/lib-module/index.js +2 -0
  71. package/lib-module/utils/BaseView/BaseView.js +43 -0
  72. package/lib-module/utils/BaseView/BaseView.native.js +6 -0
  73. package/lib-module/utils/BaseView/index.js +2 -0
  74. package/lib-module/utils/index.js +2 -1
  75. package/lib-module/utils/input.js +11 -3
  76. package/lib-module/utils/props/handlerProps.js +5 -0
  77. package/package.json +6 -3
  78. package/src/BaseProvider/index.jsx +4 -1
  79. package/src/Box/Box.jsx +14 -1
  80. package/src/Button/ButtonBase.jsx +4 -2
  81. package/src/Button/ButtonDropdown.jsx +179 -0
  82. package/src/Button/index.js +2 -1
  83. package/src/Carousel/Carousel.jsx +48 -13
  84. package/src/Carousel/CarouselItem/CarouselItem.jsx +9 -2
  85. package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +19 -15
  86. package/src/FlexGrid/Col/Col.jsx +4 -4
  87. package/src/FlexGrid/FlexGrid.jsx +11 -10
  88. package/src/FlexGrid/Row/Row.jsx +4 -3
  89. package/src/IconButton/IconButton.jsx +3 -1
  90. package/src/MultiSelectFilter/MultiSelectFilter.jsx +227 -0
  91. package/src/MultiSelectFilter/dictionary.js +12 -0
  92. package/src/MultiSelectFilter/index.js +3 -0
  93. package/src/Pagination/SideButton.jsx +5 -5
  94. package/src/Responsive/Responsive.jsx +33 -0
  95. package/src/Responsive/index.js +3 -0
  96. package/src/Search/Search.jsx +19 -33
  97. package/src/Select/Picker.native.jsx +29 -14
  98. package/src/Select/Select.jsx +7 -1
  99. package/src/Select/constants.js +5 -0
  100. package/src/StepTracker/Step.jsx +5 -1
  101. package/src/Tags/Tags.jsx +46 -33
  102. package/src/TextInput/TextInput.jsx +5 -0
  103. package/src/TextInput/TextInputBase.jsx +85 -20
  104. package/src/TextInput/dictionary.js +8 -0
  105. package/src/ThemeProvider/ThemeProvider.jsx +5 -1
  106. package/src/index.js +2 -0
  107. package/src/utils/BaseView/BaseView.jsx +38 -0
  108. package/src/utils/BaseView/BaseView.native.jsx +6 -0
  109. package/src/utils/BaseView/index.js +3 -0
  110. package/src/utils/index.js +1 -0
  111. package/src/utils/input.js +9 -4
  112. package/src/utils/props/handlerProps.js +4 -0
@@ -164,7 +164,7 @@ const Carousel = React.forwardRef(
164
164
  ),
165
165
  tag = 'ul',
166
166
  accessibilityRole,
167
- accessibilityLabel = title,
167
+ accessibilityLabel,
168
168
  accessibilityLiveRegion = 'polite',
169
169
  copy,
170
170
  ...rest
@@ -296,7 +296,7 @@ const Carousel = React.forwardRef(
296
296
 
297
297
  animate(toValue, index)
298
298
 
299
- if (onIndexChanged) onIndexChanged(calcDelta)
299
+ if (onIndexChanged) onIndexChanged(calcDelta, index)
300
300
  return calcDelta
301
301
  },
302
302
  [containerLayout.width, activeIndex, animate, children.length, onIndexChanged]
@@ -422,6 +422,32 @@ const Carousel = React.forwardRef(
422
422
  const activePanelNavigation =
423
423
  tabs && showPanelTabs ? <CarouselTabsPanel items={tabs} /> : panelNavigation
424
424
 
425
+ const isFirstFocusContainer = Boolean(refocus && !skipLinkHref)
426
+ const containerRef = (element) => {
427
+ // Apply both firstFocusRef to the container
428
+ firstFocusRef.current = element
429
+ // Also apply forwarded ref if there is one (which could be a function ref)
430
+ if (ref) {
431
+ if (typeof ref === 'object') {
432
+ // eslint-disable-next-line no-param-reassign
433
+ ref.current = element
434
+ } else if (typeof ref === 'function') {
435
+ ref(element)
436
+ }
437
+ }
438
+ }
439
+ // If container isn't used for focus, give it a label of title if none is passed in,
440
+ // otherwise read the current position on focus
441
+ const containerAccessibilityLabel =
442
+ systemProps.accessibilityLabel ?? isFirstFocusContainer
443
+ ? `${title ? `${title} ` : ''}${getCopyWithPlaceholders('stepTrackerLabel')}`
444
+ : title
445
+ const containerProps = {
446
+ accessibilityLabel: containerAccessibilityLabel,
447
+ // If used for focus, attach the ref and draw a focus box around the whole carousel
448
+ ...(isFirstFocusContainer && { ref: containerRef, focusable: true })
449
+ }
450
+
425
451
  return (
426
452
  <CarouselProvider
427
453
  activeIndex={activeIndex}
@@ -434,7 +460,13 @@ const Carousel = React.forwardRef(
434
460
  refocus={refocus}
435
461
  width={containerLayout.width}
436
462
  >
437
- <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
463
+ <View
464
+ style={staticStyles.root}
465
+ onLayout={onContainerLayout}
466
+ ref={ref}
467
+ {...systemProps}
468
+ {...containerProps}
469
+ >
438
470
  {showPreviousNextNavigation && (
439
471
  <View
440
472
  style={selectPreviousNextNavigationButtonStyles(
@@ -464,14 +496,14 @@ const Carousel = React.forwardRef(
464
496
  {getCopyWithPlaceholders('skipLink')}
465
497
  </SkipLink>
466
498
  )}
467
- <A11yText
468
- // Read the current slide position to screen readers on slide.
469
- // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
470
- ref={!skipLinkHref && refocus ? firstFocusRef : null}
471
- accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
472
- focusable={!skipLinkHref && refocus}
473
- text={getCopyWithPlaceholders('stepTrackerLabel')}
474
- />
499
+ {!isFirstFocusContainer && (
500
+ <A11yText
501
+ // Read the current slide position to screen readers on slide.
502
+ // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
503
+ accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
504
+ text={getCopyWithPlaceholders('stepTrackerLabel')}
505
+ />
506
+ )}
475
507
  <View style={selectContainerStyles(containerLayout.width)}>
476
508
  <Animated.View
477
509
  style={StyleSheet.flatten([
@@ -488,7 +520,10 @@ const Carousel = React.forwardRef(
488
520
  >
489
521
  {childrenArray.map((element, index) => {
490
522
  const hidden = !isAnimating && index !== activeIndex
491
- const clonedElement = React.cloneElement(element, { elementIndex: index, hidden })
523
+ const clonedElement = React.cloneElement(element, {
524
+ elementIndex: index,
525
+ hidden
526
+ })
492
527
  return <React.Fragment key={index.toFixed(2)}>{clonedElement}</React.Fragment>
493
528
  })}
494
529
  </Animated.View>
@@ -577,7 +612,7 @@ Carousel.propTypes = {
577
612
  * This function is also provided with a parameter indicating changed index (either 1, or -1)
578
613
  * Use it as follows:
579
614
  * ```js
580
- * const onIndexChangedCallback = React.useCallback((changedIndex) => {
615
+ * const onIndexChangedCallback = React.useCallback((changedIndex, currentActiveIndex) => {
581
616
  * console.log(changedIndex)
582
617
  * }, []) // pass local dependencies as per your component
583
618
  * <Carousel
@@ -6,7 +6,8 @@ import {
6
6
  getA11yPropsFromHtmlTag,
7
7
  selectSystemProps,
8
8
  a11yProps,
9
- viewProps
9
+ viewProps,
10
+ variantProp
10
11
  } from '../../utils'
11
12
  import { useCarousel } from '../CarouselContext'
12
13
 
@@ -18,6 +19,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
18
19
  */
19
20
  const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) => {
20
21
  const { width, activeIndex } = useCarousel()
22
+
21
23
  const selectedProps = selectProps({
22
24
  ...rest,
23
25
  ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
@@ -38,6 +40,7 @@ const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) =
38
40
 
39
41
  CarouselItem.propTypes = {
40
42
  ...selectedSystemPropTypes,
43
+ variant: variantProp.propType,
41
44
  /**
42
45
  * Index of the current slide
43
46
  * Don't pass this prop when using `Carousel.Item` as it is already being passed by `Carousel` top-level component
@@ -58,7 +61,11 @@ CarouselItem.propTypes = {
58
61
  * Carousel's innermost container defaults to `'ul'` which can be overridden. If the tag of either
59
62
  * `Carousel` or `Carousel.Item` is overriden, the other should be too, to avoid producing invalid HTML.
60
63
  */
61
- tag: PropTypes.oneOf(layoutTags)
64
+ tag: PropTypes.oneOf(layoutTags),
65
+ /**
66
+ * Function to set carousel content background color when slide is being display
67
+ */
68
+ setContentBackgroundColor: PropTypes.func
62
69
  }
63
70
 
64
71
  CarouselItem.displayName = 'Carousel.Item'
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useRef } from 'react'
1
+ import React, { forwardRef, useEffect, useRef, useState } from 'react'
2
2
  import { View } from 'react-native'
3
3
 
4
4
  import PropTypes from 'prop-types'
@@ -7,10 +7,15 @@ import StackView from '../../StackView'
7
7
  import { useCarousel } from '../CarouselContext'
8
8
  import CarouselTabsPanelItem from './CarouselTabsPanelItem'
9
9
 
10
+ const selectTabPanelStyle = () => ({
11
+ backgroundColor: 'transparent'
12
+ })
13
+
10
14
  const CarouselTabsPanel = forwardRef(({ items }, ref) => {
11
15
  const { activeIndex, goTo } = useCarousel()
12
16
  const nextFocusRef = useRef()
13
17
  const firstTabRef = useRef()
18
+ const [isInverse, setIsInverse] = useState(false)
14
19
 
15
20
  // TODO: figure out a better cross-brand way to specify subcomponent variants.
16
21
  // For now, this picks an Allium variant, and does nothing in brands that lack it.
@@ -19,20 +24,16 @@ const CarouselTabsPanel = forwardRef(({ items }, ref) => {
19
24
 
20
25
  const lastTabSelected = activeIndex === items.length - 1
21
26
 
27
+ // Get current select tab style
28
+ useEffect(() => {
29
+ const [selectedVariantIsInverse] = items.filter((_, index) => index === activeIndex)
30
+ setIsInverse(selectedVariantIsInverse?.inverse)
31
+ }, [items, activeIndex])
32
+
22
33
  return (
23
- <>
24
- <View
25
- focusable
26
- accessible
27
- onFocus={(event) => {
28
- // When user forward-tabs into this section, focus the next tab; if they backwards-tab
29
- // (shift-tab) back into the carousel content, don't interfere.
30
- const previousWebFocus = event.relatedTarget
31
- if (previousWebFocus !== firstTabRef.current) nextFocusRef.current.focus()
32
- }}
33
- />
34
+ <View style={selectTabPanelStyle()}>
34
35
  <StackView direction="row" space={3} divider={{ variant: dividerVariant }} ref={ref}>
35
- {items.map(({ title, onPress, ...panelItemProps }, index) => {
36
+ {items.map(({ title, onPress, inverse, ...panelItemProps }, index) => {
36
37
  const selected = index === activeIndex
37
38
  const isNext = index === activeIndex + 1
38
39
 
@@ -51,6 +52,7 @@ const CarouselTabsPanel = forwardRef(({ items }, ref) => {
51
52
  title={title}
52
53
  selected={selected}
53
54
  onPress={handlePress}
55
+ variant={{ inverse: isInverse }}
54
56
  {...panelItemProps}
55
57
  />
56
58
  )
@@ -58,12 +60,14 @@ const CarouselTabsPanel = forwardRef(({ items }, ref) => {
58
60
  </StackView>
59
61
  {/* TODO: integrate with skiplink, replace this with focusing skiplink target */}
60
62
  <View focusable accessible ref={lastTabSelected ? nextFocusRef : null} />
61
- </>
63
+ </View>
62
64
  )
63
65
  })
64
66
  CarouselTabsPanel.displayName = 'CarouselTabsPanel'
65
67
  CarouselTabsPanel.propTypes = {
66
- items: PropTypes.arrayOf(PropTypes.shape(CarouselTabsPanelItem.propTypes || {}))
68
+ items: PropTypes.arrayOf(PropTypes.shape(CarouselTabsPanelItem.propTypes || {})),
69
+ // Color defined by `Carousel.item` variant otherwise fallback to transparent
70
+ contentBackgroundColor: PropTypes.string
67
71
  }
68
72
 
69
73
  export default CarouselTabsPanel
@@ -1,12 +1,12 @@
1
1
  import React, { forwardRef, useContext } from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { Platform, StyleSheet, View } from 'react-native'
3
+ import { Platform, StyleSheet } from 'react-native'
4
4
  import { viewports } from '@telus-uds/system-constants'
5
5
 
6
6
  import GutterContext from '../providers/GutterContext'
7
7
  import { useViewport } from '../../ViewportProvider'
8
8
  import applyInheritance from '../helpers'
9
- import { responsiveProps } from '../../utils'
9
+ import { responsiveProps, BaseView } from '../../utils'
10
10
 
11
11
  const Col = forwardRef(
12
12
  (
@@ -162,7 +162,7 @@ const Col = forwardRef(
162
162
  xl: offsetsWithIheritance[4]
163
163
  }
164
164
  return (
165
- <View
165
+ <BaseView
166
166
  ref={ref}
167
167
  {...viewProps}
168
168
  style={[
@@ -174,7 +174,7 @@ const Col = forwardRef(
174
174
  ]}
175
175
  >
176
176
  {children}
177
- </View>
177
+ </BaseView>
178
178
  )
179
179
  }
180
180
  )
@@ -1,21 +1,22 @@
1
1
  import React, { forwardRef } from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, StyleSheet } from 'react-native'
3
+ import { StyleSheet } from 'react-native'
4
4
  import { viewports } from '@telus-uds/system-constants'
5
-
6
- import Row from './Row'
7
- import Col from './Col'
8
- import { useViewport } from '../ViewportProvider'
9
- import GutterContext from './providers/GutterContext'
10
- import applyInheritance from './helpers'
11
5
  import {
12
6
  a11yProps,
13
7
  viewProps,
14
8
  getA11yPropsFromHtmlTag,
15
9
  layoutTags,
16
- selectSystemProps
10
+ selectSystemProps,
11
+ BaseView
17
12
  } from '../utils'
18
13
 
14
+ import Row from './Row'
15
+ import Col from './Col'
16
+ import { useViewport } from '../ViewportProvider'
17
+ import GutterContext from './providers/GutterContext'
18
+ import applyInheritance from './helpers'
19
+
19
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
20
21
 
21
22
  /**
@@ -75,7 +76,7 @@ const FlexGrid = forwardRef(
75
76
 
76
77
  return (
77
78
  <GutterContext.Provider value={gutter}>
78
- <View
79
+ <BaseView
79
80
  ref={ref}
80
81
  {...props}
81
82
  style={[
@@ -84,7 +85,7 @@ const FlexGrid = forwardRef(
84
85
  ]}
85
86
  >
86
87
  {children}
87
- </View>
88
+ </BaseView>
88
89
  </GutterContext.Provider>
89
90
  )
90
91
  }
@@ -1,10 +1,11 @@
1
1
  import React, { forwardRef } from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, StyleSheet } from 'react-native'
3
+ import { StyleSheet } from 'react-native'
4
4
  import { viewports } from '@telus-uds/system-constants'
5
5
 
6
6
  import { useViewport } from '../../ViewportProvider'
7
7
  import applyInheritance from '../helpers'
8
+ import { BaseView } from '../../utils'
8
9
 
9
10
  const horizontalAlignStyles = (horizontalAlign) => {
10
11
  switch (horizontalAlign) {
@@ -96,7 +97,7 @@ const Row = forwardRef(
96
97
  }
97
98
 
98
99
  return (
99
- <View
100
+ <BaseView
100
101
  ref={ref}
101
102
  {...rest}
102
103
  style={[
@@ -111,7 +112,7 @@ const Row = forwardRef(
111
112
  ]}
112
113
  >
113
114
  {children}
114
- </View>
115
+ </BaseView>
115
116
  )
116
117
  }
117
118
  )
@@ -68,7 +68,9 @@ const IconButton = forwardRef(
68
68
  ...rest,
69
69
  accessibilityRole
70
70
  })
71
- const handlePress = linkProps.handleHref({ href, onPress })
71
+ const handlePress = () => {
72
+ linkProps.handleHref({ href, onPress })({ nativeEvent: { target: ref?.current?.id } })
73
+ }
72
74
 
73
75
  const getTokens = useThemeTokensCallback('IconButton', tokens, variant)
74
76
  const getOuterStyle = (pressableState) =>
@@ -0,0 +1,227 @@
1
+ import React, { forwardRef, useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { useThemeTokensCallback } from '../ThemeProvider'
5
+ import {
6
+ containUniqueFields,
7
+ getTokensPropType,
8
+ getPressHandlersWithArgs,
9
+ selectTokens,
10
+ useCopy,
11
+ useMultipleInputValues,
12
+ variantProp
13
+ } from '../utils'
14
+ import dictionary from './dictionary'
15
+
16
+ import Box from '../Box'
17
+ import { Button, ButtonDropdown } from '../Button'
18
+ import { CheckboxGroup } from '../Checkbox'
19
+ import Divider from '../Divider'
20
+ import FlexGrid from '../FlexGrid'
21
+ import Modal from '../Modal'
22
+ import Spacer from '../Spacer'
23
+ import StackView from '../StackView'
24
+ import Typography from '../Typography'
25
+ import { TextButton } from '../Link'
26
+
27
+ const { Col, Row } = FlexGrid
28
+
29
+ const MultiSelectFilter = forwardRef(
30
+ (
31
+ {
32
+ label,
33
+ id = label,
34
+ variant,
35
+ tokens,
36
+ items = [],
37
+ values,
38
+ initialValues,
39
+ maxValues,
40
+ onChange,
41
+ copy = 'en',
42
+ readOnly = false,
43
+ inactive = false,
44
+ rowLimit = 12,
45
+ ...rest
46
+ },
47
+ ref
48
+ ) => {
49
+ const { currentValues, setValues } = useMultipleInputValues({
50
+ initialValues,
51
+ values,
52
+ maxValues,
53
+ onChange,
54
+ readOnly
55
+ })
56
+
57
+ const getItemTokens = useThemeTokensCallback('ButtonDropdown', tokens, variant)
58
+ const getButtonTokens = (buttonState) => selectTokens('Button', getItemTokens(buttonState))
59
+ const getCopy = useCopy({ dictionary, copy })
60
+
61
+ const [isOpen, setIsOpen] = useState(false)
62
+ const [checkedIds, setCheckedIds] = useState(currentValues ?? [])
63
+
64
+ const colSize = items.length > rowLimit ? 2 : 1
65
+ const isSelected = currentValues.length > 0
66
+
67
+ const uniqueFields = ['id', 'label']
68
+ if (!containUniqueFields(items, uniqueFields)) {
69
+ throw new Error(`MultiSelectFilter items must have unique ${uniqueFields.join(', ')}`)
70
+ }
71
+
72
+ // Pass an object of relevant component state as first argument for any passed-in press handlers
73
+ const pressHandlers = getPressHandlersWithArgs(rest, [{ id, label, currentValues }])
74
+
75
+ const handleChange = (event) => {
76
+ if (pressHandlers.onPress) pressHandlers?.onPress(event)
77
+ setIsOpen(true)
78
+ }
79
+
80
+ const onApply = (e) => {
81
+ setValues(e)
82
+ setIsOpen(false)
83
+ }
84
+
85
+ return (
86
+ <>
87
+ <Modal
88
+ isOpen={isOpen}
89
+ onClose={() => setIsOpen(false)}
90
+ variant={{ width: colSize > 1 ? 'size576' : 's' }}
91
+ >
92
+ <Row>
93
+ <Typography variant={{ size: 'h4' }}>
94
+ {getCopy('filterByLabel').replace(/%\{filterCategory\}/g, label.toLowerCase())}
95
+ </Typography>
96
+ </Row>
97
+ <Spacer space={4} />
98
+ <Spacer space={1} />
99
+ <Box scroll={true}>
100
+ <Row distribute="between">
101
+ {[...Array(colSize).keys()].map((i) => (
102
+ <Col xs={12 / colSize} key={i}>
103
+ <CheckboxGroup
104
+ items={items.slice(i * rowLimit, (i + 1) * rowLimit)}
105
+ checkedIds={checkedIds}
106
+ onChange={(e) => setCheckedIds(e, i)}
107
+ />
108
+ <Spacer size={4} />
109
+ </Col>
110
+ ))}
111
+ </Row>
112
+ </Box>
113
+ <Divider
114
+ variant={{ width: 'full', color: 'E3E6E8', decorative: true, weight: 'thin' }}
115
+ space={4}
116
+ />
117
+ <Row>
118
+ <StackView direction="row" space={3} tokens={{ alignItems: 'center' }}>
119
+ <Button
120
+ onPress={() => onApply(checkedIds)}
121
+ variant={{ size: 'small', priority: 'high' }}
122
+ >
123
+ {getCopy('applyButtonLabel')}
124
+ </Button>
125
+ <Box>
126
+ <TextButton onPress={() => setCheckedIds([])}>
127
+ {getCopy('clearButtonLabel')}
128
+ </TextButton>
129
+ </Box>
130
+ </StackView>
131
+ </Row>
132
+ </Modal>
133
+ <ButtonDropdown
134
+ ref={ref}
135
+ key={id}
136
+ {...pressHandlers}
137
+ value={isOpen}
138
+ selected={isSelected}
139
+ label={label}
140
+ onChange={handleChange}
141
+ tokens={getButtonTokens}
142
+ inactive={inactive}
143
+ />
144
+ </>
145
+ )
146
+ }
147
+ )
148
+ MultiSelectFilter.displayName = 'MultiSelectFilter'
149
+
150
+ MultiSelectFilter.propTypes = {
151
+ /**
152
+ * The text displayed to the user in a ButtonDropdown.
153
+ */
154
+ label: PropTypes.string.isRequired,
155
+ /**
156
+ * An optional unique string may be provided to identify the ButtonDropdown.
157
+ * If not provided, the label is used.
158
+ */
159
+ id: PropTypes.string,
160
+ /**
161
+ * Sets the variant for ButtonDropdown element.
162
+ */
163
+ variant: variantProp.propType,
164
+ /**
165
+ * Sets the tokens for ButtonDropdown element.
166
+ */
167
+ tokens: getTokensPropType('ButtonDropdown'),
168
+ /**
169
+ * The options a user may select.
170
+ */
171
+ items: PropTypes.arrayOf(
172
+ PropTypes.shape({
173
+ /**
174
+ * The text displayed to the user with a checkbox, describing this option.
175
+ */
176
+ label: PropTypes.string.isRequired,
177
+ /**
178
+ * An optional unique string may be provided to identify this option.
179
+ * If not provided, the label is used.
180
+ */
181
+ id: PropTypes.string
182
+ })
183
+ ),
184
+ /**
185
+ * If the selected item(s) in the checkbox group(s) are to be controlled externally by
186
+ * a parent component, pass an array of strings as well as an `onChange` handler.
187
+ * Passing an array for "values" makes the MultiSelectFilter a "controlled" component that
188
+ * expects its state to be handled via `onChange` and so doesn't handle it itself.
189
+ */
190
+ values: PropTypes.arrayOf(PropTypes.string),
191
+ /**
192
+ * If `values` is not passed, making the MultiSelectFilter an "uncontrolled" component
193
+ * managing its own selected state, a default set of selections may be provided.
194
+ * Changing the `initialValues` does not change the user's selections.
195
+ */
196
+ initialValues: PropTypes.arrayOf(PropTypes.string),
197
+ /**
198
+ * If provided, sets a maximum number of items a user may select at once.
199
+ */
200
+ maxValues: PropTypes.number,
201
+ /**
202
+ * If provided, this function is called when the current selection is changed
203
+ * and is passed an array of the `id`s of all currently selected `items`.
204
+ */
205
+ onChange: PropTypes.func,
206
+ /**
207
+ * Select English or French copy for the accessible label.
208
+ */
209
+ copy: PropTypes.oneOf(['en', 'fr']),
210
+ /**
211
+ * If true, the ButtonDropdown cannot be selected by the user and simply show their current state.
212
+ */
213
+ readOnly: PropTypes.string,
214
+ /**
215
+ * If true, the MultiSelectFilter cannot be interacted with, ButtonDropdown is
216
+ * set as `disabled` and if the theme supports `inactive` appearances rules, these
217
+ * are applied.
218
+ */
219
+ inactive: PropTypes.string,
220
+ /**
221
+ * Sets the maximum number of items in one column. If number of items are more
222
+ * than the `rowLimit`, they will be rendered in 2 columns.
223
+ */
224
+ rowLimit: PropTypes.number
225
+ }
226
+
227
+ export default MultiSelectFilter
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ filterByLabel: 'Filter by %{filterCategory}:',
4
+ applyButtonLabel: 'Apply',
5
+ clearButtonLabel: 'Clear'
6
+ },
7
+ fr: {
8
+ filterByLabel: 'Filtrer par %{filterCategory}:',
9
+ applyButtonLabel: 'Appliquer',
10
+ clearButtonLabel: 'Effacer'
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ import MultiSelectFilter from './MultiSelectFilter'
2
+
3
+ export default MultiSelectFilter
@@ -13,8 +13,9 @@ import dictionary from './dictionary'
13
13
  import useCopy from '../utils/useCopy'
14
14
 
15
15
  // We need to drop the icon here since it gets rendered via children and not
16
- // `ButtonBase` in order to tap into the state of the button
17
- const selectButtonTokens = ({ icon: _, ...rest }) => selectTokens('Button', rest)
16
+ // `ButtonBase` in order to tap into the state of the button; `displayLabel` flag
17
+ // is also not needed
18
+ const selectButtonTokens = ({ icon: _, displayLabel: __, ...rest }) => selectTokens('Button', rest)
18
19
  const selectIconTokens = ({ color, iconSize, iconDisplace }, direction) => {
19
20
  return {
20
21
  color,
@@ -36,13 +37,12 @@ const SideButton = forwardRef(
36
37
 
37
38
  const getCopy = useCopy({ dictionary, copy })
38
39
 
39
- const { icon } = getTokens(tokens, buttonVariant)
40
+ const { icon, displayLabel } = getTokens(tokens, buttonVariant)
40
41
 
41
42
  const getButtonTokens = (buttonState) => selectButtonTokens(getTokens(buttonState))
42
43
  const getIconTokens = (buttonState) => selectIconTokens(getTokens(buttonState), direction)
43
44
 
44
45
  const label = direction === 'previous' ? getCopy('previousText') : getCopy('nextText')
45
- const showLabel = viewport !== 'sm' && viewport !== 'xs'
46
46
 
47
47
  const accessibilityLabel =
48
48
  direction === 'previous' ? getCopy('previousLabel') : getCopy('nextLabel')
@@ -69,7 +69,7 @@ const SideButton = forwardRef(
69
69
  iconPosition={directionToSide[direction]}
70
70
  iconProps={iconProps}
71
71
  >
72
- {showLabel && <Text style={textStyles}>{label}</Text>}
72
+ {displayLabel && <Text style={textStyles}>{label}</Text>}
73
73
  </IconText>
74
74
  )
75
75
  }}
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { viewports } from '@telus-uds/system-constants'
4
+ import { useResponsiveProp } from '../utils'
5
+
6
+ /**
7
+ * Responsive conditionally renders children based on whether the viewport matches the provided
8
+ * min and max viewports.
9
+ *
10
+ * In SSR, like other viewport utilities, it treats the viewport as `xs` both in SSR itself and
11
+ * during first hydration on the client side; then if the viewport is not `xs`, it re-renders
12
+ * after hydration. This may cause a layout shift on devices other than the narrowest.
13
+ */
14
+
15
+ const Responsive = ({ min = 'xs', max, children }) => {
16
+ // Start returning children at the 'min' viewport or greater
17
+ const byViewports = { [min]: children }
18
+ if (max && max !== 'xl') {
19
+ // Stop returning children at the viewport one above 'max' or greater
20
+ const maxIndex = viewports.keys.indexOf(max)
21
+ const maxPlusOne = maxIndex >= 0 ? viewports.keys[maxIndex + 1] : null
22
+ if (maxPlusOne) byViewports[maxPlusOne] = null
23
+ }
24
+ return <>{useResponsiveProp(byViewports, null)}</>
25
+ }
26
+
27
+ Responsive.propTypes = {
28
+ min: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
29
+ max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
30
+ children: PropTypes.node.isRequired
31
+ }
32
+
33
+ export default Responsive
@@ -0,0 +1,3 @@
1
+ import Responsive from './Responsive'
2
+
3
+ export default Responsive