@telus-uds/components-base 1.8.5 → 1.11.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 (69) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/component-docs.json +666 -27
  3. package/lib/Card/Card.js +9 -4
  4. package/lib/Carousel/Carousel.js +672 -0
  5. package/lib/Carousel/CarouselContext.js +59 -0
  6. package/lib/Carousel/CarouselItem/CarouselItem.js +92 -0
  7. package/lib/Carousel/CarouselItem/index.js +13 -0
  8. package/lib/Carousel/dictionary.js +23 -0
  9. package/lib/Carousel/index.js +32 -0
  10. package/lib/ExpandCollapse/Panel.js +10 -1
  11. package/lib/InputSupports/InputSupports.js +10 -3
  12. package/lib/InputSupports/useInputSupports.js +3 -2
  13. package/lib/Modal/Modal.js +4 -0
  14. package/lib/Skeleton/Skeleton.js +1 -0
  15. package/lib/StepTracker/StepTracker.js +15 -12
  16. package/lib/TextInput/TextInput.js +3 -12
  17. package/lib/TextInput/TextInputBase.js +9 -0
  18. package/lib/TextInput/propTypes.js +3 -8
  19. package/lib/index.js +23 -0
  20. package/lib/utils/props/clickProps.js +2 -2
  21. package/lib/utils/props/handlerProps.js +77 -31
  22. package/lib/utils/props/textInputProps.js +8 -1
  23. package/lib/utils/useScrollBlocking.js +66 -0
  24. package/lib/utils/useScrollBlocking.native.js +11 -0
  25. package/lib-module/Card/Card.js +5 -4
  26. package/lib-module/Carousel/Carousel.js +617 -0
  27. package/lib-module/Carousel/CarouselContext.js +43 -0
  28. package/lib-module/Carousel/CarouselItem/CarouselItem.js +75 -0
  29. package/lib-module/Carousel/CarouselItem/index.js +2 -0
  30. package/lib-module/Carousel/dictionary.js +16 -0
  31. package/lib-module/Carousel/index.js +2 -0
  32. package/lib-module/ExpandCollapse/Panel.js +9 -1
  33. package/lib-module/InputSupports/InputSupports.js +10 -3
  34. package/lib-module/InputSupports/useInputSupports.js +3 -2
  35. package/lib-module/Modal/Modal.js +3 -0
  36. package/lib-module/Skeleton/Skeleton.js +1 -0
  37. package/lib-module/StepTracker/StepTracker.js +14 -12
  38. package/lib-module/TextInput/TextInput.js +3 -9
  39. package/lib-module/TextInput/TextInputBase.js +10 -1
  40. package/lib-module/TextInput/propTypes.js +4 -8
  41. package/lib-module/index.js +2 -0
  42. package/lib-module/utils/props/clickProps.js +2 -2
  43. package/lib-module/utils/props/handlerProps.js +78 -31
  44. package/lib-module/utils/props/textInputProps.js +8 -1
  45. package/lib-module/utils/useScrollBlocking.js +58 -0
  46. package/lib-module/utils/useScrollBlocking.native.js +2 -0
  47. package/package.json +3 -3
  48. package/src/Card/Card.jsx +6 -4
  49. package/src/Carousel/Carousel.jsx +649 -0
  50. package/src/Carousel/CarouselContext.jsx +30 -0
  51. package/src/Carousel/CarouselItem/CarouselItem.jsx +66 -0
  52. package/src/Carousel/CarouselItem/index.js +3 -0
  53. package/src/Carousel/dictionary.js +16 -0
  54. package/src/Carousel/index.js +2 -0
  55. package/src/ExpandCollapse/Panel.jsx +8 -1
  56. package/src/InputSupports/InputSupports.jsx +18 -3
  57. package/src/InputSupports/useInputSupports.js +2 -2
  58. package/src/Modal/Modal.jsx +3 -1
  59. package/src/Skeleton/Skeleton.jsx +1 -0
  60. package/src/StepTracker/StepTracker.jsx +21 -8
  61. package/src/TextInput/TextInput.jsx +2 -9
  62. package/src/TextInput/TextInputBase.jsx +11 -1
  63. package/src/TextInput/propTypes.js +3 -7
  64. package/src/index.js +2 -0
  65. package/src/utils/props/clickProps.js +2 -2
  66. package/src/utils/props/handlerProps.js +64 -16
  67. package/src/utils/props/textInputProps.js +7 -1
  68. package/src/utils/useScrollBlocking.js +57 -0
  69. 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'
@@ -1,6 +1,7 @@
1
1
  import React, { forwardRef, useState } from 'react'
2
2
  import { Animated, Platform, View } from 'react-native'
3
3
  import PropTypes from 'prop-types'
4
+ import ABBPropTypes from 'airbnb-prop-types'
4
5
 
5
6
  import ExpandCollapseControl from './Control'
6
7
  import { useThemeTokens } from '../ThemeProvider'
@@ -51,6 +52,7 @@ const ExpandCollapsePanel = forwardRef(
51
52
  children,
52
53
  tokens,
53
54
  variant,
55
+ controlRef,
54
56
  ...rest
55
57
  },
56
58
  ref
@@ -95,6 +97,7 @@ const ExpandCollapsePanel = forwardRef(
95
97
  isExpanded={isExpanded}
96
98
  tokens={controlTokens}
97
99
  onPress={handleControlPress}
100
+ ref={controlRef}
98
101
  >
99
102
  {control}
100
103
  </ExpandCollapseControl>
@@ -142,7 +145,11 @@ ExpandCollapsePanel.propTypes = {
142
145
  /**
143
146
  * Optional theme token overrides that may be passed to the ExpandCollapseControl element.
144
147
  */
145
- controlTokens: getTokensPropType('ExpandCollapseControl')
148
+ controlTokens: getTokensPropType('ExpandCollapseControl'),
149
+ /**
150
+ * An optional ref to be attached to the control
151
+ */
152
+ controlRef: ABBPropTypes.ref()
146
153
  }
147
154
 
148
155
  export default ExpandCollapsePanel
@@ -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'),
@@ -1,5 +1,4 @@
1
1
  import React, { forwardRef } from 'react'
2
- import { Platform } from 'react-native'
3
2
  import {
4
3
  a11yProps,
5
4
  focusHandlerProps,
@@ -42,13 +41,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
42
41
  * supported props and <a href="https://reactnative.dev/docs/textinput" target="_blank">React Native Web documentation</a> for
43
42
  * their implementation on the web.
44
43
  */
45
- const TextInput = forwardRef(({ tokens, variant = {}, pattern, ...rest }, ref) => {
46
- React.useEffect(() => {
47
- if (Platform.OS === 'web' && pattern && ref.current) {
48
- // eslint-disable-next-line no-param-reassign
49
- ref.current.pattern = pattern
50
- }
51
- }, [ref, pattern])
44
+ const TextInput = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
52
45
  const { supportsProps, ...selectedProps } = selectProps(rest)
53
46
 
54
47
  const inputProps = {
@@ -58,7 +51,7 @@ const TextInput = forwardRef(({ tokens, variant = {}, pattern, ...rest }, ref) =
58
51
  }
59
52
 
60
53
  return (
61
- <InputSupports {...supportsProps}>
54
+ <InputSupports nativeID={selectedProps.nativeID} {...supportsProps}>
62
55
  {({ inputId, ...props }) => (
63
56
  <TextInputBase ref={ref} {...inputProps} nativeID={inputId} {...props} />
64
57
  )}
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useState } from 'react'
1
+ import React, { forwardRef, useEffect, useState } from 'react'
2
2
  import { Platform, StyleSheet, TextInput as NativeTextInput, View } from 'react-native'
3
3
 
4
4
  import PropTypes from 'prop-types'
@@ -128,6 +128,7 @@ const TextInputBase = forwardRef(
128
128
  onBlur,
129
129
  onMouseOver,
130
130
  onMouseOut,
131
+ pattern,
131
132
  tokens,
132
133
  variant = {},
133
134
  ...rest
@@ -161,6 +162,15 @@ const TextInputBase = forwardRef(
161
162
  readOnly
162
163
  })
163
164
 
165
+ const element = ref?.current
166
+ useEffect(() => {
167
+ if (Platform.OS === 'web' && pattern && element) {
168
+ // React Native Web doesn't support `pattern`, so we have to attach it via a ref,
169
+ // which a `pattern` user must provide anyway to call .checkValidity() on the element.
170
+ element.pattern = pattern
171
+ }
172
+ }, [element, pattern])
173
+
164
174
  const handleChangeText = (event) => {
165
175
  const text = event.nativeEvent?.text || event.target?.value
166
176
  setValue(text, event)
@@ -1,6 +1,7 @@
1
1
  import PropTypes from 'prop-types'
2
- import { Platform } from 'react-native'
3
2
 
3
+ // These are prop types specific to UDS TextInput; see also ../utils/props/textInputProps
4
+ // for generic React Native props and HTML input attrs that are passed through.
4
5
  const textInputPropTypes = {
5
6
  /**
6
7
  * If the input's state is to be controlled by a parent component, use this prop
@@ -24,12 +25,7 @@ const textInputPropTypes = {
24
25
  * Use to react upon input's value changes. Required when the `value` prop is set.
25
26
  * Will receive the input's value as an argument.
26
27
  */
27
- onChange: PropTypes.func,
28
- ...Platform.select({
29
- web: {
30
- pattern: PropTypes.string
31
- }
32
- })
28
+ onChange: PropTypes.func
33
29
  }
34
30
 
35
31
  export default textInputPropTypes
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'
@@ -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 }
@@ -121,7 +121,13 @@ const crossPlatform = {
121
121
  const webOnly = {
122
122
  disabled: PropTypes.bool,
123
123
  dir: PropTypes.oneOf(['auto', 'ltr', 'rtl']),
124
- lang: PropTypes.string
124
+ lang: PropTypes.string,
125
+ /**
126
+ * Sets the HTML input `pattern` attr. Not supported by React Native Web, but is supported by UDS.
127
+ * Must also pass in a ref and check validity by calling the HTML element's checkValidity method:
128
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/checkValidity
129
+ */
130
+ pattern: PropTypes.string
125
131
  }
126
132
 
127
133
  /**
@@ -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 () => {}