@telus-uds/components-base 1.9.0 → 1.12.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 (57) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/component-docs.json +650 -27
  3. package/lib/Carousel/Carousel.js +672 -0
  4. package/lib/Carousel/CarouselContext.js +59 -0
  5. package/lib/Carousel/CarouselItem/CarouselItem.js +92 -0
  6. package/lib/Carousel/CarouselItem/index.js +13 -0
  7. package/lib/Carousel/dictionary.js +23 -0
  8. package/lib/Carousel/index.js +32 -0
  9. package/lib/InputSupports/InputSupports.js +10 -3
  10. package/lib/InputSupports/useInputSupports.js +3 -2
  11. package/lib/Modal/Modal.js +4 -0
  12. package/lib/Skeleton/Skeleton.js +1 -0
  13. package/lib/StepTracker/StepTracker.js +15 -12
  14. package/lib/TextInput/TextInput.js +3 -1
  15. package/lib/index.js +23 -0
  16. package/lib/utils/index.js +9 -0
  17. package/lib/utils/props/clickProps.js +2 -2
  18. package/lib/utils/props/handlerProps.js +77 -31
  19. package/lib/utils/useScrollBlocking.js +66 -0
  20. package/lib/utils/useScrollBlocking.native.js +11 -0
  21. package/lib-module/Carousel/Carousel.js +617 -0
  22. package/lib-module/Carousel/CarouselContext.js +43 -0
  23. package/lib-module/Carousel/CarouselItem/CarouselItem.js +75 -0
  24. package/lib-module/Carousel/CarouselItem/index.js +2 -0
  25. package/lib-module/Carousel/dictionary.js +16 -0
  26. package/lib-module/Carousel/index.js +2 -0
  27. package/lib-module/InputSupports/InputSupports.js +10 -3
  28. package/lib-module/InputSupports/useInputSupports.js +3 -2
  29. package/lib-module/Modal/Modal.js +3 -0
  30. package/lib-module/Skeleton/Skeleton.js +1 -0
  31. package/lib-module/StepTracker/StepTracker.js +14 -12
  32. package/lib-module/TextInput/TextInput.js +3 -1
  33. package/lib-module/index.js +2 -0
  34. package/lib-module/utils/index.js +1 -0
  35. package/lib-module/utils/props/clickProps.js +2 -2
  36. package/lib-module/utils/props/handlerProps.js +78 -31
  37. package/lib-module/utils/useScrollBlocking.js +58 -0
  38. package/lib-module/utils/useScrollBlocking.native.js +2 -0
  39. package/package.json +4 -4
  40. package/src/Carousel/Carousel.jsx +649 -0
  41. package/src/Carousel/CarouselContext.jsx +30 -0
  42. package/src/Carousel/CarouselItem/CarouselItem.jsx +66 -0
  43. package/src/Carousel/CarouselItem/index.js +3 -0
  44. package/src/Carousel/dictionary.js +16 -0
  45. package/src/Carousel/index.js +2 -0
  46. package/src/InputSupports/InputSupports.jsx +18 -3
  47. package/src/InputSupports/useInputSupports.js +2 -2
  48. package/src/Modal/Modal.jsx +3 -1
  49. package/src/Skeleton/Skeleton.jsx +1 -0
  50. package/src/StepTracker/StepTracker.jsx +21 -8
  51. package/src/TextInput/TextInput.jsx +1 -1
  52. package/src/index.js +2 -0
  53. package/src/utils/index.js +1 -0
  54. package/src/utils/props/clickProps.js +2 -2
  55. package/src/utils/props/handlerProps.js +64 -16
  56. package/src/utils/useScrollBlocking.js +57 -0
  57. package/src/utils/useScrollBlocking.native.js +2 -0
@@ -0,0 +1,66 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View, Platform } from 'react-native'
4
+ import {
5
+ layoutTags,
6
+ getA11yPropsFromHtmlTag,
7
+ selectSystemProps,
8
+ a11yProps,
9
+ viewProps
10
+ } from '../../utils'
11
+ import { useCarousel } from '../CarouselContext'
12
+
13
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
14
+
15
+ /**
16
+ * `Carousel.Item` is used to wrap the content of an individual slide and is suppsoed to be the
17
+ * only top-level component passed to the `Carousel`
18
+ */
19
+ const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) => {
20
+ const { width, activeIndex } = useCarousel()
21
+ const selectedProps = selectProps({
22
+ ...rest,
23
+ ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
24
+ })
25
+
26
+ const focusabilityProps = activeIndex === elementIndex ? {} : a11yProps.nonFocusableProps
27
+ const style = { width }
28
+ if (hidden && Platform.OS === 'web') {
29
+ // On web, visibility: hidden makes all children non-focusable. It doesn't exist on native.
30
+ style.visibility = 'hidden'
31
+ }
32
+ return (
33
+ <View style={style} {...selectedProps} {...focusabilityProps}>
34
+ {children}
35
+ </View>
36
+ )
37
+ }
38
+
39
+ CarouselItem.propTypes = {
40
+ ...selectedSystemPropTypes,
41
+ /**
42
+ * Index of the current slide
43
+ * Don't pass this prop when using `Carousel.Item` as it is already being passed by `Carousel` top-level component
44
+ */
45
+ elementIndex: PropTypes.number,
46
+ /**
47
+ * Provide custom accessibilityLabelledBy for Carousel slide
48
+ */
49
+ accessibilityLabelledBy: PropTypes.string,
50
+ /**
51
+ * Content of the slide
52
+ */
53
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
54
+ /**
55
+ * Sets the HTML tag of the outer container. By default `'li'` so that assistive technology sees
56
+ * the Carousel as a list of items.
57
+ *
58
+ * Carousel's innermost container defaults to `'ul'` which can be overridden. If the tag of either
59
+ * `Carousel` or `Carousel.Item` is overriden, the other should be too, to avoid producing invalid HTML.
60
+ */
61
+ tag: PropTypes.oneOf(layoutTags)
62
+ }
63
+
64
+ CarouselItem.displayName = 'Carousel.Item'
65
+
66
+ export default CarouselItem
@@ -0,0 +1,3 @@
1
+ import CarouselItem from './CarouselItem'
2
+
3
+ export default CarouselItem
@@ -0,0 +1,16 @@
1
+ // 'stepLabel' and 'stepTrackerLabel' are passed down to StepTracker
2
+ export default {
3
+ en: {
4
+ carouselLabel: '%{stepCount} items',
5
+ iconButtonLabel: 'Show %{itemLabel} %{targetStep} of %{stepCount}',
6
+ stepLabel: '%{itemLabel} %{stepNumber}',
7
+ stepTrackerLabel: '%{itemLabel} %{stepNumber} of %{stepCount}'
8
+ },
9
+ fr: {
10
+ // TODO: French translations here
11
+ carouselLabel: '(fr) %{stepCount} items',
12
+ iconButtonLabel: '(fr) Show %{itemLabel} %{targetStep} of %{stepCount}',
13
+ stepLabel: '(fr) %{itemLabel} %{stepNumber}',
14
+ stepTrackerLabel: '(fr) %{itemLabel} %{stepNumber} of %{stepCount}'
15
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './CarouselContext'
2
+ export { default as Carousel } from './Carousel'
@@ -9,7 +9,17 @@ import useInputSupports from './useInputSupports'
9
9
 
10
10
  const InputSupports = forwardRef(
11
11
  (
12
- { children, copy = 'en', label, hint, hintPosition = 'inline', feedback, tooltip, validation },
12
+ {
13
+ children,
14
+ copy = 'en',
15
+ label,
16
+ hint,
17
+ hintPosition = 'inline',
18
+ feedback,
19
+ tooltip,
20
+ validation,
21
+ nativeID
22
+ },
13
23
  ref
14
24
  ) => {
15
25
  const { space } = useThemeTokens('InputSupports')
@@ -18,7 +28,8 @@ const InputSupports = forwardRef(
18
28
  feedback,
19
29
  hint,
20
30
  label,
21
- validation
31
+ validation,
32
+ nativeID
22
33
  })
23
34
 
24
35
  return (
@@ -72,7 +83,11 @@ InputSupports.propTypes = {
72
83
  /**
73
84
  * Use to visually mark an input as valid or invalid.
74
85
  */
75
- validation: PropTypes.oneOf(['error', 'success'])
86
+ validation: PropTypes.oneOf(['error', 'success']),
87
+ /**
88
+ * ID for DOM element on web
89
+ */
90
+ nativeID: PropTypes.string
76
91
  }
77
92
 
78
93
  export default InputSupports
@@ -2,7 +2,7 @@ import useUniqueId from '../utils/useUniqueId'
2
2
 
3
3
  const joinDefined = (array) => array.filter((item) => item !== undefined).join(' ')
4
4
 
5
- const useInputSupports = ({ label, feedback, validation, hint }) => {
5
+ const useInputSupports = ({ label, feedback, validation, hint, nativeID }) => {
6
6
  const hasValidationError = validation === 'error'
7
7
 
8
8
  const inputId = useUniqueId('input')
@@ -20,7 +20,7 @@ const useInputSupports = ({ label, feedback, validation, hint }) => {
20
20
  }
21
21
 
22
22
  return {
23
- inputId,
23
+ inputId: nativeID || inputId,
24
24
  hintId,
25
25
  feedbackId,
26
26
  a11yProps
@@ -22,6 +22,7 @@ import {
22
22
  import { useViewport } from '../ViewportProvider'
23
23
  import IconButton from '../IconButton'
24
24
  import dictionary from './dictionary'
25
+ import useScrollBlocking from '../utils/useScrollBlocking'
25
26
 
26
27
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
27
28
 
@@ -89,6 +90,7 @@ const Modal = forwardRef(
89
90
  ({ children, isOpen, onClose, maxWidth, tokens, variant, copy, closeButton, ...rest }, ref) => {
90
91
  const viewport = useViewport()
91
92
  const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth })
93
+ const modalRef = useScrollBlocking(isOpen)
92
94
 
93
95
  const { closeIcon: CloseIconComponent } = themeTokens
94
96
 
@@ -113,7 +115,7 @@ const Modal = forwardRef(
113
115
 
114
116
  return (
115
117
  <NativeModal transparent {...selectProps(rest)}>
116
- <View style={[staticStyles.positioningContainer]}>
118
+ <View style={[staticStyles.positioningContainer]} ref={modalRef}>
117
119
  <View
118
120
  style={[staticStyles.sizingContainer, selectContainerStyles(themeTokens)]}
119
121
  pointerEvents="box-none" // don't capture backdrop press events
@@ -22,6 +22,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
22
22
  const selectSkeletonStyles = ({ color, radius, fadeAnimation }) => ({
23
23
  backgroundColor: color,
24
24
  borderRadius: radius,
25
+ maxWidth: '100%',
25
26
  ...fadeAnimation
26
27
  })
27
28
 
@@ -95,10 +95,17 @@ const StepTracker = forwardRef(
95
95
  }
96
96
  )
97
97
  const getCopy = useCopy({ dictionary, copy })
98
- const stepTrackerLabel = getCopy('stepTrackerLabel')
99
- .replace('%{stepNumber}', current < steps.length ? current + 1 : steps.length)
100
- .replace('%{stepCount}', steps.length)
101
- .replace('%{stepLabel}', current < steps.length ? steps[current] : steps[steps.length - 1])
98
+ const stepTrackerLabel = showStepTrackerLabel
99
+ ? getCopy('stepTrackerLabel')
100
+ .replace('%{stepNumber}', current < steps.length ? current + 1 : steps.length)
101
+ .replace('%{stepCount}', steps.length)
102
+ .replace(
103
+ '%{stepLabel}',
104
+ current < steps.length ? steps[current] : steps[steps.length - 1]
105
+ )
106
+ : ''
107
+ const getStepLabel = (index) =>
108
+ themeTokens.showStepLabel ? getCopy('stepLabel').replace('%{stepNumber}', index + 1) : ''
102
109
  if (!steps.length) return null
103
110
  const selectedProps = selectProps({
104
111
  accessibilityLabel: stepTrackerLabel,
@@ -123,7 +130,7 @@ const StepTracker = forwardRef(
123
130
  status={current}
124
131
  key={label}
125
132
  label={label}
126
- name={getCopy('stepLabel').replace('%{stepNumber}', index + 1)}
133
+ name={getStepLabel(index)}
127
134
  stepIndex={index}
128
135
  stepCount={steps.length}
129
136
  tokens={themeTokens}
@@ -148,13 +155,19 @@ const StepTracker = forwardRef(
148
155
  )
149
156
  StepTracker.displayName = 'StepTracker'
150
157
 
158
+ // If a language dictionary entry is provided, it must contain every key
159
+ const dictionaryContentShape = PropTypes.shape({
160
+ stepLabel: PropTypes.string.isRequired,
161
+ stepTrackerLabel: PropTypes.string.isRequired
162
+ })
163
+
151
164
  StepTracker.propTypes = {
152
165
  ...selectedSystemPropTypes,
153
166
  current: PropTypes.number,
154
- copy: PropTypes.oneOf(['en', 'fr']),
167
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
155
168
  dictionary: PropTypes.shape({
156
- en: PropTypes.shape({ stepLabel: PropTypes.string, stepTrackerLabel: PropTypes.string }),
157
- fr: PropTypes.shape({ stepLabel: PropTypes.string, stepTrackerLabel: PropTypes.string })
169
+ en: dictionaryContentShape,
170
+ fr: dictionaryContentShape
158
171
  }),
159
172
  steps: PropTypes.arrayOf(PropTypes.string),
160
173
  tokens: getTokensPropType('StepTracker'),
@@ -51,7 +51,7 @@ const TextInput = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
51
51
  }
52
52
 
53
53
  return (
54
- <InputSupports {...supportsProps}>
54
+ <InputSupports nativeID={selectedProps.nativeID} {...supportsProps}>
55
55
  {({ inputId, ...props }) => (
56
56
  <TextInputBase ref={ref} {...inputProps} nativeID={inputId} {...props} />
57
57
  )}
package/src/index.js CHANGED
@@ -3,6 +3,7 @@ export { default as ActivityIndicator } from './ActivityIndicator'
3
3
  export { default as Box } from './Box'
4
4
  export * from './Button'
5
5
  export { default as Card, PressableCardBase } from './Card'
6
+ export * from './Carousel'
6
7
  export { default as Checkbox } from './Checkbox'
7
8
  export * from './Checkbox'
8
9
  export { default as Divider } from './Divider'
@@ -16,6 +17,7 @@ export { default as Icon } from './Icon'
16
17
  export * from './Icon'
17
18
  export { default as IconButton } from './IconButton'
18
19
  export { default as InputLabel } from './InputLabel'
20
+ export { default as InputSupports } from './InputSupports'
19
21
  export * from './Link'
20
22
  export { default as List, ListItem, ListBase } from './List'
21
23
  export { default as Modal } from './Modal'
@@ -10,6 +10,7 @@ export { default as useCopy } from './useCopy'
10
10
  export { default as useHash } from './useHash'
11
11
  export { default as useSpacingScale } from './useSpacingScale'
12
12
  export { default as useResponsiveProp } from './useResponsiveProp'
13
+ export { default as useScrollBlocking } from './useScrollBlocking'
13
14
  export * from './useResponsiveProp'
14
15
  export { default as useUniqueId } from './useUniqueId'
15
16
  export { default as withLinkRouter } from './withLinkRouter'
@@ -2,8 +2,8 @@ import PropTypes from 'prop-types'
2
2
 
3
3
  const clickHandlerMapping = {
4
4
  onClick: 'onPress',
5
- mouseDown: 'onPressIn',
6
- mouseUp: 'onPressOut'
5
+ onMouseDown: 'onPressIn',
6
+ onMouseUp: 'onPressOut'
7
7
  }
8
8
 
9
9
  export default {
@@ -1,6 +1,8 @@
1
1
  import PropTypes from 'prop-types'
2
+ import { Platform } from 'react-native'
3
+ import getPropSelector from './getPropSelector'
2
4
 
3
- export const focusHandlerProps = {
5
+ const focusHandlerProps = {
4
6
  types: {
5
7
  /**
6
8
  * onBlur handler
@@ -10,14 +12,11 @@ export const focusHandlerProps = {
10
12
  * onFocus handler
11
13
  */
12
14
  onFocus: PropTypes.func
13
- },
14
- select: ({ onBlur, onFocus }) => ({
15
- onBlur,
16
- onFocus
17
- })
15
+ }
18
16
  }
17
+ focusHandlerProps.select = getPropSelector(focusHandlerProps.types)
19
18
 
20
- export const textInputHandlerProps = {
19
+ const textInputHandlerProps = {
21
20
  types: {
22
21
  /**
23
22
  * onChange handler
@@ -34,14 +33,63 @@ export const textInputHandlerProps = {
34
33
  /**
35
34
  * onSubmitEditing handler
36
35
  */
37
- onSubmitEditing: PropTypes.func
38
- },
39
- select: ({ onChange, onChangeText, onSubmit, onSubmitEditing }) => ({
40
- onChange,
41
- onChangeText,
42
- onSubmit,
43
- onSubmitEditing
44
- })
36
+ onSubmitEditing: PropTypes.func,
37
+ /**
38
+ * onContentSizeChange handler
39
+ */
40
+ onContentSizeChange: PropTypes.func,
41
+ /**
42
+ * onEndEditing handler
43
+ */
44
+ onEndEditing: PropTypes.func,
45
+ /**
46
+ * onScroll handler
47
+ */
48
+ onScroll: PropTypes.func,
49
+ /**
50
+ * onSelectionChange handler
51
+ */
52
+ onSelectionChange: PropTypes.func,
53
+ /**
54
+ * onKeyPress handler
55
+ */
56
+ onKeyPress: PropTypes.func,
57
+ /**
58
+ * onKeyUp handler (only supported on Web)
59
+ */
60
+ onKeyUp: PropTypes.func,
61
+ /**
62
+ * onKeyDown handler (only supported on Web)
63
+ */
64
+ onKeyDown: PropTypes.func
65
+ }
66
+ }
67
+ const selectTextInputHandlers = getPropSelector(textInputHandlerProps.types)
68
+ textInputHandlerProps.select = (props) => {
69
+ // Support for onKeyPress/onKeyUp/onKeyDown is inconsistent between React Native and React Native Web
70
+ const { onKeyPress, onKeyUp, onKeyDown, ...resolvedProps } = selectTextInputHandlers(props)
71
+ if (onKeyPress || onKeyUp || onKeyDown) {
72
+ if (Platform.OS !== 'web') {
73
+ // React Native only supports onKeyPress. Call any key handlers supplied in expected order.
74
+ resolvedProps.onKeyPress = (event) => {
75
+ if (typeof onKeyDown === 'function') onKeyDown(event)
76
+ if (typeof onKeyPress === 'function') onKeyPress(event)
77
+ if (typeof onKeyUp === 'function') onKeyUp(event)
78
+ }
79
+ } else {
80
+ // React Native Web supports onKeyUp the normal way.
81
+ if (onKeyUp) resolvedProps.onKeyUp = onKeyUp
82
+ // React Native Web doesn't support the `onKeyDown` prop name, but maps a supplied onKeyPress handler
83
+ // to the onKeyDown event and calls it with a keydown event. Make React Native Web call either or both.
84
+ if (onKeyPress || onKeyDown) {
85
+ resolvedProps.onKeyPress = (event) => {
86
+ if (typeof onKeyDown === 'function') onKeyDown(event)
87
+ if (typeof onKeyPress === 'function') onKeyPress(event)
88
+ }
89
+ }
90
+ }
91
+ }
92
+ return resolvedProps
45
93
  }
46
94
 
47
- export default { focusHandlerProps, textInputHandlerProps }
95
+ export { focusHandlerProps, textInputHandlerProps }
@@ -0,0 +1,57 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ const addScrollBlocking = (preventScrolling, stopPropagation, ref) => {
4
+ document.body.addEventListener('touchmove', preventScrolling, { passive: false })
5
+ ref.current?.addEventListener('touchmove', stopPropagation)
6
+ document.body.style.overflow = 'hidden'
7
+ }
8
+
9
+ const removeScrollBlocking = (preventScrolling, stopPropagation, ref) => {
10
+ document.body.removeEventListener('touchmove', preventScrolling)
11
+ ref.current?.removeEventListener('touchmove', stopPropagation)
12
+ document.body.style.overflow = 'inherit'
13
+ }
14
+
15
+ /**
16
+ * Disables scrolling when passed `true` or an array where all items are `true`.
17
+ *
18
+ * Returns an optional callback ref. Pass this to an element if it or its children
19
+ * should allow touch-based scrolling within that element's bounds.
20
+ *
21
+ * @param {boolean | boolean[]} conditionProps
22
+ * @returns
23
+ */
24
+ const useScrollBlocking = (conditionProps) => {
25
+ // useRef refs are null on first render and don't trigger a re-render when they get their
26
+ // element. Force re-run when ref mounts to ensure the stopPropagation listener is attached.
27
+ const ref = useRef()
28
+ const [refIsMounted, setRefIsMounted] = useState(false)
29
+ const callbackRef = useCallback((element) => {
30
+ ref.current = element
31
+ setRefIsMounted(Boolean(element))
32
+ }, [])
33
+
34
+ const conditionsMet = Array.isArray(conditionProps)
35
+ ? conditionProps.every((condition) => condition)
36
+ : Boolean(conditionProps)
37
+
38
+ const preventScrolling = useCallback((event) => event.preventDefault(), [])
39
+ const stopPropagation = useCallback((event) => event.stopPropagation(), [])
40
+
41
+ useEffect(() => {
42
+ const cleanup = () => removeScrollBlocking(preventScrolling, stopPropagation, ref)
43
+
44
+ if (conditionsMet) {
45
+ addScrollBlocking(preventScrolling, stopPropagation, ref)
46
+ } else {
47
+ cleanup()
48
+ }
49
+ return cleanup
50
+ // preventScrolling and stopPropagation are stable callbacks with no deps, so this
51
+ // will re-run when conditionsMet or refIsMounted flip between true and false.
52
+ }, [preventScrolling, conditionsMet, stopPropagation, refIsMounted])
53
+
54
+ return callbackRef
55
+ }
56
+
57
+ export default useScrollBlocking
@@ -0,0 +1,2 @@
1
+ // This is a no-op to emphasize that the original hook is web-only
2
+ export default () => {}