@telus-uds/components-base 1.14.2 → 1.16.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 (161) hide show
  1. package/CHANGELOG.md +40 -2
  2. package/__tests17__/A11yText/A11yText.test.jsx +34 -0
  3. package/__tests17__/ActivityIndicator/ActivityIndicator.test.jsx +68 -0
  4. package/__tests17__/ActivityIndicator/__snapshots__/ActivityIndicator.test.jsx.snap +299 -0
  5. package/__tests17__/Box/Box.test.jsx +111 -0
  6. package/__tests17__/Button/Button.test.jsx +86 -0
  7. package/__tests17__/Button/ButtonBase.test.jsx +82 -0
  8. package/__tests17__/Button/ButtonGroup.test.jsx +347 -0
  9. package/__tests17__/Button/ButtonLink.test.jsx +61 -0
  10. package/__tests17__/Card/Card.test.jsx +63 -0
  11. package/__tests17__/Carousel/Carousel.test.jsx +128 -0
  12. package/__tests17__/Carousel/CarouselTabs.test.jsx +142 -0
  13. package/__tests17__/Checkbox/Checkbox.test.jsx +94 -0
  14. package/__tests17__/Checkbox/CheckboxGroup.test.jsx +246 -0
  15. package/__tests17__/Divider/Divider.test.jsx +91 -0
  16. package/__tests17__/ExpandCollapse/ExpandCollapse.test.jsx +109 -0
  17. package/__tests17__/Feedback/Feedback.test.jsx +42 -0
  18. package/__tests17__/FlexGrid/Col.test.jsx +261 -0
  19. package/__tests17__/FlexGrid/FlexGrid.test.jsx +136 -0
  20. package/__tests17__/FlexGrid/Row.test.jsx +273 -0
  21. package/__tests17__/HorizontalScroll/HorizontalScroll.test.jsx +165 -0
  22. package/__tests17__/Icon/Icon.test.jsx +61 -0
  23. package/__tests17__/IconButton/IconButton.test.jsx +52 -0
  24. package/__tests17__/InputLabel/InputLabel.test.jsx +28 -0
  25. package/__tests17__/InputLabel/__snapshots__/InputLabel.test.jsx.snap +3 -0
  26. package/__tests17__/InputSupports/InputSupports.test.jsx +60 -0
  27. package/__tests17__/Link/Link.test.jsx +63 -0
  28. package/__tests17__/Link/TextButton.test.jsx +35 -0
  29. package/__tests17__/List/List.test.jsx +82 -0
  30. package/__tests17__/Modal/Modal.test.jsx +47 -0
  31. package/__tests17__/Notification/Notification.test.jsx +20 -0
  32. package/__tests17__/Pagination/Pagination.test.jsx +160 -0
  33. package/__tests17__/Progress/Progress.test.jsx +79 -0
  34. package/__tests17__/Radio/Radio.test.jsx +87 -0
  35. package/__tests17__/Radio/RadioGroup.test.jsx +220 -0
  36. package/__tests17__/RadioCard/RadioCard.test.jsx +87 -0
  37. package/__tests17__/RadioCard/RadioCardGroup.test.jsx +246 -0
  38. package/__tests17__/Search/Search.test.jsx +87 -0
  39. package/__tests17__/Select/Select.test.jsx +94 -0
  40. package/__tests17__/SideNav/SideNav.test.jsx +110 -0
  41. package/__tests17__/Skeleton/Skeleton.test.jsx +61 -0
  42. package/__tests17__/SkipLink/SkipLink.test.jsx +61 -0
  43. package/__tests17__/Spacer/Spacer.test.jsx +63 -0
  44. package/__tests17__/StackView/StackView.test.jsx +211 -0
  45. package/__tests17__/StackView/StackWrap.test.jsx +47 -0
  46. package/__tests17__/StackView/getStackedContent.test.jsx +295 -0
  47. package/__tests17__/StepTracker/StepTracker.test.jsx +108 -0
  48. package/__tests17__/Tabs/Tabs.test.jsx +49 -0
  49. package/__tests17__/Tags/Tags.test.jsx +327 -0
  50. package/__tests17__/TextInput/TextArea.test.jsx +35 -0
  51. package/__tests17__/TextInput/TextInputBase.test.jsx +125 -0
  52. package/__tests17__/ThemeProvider/ThemeProvider.test.jsx +80 -0
  53. package/__tests17__/ThemeProvider/useThemeTokens.test.jsx +514 -0
  54. package/__tests17__/ThemeProvider/utils/theme-tokens.test.js +41 -0
  55. package/__tests17__/ToggleSwitch/ToggleSwitch.test.jsx +82 -0
  56. package/__tests17__/ToggleSwitch/ToggleSwitchGroup.test.jsx +192 -0
  57. package/__tests17__/Tooltip/Tooltip.test.jsx +65 -0
  58. package/__tests17__/Tooltip/getTooltipPosition.test.js +79 -0
  59. package/__tests17__/Typography/typography.test.jsx +90 -0
  60. package/__tests17__/utils/children.test.jsx +128 -0
  61. package/__tests17__/utils/containUniqueFields.test.js +25 -0
  62. package/__tests17__/utils/input.test.js +375 -0
  63. package/__tests17__/utils/props.test.js +36 -0
  64. package/__tests17__/utils/semantics.test.jsx +34 -0
  65. package/__tests17__/utils/useCopy.test.js +42 -0
  66. package/__tests17__/utils/useResponsiveProp.test.jsx +202 -0
  67. package/__tests17__/utils/useSpacingScale.test.jsx +273 -0
  68. package/__tests17__/utils/useUniqueId.test.js +31 -0
  69. package/component-docs.json +120 -85
  70. package/lib/A11yInfoProvider/index.js +14 -5
  71. package/lib/Button/ButtonGroup.js +3 -2
  72. package/lib/Carousel/Carousel.js +18 -2
  73. package/lib/Carousel/CarouselTabs/CarouselTabs.js +6 -7
  74. package/lib/Checkbox/Checkbox.js +9 -6
  75. package/lib/ExpandCollapse/Control.js +6 -5
  76. package/lib/ExpandCollapse/Panel.js +5 -4
  77. package/lib/List/ListItem.js +10 -236
  78. package/lib/List/ListItemBase.js +162 -0
  79. package/lib/List/ListItemContent.js +85 -0
  80. package/lib/List/ListItemMark.js +158 -0
  81. package/lib/List/PressableListItemBase.js +147 -0
  82. package/lib/Notification/Notification.js +2 -1
  83. package/lib/Pagination/Pagination.js +4 -3
  84. package/lib/Radio/Radio.js +9 -6
  85. package/lib/RadioCard/RadioCard.js +9 -6
  86. package/lib/Select/Select.js +1 -0
  87. package/lib/Skeleton/Skeleton.js +18 -13
  88. package/lib/Skeleton/useSkeletonNativeAnimation.js +4 -2
  89. package/lib/Tabs/Tabs.js +12 -3
  90. package/lib/Tags/Tags.js +3 -3
  91. package/lib/TextInput/TextInput.js +5 -4
  92. package/lib/ToggleSwitch/ToggleSwitch.js +24 -19
  93. package/lib/ViewportProvider/useViewportListener.js +11 -5
  94. package/lib/utils/hasOwnProperty.js +18 -0
  95. package/lib/utils/props/a11yProps.js +171 -1
  96. package/lib/utils/props/getPropSelector.js +47 -5
  97. package/lib/utils/ssr.js +116 -1
  98. package/lib/utils/useResponsiveProp.js +5 -3
  99. package/lib/utils/withLinkRouter.js +3 -5
  100. package/lib-module/A11yInfoProvider/index.js +14 -4
  101. package/lib-module/Button/ButtonGroup.js +3 -2
  102. package/lib-module/Carousel/Carousel.js +16 -2
  103. package/lib-module/Carousel/CarouselTabs/CarouselTabs.js +7 -6
  104. package/lib-module/Checkbox/Checkbox.js +9 -6
  105. package/lib-module/ExpandCollapse/Control.js +6 -5
  106. package/lib-module/ExpandCollapse/Panel.js +5 -4
  107. package/lib-module/List/ListItem.js +13 -235
  108. package/lib-module/List/ListItemBase.js +139 -0
  109. package/lib-module/List/ListItemContent.js +66 -0
  110. package/lib-module/List/ListItemMark.js +143 -0
  111. package/lib-module/List/PressableListItemBase.js +117 -0
  112. package/lib-module/Notification/Notification.js +2 -1
  113. package/lib-module/Pagination/Pagination.js +5 -3
  114. package/lib-module/Radio/Radio.js +9 -6
  115. package/lib-module/RadioCard/RadioCard.js +9 -6
  116. package/lib-module/Select/Select.js +1 -0
  117. package/lib-module/Skeleton/Skeleton.js +15 -13
  118. package/lib-module/Skeleton/useSkeletonNativeAnimation.js +3 -2
  119. package/lib-module/Tabs/Tabs.js +13 -4
  120. package/lib-module/Tags/Tags.js +3 -3
  121. package/lib-module/TextInput/TextInput.js +5 -4
  122. package/lib-module/ToggleSwitch/ToggleSwitch.js +24 -19
  123. package/lib-module/ViewportProvider/useViewportListener.js +10 -4
  124. package/lib-module/utils/hasOwnProperty.js +11 -0
  125. package/lib-module/utils/props/a11yProps.js +169 -1
  126. package/lib-module/utils/props/getPropSelector.js +44 -5
  127. package/lib-module/utils/ssr.js +106 -0
  128. package/lib-module/utils/useResponsiveProp.js +3 -4
  129. package/lib-module/utils/withLinkRouter.js +3 -5
  130. package/package.json +12 -17
  131. package/src/A11yInfoProvider/index.jsx +20 -4
  132. package/src/Button/ButtonGroup.jsx +4 -2
  133. package/src/Carousel/Carousel.jsx +15 -2
  134. package/src/Carousel/CarouselTabs/CarouselTabs.jsx +5 -3
  135. package/src/Checkbox/Checkbox.jsx +7 -3
  136. package/src/ExpandCollapse/Control.jsx +8 -5
  137. package/src/ExpandCollapse/Panel.jsx +7 -5
  138. package/src/List/ListItem.jsx +12 -191
  139. package/src/List/ListItemBase.jsx +118 -0
  140. package/src/List/ListItemContent.jsx +52 -0
  141. package/src/List/ListItemMark.jsx +99 -0
  142. package/src/List/PressableListItemBase.jsx +102 -0
  143. package/src/Notification/Notification.jsx +1 -1
  144. package/src/Pagination/Pagination.jsx +6 -1
  145. package/src/Radio/Radio.jsx +7 -3
  146. package/src/RadioCard/RadioCard.jsx +7 -3
  147. package/src/Select/Select.jsx +1 -1
  148. package/src/Skeleton/Skeleton.jsx +25 -19
  149. package/src/Skeleton/useSkeletonNativeAnimation.js +3 -3
  150. package/src/Tabs/Tabs.jsx +19 -2
  151. package/src/Tags/Tags.jsx +3 -3
  152. package/src/TextInput/TextInput.jsx +4 -4
  153. package/src/ToggleSwitch/ToggleSwitch.jsx +3 -3
  154. package/src/ViewportProvider/useViewportListener.js +10 -5
  155. package/src/utils/hasOwnProperty.js +11 -0
  156. package/src/utils/props/a11yProps.js +107 -1
  157. package/src/utils/props/getPropSelector.js +45 -4
  158. package/src/utils/ssr.jsx +124 -0
  159. package/src/utils/useResponsiveProp.js +3 -3
  160. package/src/utils/withLinkRouter.jsx +1 -3
  161. package/src/utils/ssr.js +0 -35
@@ -98,6 +98,12 @@ const RadioCard = forwardRef(
98
98
  const getCardTokens = (cardState) => selectPressableCardTokens(getTokens(cardState))
99
99
  const { themeOptions } = useTheme()
100
100
 
101
+ const selectedProps = selectProps({
102
+ accessibilityRole: 'radio',
103
+ accessibilityState: { checked: isChecked, disabled: inactive },
104
+ ...rest
105
+ })
106
+
101
107
  return (
102
108
  <PressableCardBase
103
109
  ref={ref}
@@ -105,9 +111,7 @@ const RadioCard = forwardRef(
105
111
  checked={isChecked}
106
112
  tokens={getCardTokens}
107
113
  onPress={handleChange}
108
- accessibilityRole="radio"
109
- accessibilityState={{ checked: isChecked, disabled: inactive }}
110
- {...selectProps(rest)}
114
+ {...selectedProps}
111
115
  >
112
116
  {(cardState) => {
113
117
  const { radioSpace, contentSpace, ...themeTokens } = getTokens(cardState)
@@ -215,7 +215,7 @@ const Select = forwardRef(
215
215
  const { themeOptions } = useTheme()
216
216
 
217
217
  return (
218
- <InputSupports {...supportsProps} {...selectedProps}>
218
+ <InputSupports {...supportsProps} {...selectedProps} validation={validation}>
219
219
  {({ inputId, ...props }) => (
220
220
  <View style={selectOuterBorderStyles(themeTokens)}>
221
221
  <Picker
@@ -1,5 +1,5 @@
1
1
  import React, { forwardRef } from 'react'
2
- import { Animated, Platform } from 'react-native'
2
+ import { Animated, Platform, StyleSheet, View } from 'react-native'
3
3
  import propTypes from 'prop-types'
4
4
  import StackView from '../StackView'
5
5
  import { useThemeTokens } from '../ThemeProvider'
@@ -19,11 +19,10 @@ import skeletonWebAnimation from './skeletonWebAnimation'
19
19
 
20
20
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
21
21
 
22
- const selectSkeletonStyles = ({ color, radius, fadeAnimation }) => ({
22
+ const selectSkeletonStyles = ({ color, radius }) => ({
23
23
  backgroundColor: color,
24
24
  borderRadius: radius,
25
- maxWidth: '100%',
26
- ...fadeAnimation
25
+ maxWidth: '100%'
27
26
  })
28
27
 
29
28
  const selectLineStyles = ({ skeletonHeight, lineWidth }) => ({
@@ -66,12 +65,13 @@ const Skeleton = forwardRef(
66
65
  const skeletonHeight = useSpacingScale(spacingScaleValue)
67
66
  const nativeAnimation = useSkeletonNativeAnimation()
68
67
 
69
- const getAnimationBaseOnPlatform = () => {
70
- if (Platform.OS !== 'web') {
71
- return nativeAnimation
72
- }
68
+ const getAnimationBasedOnPlatform = () => {
69
+ const animation = Platform.OS === 'web' ? skeletonWebAnimation : nativeAnimation
70
+ // We must pass the animation styles through `StyleSheet.create`
71
+ // @see https://github.com/necolas/react-native-web/issues/2387
72
+ const styles = StyleSheet.create({ animation })
73
73
 
74
- return skeletonWebAnimation
74
+ return styles.animation
75
75
  }
76
76
 
77
77
  const getLineWidth = () => {
@@ -97,16 +97,22 @@ const Skeleton = forwardRef(
97
97
  return selectLineStyles({ skeletonHeight, lineWidth: getLineWidth() })
98
98
  }
99
99
 
100
- const renderSkeleton = (index = 0) => (
101
- <Animated.View
102
- testID="skeleton"
103
- key={`skeleton-${index + 1}`}
104
- style={[
105
- selectSkeletonStyles({ ...themeTokens, fadeAnimation: getAnimationBaseOnPlatform() }),
106
- getStyledBasedOnShape()
107
- ]}
108
- />
109
- )
100
+ const renderSkeleton = (index = 0) => {
101
+ // @see https://github.com/necolas/react-native-web/issues/2387
102
+ const Component = Platform.OS === 'web' ? View : Animated.View
103
+
104
+ return (
105
+ <Component
106
+ testID="skeleton"
107
+ key={`skeleton-${index + 1}`}
108
+ style={[
109
+ selectSkeletonStyles(themeTokens),
110
+ getAnimationBasedOnPlatform(),
111
+ getStyledBasedOnShape()
112
+ ]}
113
+ />
114
+ )
115
+ }
110
116
 
111
117
  return (
112
118
  <StackView space={themeTokens.spaceBetweenLines} ref={ref} {...selectProps(rest)}>
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'react'
2
- import { Animated } from 'react-native'
2
+ import { Animated, Platform } from 'react-native'
3
3
  import { ANIMATION_DURATION, DEFAULT_OPACITY, OPACITY_STOP } from './skeleton.constant'
4
4
 
5
5
  const useSkeletonNativeAnimation = () => {
@@ -10,12 +10,12 @@ const useSkeletonNativeAnimation = () => {
10
10
  Animated.timing(fadeAnimation, {
11
11
  toValue: OPACITY_STOP,
12
12
  duration: ANIMATION_DURATION,
13
- useNativeDriver: true
13
+ useNativeDriver: Platform.OS !== 'web'
14
14
  }),
15
15
  Animated.timing(fadeAnimation, {
16
16
  toValue: DEFAULT_OPACITY,
17
17
  duration: ANIMATION_DURATION,
18
- useNativeDriver: true
18
+ useNativeDriver: Platform.OS !== 'web'
19
19
  })
20
20
  ])
21
21
  Animated.loop(fade).start()
package/src/Tabs/Tabs.jsx CHANGED
@@ -6,6 +6,8 @@ import StackView from '../StackView'
6
6
  import {
7
7
  a11yProps,
8
8
  getTokensPropType,
9
+ focusHandlerProps,
10
+ pressProps,
9
11
  selectSystemProps,
10
12
  useHash,
11
13
  useInputValue,
@@ -20,6 +22,12 @@ import HorizontalScroll, {
20
22
  import TabsItem from './TabsItem'
21
23
 
22
24
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
25
+ const [selectItemProps, selectedItemPropTypes] = selectSystemProps([
26
+ a11yProps,
27
+ focusHandlerProps,
28
+ pressProps,
29
+ viewProps
30
+ ])
23
31
 
24
32
  const { selectHorizontalScrollTokens, useItemPositions } = horizontalScrollUtils
25
33
 
@@ -99,15 +107,22 @@ const Tabs = forwardRef(
99
107
  label,
100
108
  id,
101
109
  accessibilityRole = defaultTabItemAccessibiltyRole,
110
+ onPress,
102
111
  ref: itemRef,
103
112
  LinkRouter: ItemLinkRouter = LinkRouter,
104
- linkRouterProps: itemLinkRouterProps
113
+ linkRouterProps: itemLinkRouterProps,
114
+ ...itemRest
105
115
  },
106
116
  index
107
117
  ) => {
108
118
  const itemId = id ?? label
109
119
  const isSelected = Boolean(currentValue && currentValue === itemId)
110
- const handlePress = (event) => setValue(itemId, event)
120
+ const handlePress = (event) => {
121
+ if (typeof onPress === 'function') onPress(event)
122
+ setValue(itemId, event)
123
+ }
124
+
125
+ const itemProps = selectItemProps(itemRest)
111
126
  return (
112
127
  <TabsItem
113
128
  ref={itemRef}
@@ -122,6 +137,7 @@ const Tabs = forwardRef(
122
137
  accessibilityRole={accessibilityRole}
123
138
  LinkRouter={ItemLinkRouter}
124
139
  linkRouterProps={{ ...linkRouterProps, ...itemLinkRouterProps }}
140
+ {...itemProps}
125
141
  >
126
142
  {label}
127
143
  </TabsItem>
@@ -140,6 +156,7 @@ Tabs.propTypes = {
140
156
  ...withLinkRouter.PropTypes,
141
157
  items: PropTypes.arrayOf(
142
158
  PropTypes.shape({
159
+ ...selectedItemPropTypes,
143
160
  ...withLinkRouter.PropTypes,
144
161
  href: PropTypes.string,
145
162
  label: PropTypes.string,
package/src/Tags/Tags.jsx CHANGED
@@ -127,12 +127,12 @@ const Tags = forwardRef(
127
127
  toggleOneValue(id, event)
128
128
  }
129
129
 
130
- const itemProps = {
130
+ const itemProps = selectItemProps({
131
131
  accessibilityState: { checked: isSelected },
132
132
  accessibilityRole: itemA11yRole,
133
133
  ...a11yProps.getPositionInSet(items.length, index),
134
- ...selectItemProps(itemRest)
135
- }
134
+ ...itemRest
135
+ })
136
136
 
137
137
  return (
138
138
  <ButtonBase
@@ -41,7 +41,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
41
41
  * supported props and <a href="https://reactnative.dev/docs/textinput" target="_blank">React Native Web documentation</a> for
42
42
  * their implementation on the web.
43
43
  */
44
- const TextInput = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
44
+ const TextInput = forwardRef(({ tokens, nativeID, variant = {}, ...rest }, ref) => {
45
45
  const { supportsProps, ...selectedProps } = selectProps(rest)
46
46
 
47
47
  const inputProps = {
@@ -51,9 +51,9 @@ const TextInput = forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
51
51
  }
52
52
 
53
53
  return (
54
- <InputSupports nativeID={selectedProps.nativeID} {...supportsProps}>
55
- {({ inputId, ...props }) => (
56
- <TextInputBase ref={ref} {...inputProps} nativeID={inputId} {...props} />
54
+ <InputSupports nativeID={nativeID} {...supportsProps}>
55
+ {({ inputId, ...propsFromInputSupports }) => (
56
+ <TextInputBase ref={ref} nativeID={inputId} {...propsFromInputSupports} {...inputProps} />
57
57
  )}
58
58
  </InputSupports>
59
59
  )
@@ -26,7 +26,9 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
26
26
  viewProps
27
27
  ])
28
28
 
29
- const selectButtonTokens = (tokens) =>
29
+ // We need to drop the icon before passing it to the `ButtonBase`, because it's
30
+ // being handled separately in this case
31
+ const selectButtonTokens = ({ icon: _, ...tokens }) =>
30
32
  selectTokens('Button', {
31
33
  ...tokens,
32
34
  // Width tokens are applied to our inner track. Disable Button width token so it wraps our track width.
@@ -215,8 +217,6 @@ ToggleSwitch.propTypes = {
215
217
 
216
218
  const staticStyles = StyleSheet.create({
217
219
  track: {
218
- flexGrow: 1,
219
- alignSelf: 'stretch',
220
220
  flexDirection: 'row'
221
221
  },
222
222
  switch: {
@@ -28,11 +28,16 @@ const useViewportListenerCSR = (setViewport) => {
28
28
  const onChange = ({ window }) => setViewport(viewports.select(window.width))
29
29
  const listener = Dimensions.addEventListener('change', onChange)
30
30
 
31
- // From RN 0.65.0, Dimensions.removeEventListener is deprecated for `remove` on addEventListener return value;
32
- // however, that is not available in RN <=0.64.X, therefore not in any Expo release as of 2021 (Expo SDK 43).
33
- return listener?.remove || (() => Dimensions.removeEventListener('change', onChange))
34
-
35
- // setViewport is a function from `useState` so it is stable and won't make the effect re-run
31
+ return () => {
32
+ if (typeof listener?.remove === 'function') {
33
+ // Can't just return listener.remove because listener.emitter disappears, causing an internal error.
34
+ // See https://github.com/facebook/react-native/issues/34508
35
+ listener.remove()
36
+ // From RN 0.65.0, Dimensions.removeEventListener is deprecated for `remove` on addEventListener return value
37
+ } else if (typeof Dimensions.removeEventListener === 'function') {
38
+ Dimensions.removeEventListener('change', onChange)
39
+ }
40
+ }
36
41
  }, [setViewport])
37
42
  }
38
43
 
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Linter disallows object instance prototype methods like someObject.hasOwnProperty(key),
3
+ * but we can use this instead.
4
+ *
5
+ * @param {object} object
6
+ * @param {String} key
7
+ * @returns {Boolean}
8
+ */
9
+ export default function hasOwnProperty(object, key) {
10
+ return Object.prototype.hasOwnProperty.call(object, key)
11
+ }
@@ -98,6 +98,101 @@ const a11yPropTypes = {
98
98
  accessibilityValueText: PropTypes.string
99
99
  }
100
100
 
101
+ const a11yPropTypesByPlatform = Platform.select({
102
+ // React Native Web adds many a11y props that alias aria-* attributes
103
+ // Types based on https://necolas.github.io/react-native-web/docs/accessibility/
104
+ web: a11yPropTypes,
105
+ // Ignore web-only props in native builds
106
+ default: nativeA11yPropTypes
107
+ })
108
+
109
+ // These RNW-only props only exist in RNW >=0.18. Catch them and map them according to platform
110
+ // so all props work on RN, RNW >=0.18 and RNW <=0.18, regardless of which they were written for:
111
+ // - On native, bundle them into objects, like `accessibilityValue: { max: 100 }`
112
+ // - On web, split them into both of:
113
+ // - The appropriate aria-* attr, like `aria-valuenow`, which will work regardless of RNW version
114
+ // - The corresponding RNW >=0.18 prop, like `accessibilityValueNow`, which in some cases does more
115
+ // than just add the aria-* (e.g. `accessibilityDisabled` adds `disabled` if element supports it,
116
+ // and future releases might add more features here).
117
+ const rwnPropMappings = {
118
+ // Former accessibilityValue props.
119
+ accessibilityValueMax: (value) =>
120
+ Platform.select({
121
+ web: { 'aria-valuemax': value, accessibilityValueMax: value },
122
+ default: { accessibilityValue: { max: value } }
123
+ }),
124
+ accessibilityValueMin: (value) =>
125
+ Platform.select({
126
+ web: { 'aria-valuemin': value, accessibilityValueMin: value },
127
+ default: { accessibilityValue: { min: value } }
128
+ }),
129
+ accessibilityValueNow: (value) =>
130
+ Platform.select({
131
+ web: { 'aria-valuenow': value, accessibilityValueNow: value },
132
+ default: { accessibilityValue: { now: value } }
133
+ }),
134
+ accessibilityValueText: (value) =>
135
+ Platform.select({
136
+ web: { 'aria-valuetext': value, accessibilityValueText: value },
137
+ default: { accessibilityValue: { text: value } }
138
+ }),
139
+
140
+ // Former accessibilityState props
141
+ accessibilityBusy: (value) =>
142
+ Platform.select({
143
+ web: { 'aria-busy': value, accessibilityBusy: value },
144
+ default: { accessibilityState: { busy: value } }
145
+ }),
146
+ accessibilityChecked: (value) =>
147
+ Platform.select({
148
+ web: { 'aria-checked': value, accessibilityChecked: value },
149
+ default: { accessibilityState: { checked: value } }
150
+ }),
151
+ accessibilityDisabled: (value) =>
152
+ Platform.select({
153
+ web: {
154
+ 'aria-disabled': value,
155
+ // RNW >= 0.18 maps `accessibilityDisabled` to `disabled` attr if element supports it
156
+ accessibilityDisabled: value,
157
+ // As of RNW 0.18.9, Pressable doesn't support `accessibilityDisabled`, only `disabled`,
158
+ // but everything else supports `accessibilityDisabled` but not `disabled`.
159
+ disabled: value
160
+ },
161
+ default: { accessibilityState: { disabled: value } }
162
+ }),
163
+ accessibilityExpanded: (value) =>
164
+ Platform.select({
165
+ web: { 'aria-expanded': value, accessibilityExpanded: value },
166
+ default: { accessibilityState: { expanded: value } }
167
+ }),
168
+ accessibilitySelected: (value) =>
169
+ Platform.select({
170
+ web: { 'aria-selected': value, accessibilitySelected: value },
171
+ default: { accessibilityState: { selected: value } }
172
+ })
173
+ }
174
+ if (Platform.OS === 'web') {
175
+ const mapIfDefined = (value, fn) => (value === undefined ? undefined : fn(value))
176
+
177
+ // On Web only, these React Native object props need manual mapping in RNW >=0.18
178
+ // which dropped support for the React Native shape of these props.
179
+ // Re-use our RNW 0.18 prop mappings to support both RNW <0.18 (aria-*) and
180
+ // new features added in >=0.18 (e.g. for accessibilityDisabled).
181
+ rwnPropMappings.accessibilityValue = ({ max, min, now, text } = {}) => ({
182
+ ...mapIfDefined(max, rwnPropMappings.accessibilityValueMax),
183
+ ...mapIfDefined(min, rwnPropMappings.accessibilityValueMin),
184
+ ...mapIfDefined(now, rwnPropMappings.accessibilityValueNow),
185
+ ...mapIfDefined(text, rwnPropMappings.accessibilityValueText)
186
+ })
187
+ rwnPropMappings.accessibilityState = ({ busy, checked, disabled, expanded, selected } = {}) => ({
188
+ ...mapIfDefined(busy, rwnPropMappings.accessibilityBusy),
189
+ ...mapIfDefined(checked, rwnPropMappings.accessibilityChecked),
190
+ ...mapIfDefined(disabled, rwnPropMappings.accessibilityDisabled),
191
+ ...mapIfDefined(expanded, rwnPropMappings.accessibilityExpanded),
192
+ ...mapIfDefined(selected, rwnPropMappings.accessibilitySelected)
193
+ })
194
+ }
195
+
101
196
  export default {
102
197
  /**
103
198
  * Proptypes for recognised React Native accessiblity (a11y) props.
@@ -110,7 +205,18 @@ export default {
110
205
  * Where components accept React Native a11y props, pass { ...rest } from its props to this,
111
206
  * then spread the returned object into the component's props (usually its outer container).
112
207
  */
113
- select: getPropSelector(a11yPropTypes, /^aria-/),
208
+ select: getPropSelector(
209
+ // Allow all React Native accessibility props
210
+ a11yPropTypesByPlatform,
211
+ // Allow any `aria-*` attribute on web; ignore them on native
212
+ Platform.OS === 'web' && /^aria-/,
213
+ // For the props added and deprecated in React Native Web 0.18, convert them to
214
+ // a form that is platform-appropriate and RNW-version safe
215
+ (key, value) => {
216
+ const rnwPropMapper = rwnPropMappings[key]
217
+ return rnwPropMapper ? rnwPropMapper(value) : undefined
218
+ }
219
+ ),
114
220
  /**
115
221
  * Use this to disable focus for elements which are visually hidden but still rendered.
116
222
  */
@@ -1,14 +1,55 @@
1
- export default function getPropSelector(propTypes, regexp) {
2
- const keys = Object.keys(propTypes)
1
+ import merge from 'lodash.merge'
2
+ import hasOwnProperty from '../hasOwnProperty'
3
+
4
+ /**
5
+ * @callback PropSelectorCallback - a callback called for each prop passed to a component
6
+ * @param {string} key - the key for the prop to be tested
7
+ * @param {*} value - the value of the prop being passed in to the component
8
+ * @returns {object|undefined}
9
+ */
10
+
11
+ /**
12
+ * @param {PropSelectorCallback} callback
13
+ * @param {object} items
14
+ * @param {string} key
15
+ * @param {*} value
16
+ * @returns {object|undefined}
17
+ */
18
+ const applyCallback = (callback, items, key, value) => {
19
+ // If there's no callback, continue and look up keys as normal
20
+ if (typeof callback !== 'function') return undefined
21
+
22
+ const newItems = callback(key, value)
23
+
24
+ // If the callback doesn't return anything, continue and look up keys as normal
25
+ if (!newItems) return undefined
26
+
27
+ // If the callback returns items, merge them in, deep merging props that are objects
28
+ return merge({}, items, newItems)
29
+ }
30
+
31
+ /**
32
+ * Generates a function to filter an object of props down to a subset of allowed props, with
33
+ * optional prop alteration and re-mapping via an optional callback.
34
+ *
35
+ * @param {object} propTypes - an object where every defined key is a valid prop
36
+ * @param {*} [regexp] - an optional regular expression where any match is a valid prop
37
+ * @param {PropSelectorCallback} callback - optional function taking `(key, value)` returning either undefined or an object of new props to merge in
38
+ * @returns {object} - valid props for this component
39
+ */
40
+ export default function getPropSelector(propTypes, regexp, callback) {
3
41
  return (props) =>
4
42
  Object.entries(props).reduce(
5
43
  (items, [key, value]) =>
6
- keys.includes(key) || (regexp && regexp.test(key))
44
+ // If there's a callback and it matches something, applyCallback merges it in; return that
45
+ applyCallback(callback, items, key, value) ||
46
+ // If there's no callback match, check if this prop is valid and merge it in if it is
47
+ (hasOwnProperty(propTypes, key) || (regexp && regexp.test(key))
7
48
  ? {
8
49
  ...items,
9
50
  [key]: value
10
51
  }
11
- : items,
52
+ : items),
12
53
  {}
13
54
  )
14
55
  }
@@ -0,0 +1,124 @@
1
+ import React from 'react'
2
+ import { AppRegistry } from 'react-native'
3
+ /** @typedef {import('react').ComponentType} ReactComponent */
4
+ /** @typedef {import('react').ReactElement} ReactElement */
5
+
6
+ /**
7
+ * Returns object with `renderApp` and `getStyles` functions.
8
+ * Weave these into your app's server-side render process:
9
+ *
10
+ * - Call `renderApp` first to do the actual server-side render
11
+ * - After the render is complete, call `getStyles`
12
+ * - Include the style tags returned by `getStyles` in the SSR <head>
13
+ *
14
+ * @param {string} [appName] - optional unique identifier if ssrStyles is called multiple times for multiple apps
15
+ * @param {object} [options] -
16
+ * - `styleGetters`: optional array of additional style getter functions to call after render
17
+ * - `collectStyles`: optional function, takes the rendered app, returns either the same or a new app element
18
+ * @param {boolean} [options.styleGetters]
19
+ * @param {(ReactElement) => ReactElement} [options.collectStyles]
20
+ */
21
+ export const ssrStyles = (appName = 'UDS app', { styleGetters = [], collectStyles } = {}) => {
22
+ let hasAppRendered = false
23
+ return {
24
+ /**
25
+ * Server-side-renders the provided app in a way that supports collecting
26
+ * styles for Styled Components and React Native Web stylesheets.
27
+ *
28
+ * @param {ReactComponent} App - the root component for the app
29
+ * @param {object} [props] - props for this render e.g. page routing props
30
+ * @param {object} [options] -
31
+ * - `renderedByRNW`: pass as true if the main render is by AppRegistry.runApplication from React Native
32
+ * - `WrapperComponent`: Component rendered with no props around app content and inside root tag
33
+ * @param {boolean} [options.renderedByRNW]
34
+ * @param {ReactComponent} [options.WrapperComponent]
35
+ *
36
+ * @returns {ReactElement} - the rendered app output
37
+ */
38
+ renderApp: (App, props, { renderedByRNW = false, WrapperComponent } = {}) => {
39
+ AppRegistry.registerComponent(appName, () => App)
40
+ // AppRegistry.getApplication renders the app in a container, and collects styles.
41
+ const { element, getStyleElement } = AppRegistry.getApplication(appName, {
42
+ WrapperComponent,
43
+ initialProps: props
44
+ })
45
+
46
+ let renderedApp =
47
+ // React Native Web's AppRegistry.getApplication assumes the app is rendered using
48
+ // AppRegistry.runApplication and wraps it in <AppContainer>, which wraps the entire app
49
+ // in two outer containers resembling <View style={{ flex: 1 }} pointerEvents="box-none">.
50
+ // So, use that IF user says AppRegistry.runApplication will do the client-side render.
51
+ (renderedByRNW && element) ||
52
+ // If the live app is not rendered using AppRegistry.runApplication, we need to
53
+ // re-render it without the <AppContainer> wrapper, to avoid SSR mismatch errors.
54
+ // Default to this as many platforms (e.g. NextJS) will use their own renderers.
55
+ WrapperComponent ? (
56
+ <WrapperComponent>
57
+ <App {...props} />
58
+ </WrapperComponent>
59
+ ) : (
60
+ <App {...props} />
61
+ )
62
+
63
+ const getRNWStyle = () => getStyleElement({ key: 'react-native-stylesheet' })
64
+ styleGetters.push(getRNWStyle)
65
+
66
+ if (typeof collectStyles === 'function') {
67
+ renderedApp = collectStyles(renderedApp)
68
+ }
69
+ hasAppRendered = true
70
+ return renderedApp
71
+ },
72
+ /**
73
+ * Turns styles collected during renderApp into an array of React elements of
74
+ * HTML <style> tags ready for insertion into the SSR HTML's <head>.
75
+ *
76
+ * Must be called after `renderApp` has completed.
77
+ *
78
+ * @param {...ReactElement} existingStyles - any existing style tag elements to merge in
79
+ * @returns {ReactElement[]} - flat array of <style> React elements
80
+ */
81
+ getStyles: (...existingStyles) => {
82
+ if (!hasAppRendered) throw new Error('Called getStyles before renderApp in ssrStyles')
83
+ return [...existingStyles, ...styleGetters.flatMap((getter) => getter())]
84
+ }
85
+ }
86
+ }
87
+
88
+ export default ssrStyles
89
+
90
+ /**
91
+ * @deprecated - use ssrStyles instead
92
+ *
93
+ * Registers the app's root component with React Native Web and generates
94
+ * the main <style> tag containing React Native Web stylesheet styles.
95
+ *
96
+ * @param {ReactComponent} AppRoot
97
+ * @param {string} [appName]
98
+ * @returns {ReactElement[]}
99
+ */
100
+
101
+ export const getReactNativeWebSSRStyles = (AppRoot, appName = 'app') => {
102
+ AppRegistry.registerComponent(appName, () => AppRoot)
103
+ const { getStyleElement } = AppRegistry.getApplication(appName)
104
+ return [getStyleElement()]
105
+ }
106
+
107
+ /**
108
+ * @deprecated - use ssrStyles instead
109
+ *
110
+ * Gets style tags for each currently supported CSS-in-JS library and returns
111
+ * them alongside any existing style tags.
112
+ *
113
+ * @param {ReactComponent} AppRoot
114
+ * @param {string} [appName]
115
+ * @param {ReactElement[]} [existingStyles]
116
+ * @returns {ReactElement[]}
117
+ */
118
+ export const getSSRStyles = (AppRoot, appName = 'app', existingStyles = []) => {
119
+ return [
120
+ ...existingStyles,
121
+ ...getReactNativeWebSSRStyles(AppRoot, appName)
122
+ // if any other CSS-in-JS is added e.g. styled-components generate and add its styles here
123
+ ]
124
+ }
@@ -1,9 +1,9 @@
1
1
  import { viewports } from '@telus-uds/system-constants'
2
2
  import { useViewport } from '../ViewportProvider'
3
+ import hasOwnProperty from './hasOwnProperty'
3
4
 
4
- const hasOwn = (objectProp, key) => Object.prototype.hasOwnProperty.call(objectProp, key)
5
5
  const hasResponsiveProperties = (objectProp) =>
6
- viewports.keys.some((key) => hasOwn(objectProp, key))
6
+ viewports.keys.some((key) => hasOwnProperty(objectProp, key))
7
7
 
8
8
  /**
9
9
  * Resolves a prop which may be a responsive object with keys for viewports.
@@ -23,7 +23,7 @@ export const resolveResponsiveProp = (prop, viewport, defaultValue) => {
23
23
  ? // If there's a current viewport, return the closest match at or below it
24
24
  viewports.inherit(prop)[viewport]
25
25
  : // If no current viewport is available, default to smallest viewport
26
- prop[viewports.keys.find((key) => hasOwn(prop, key))]
26
+ prop[viewports.keys.find((key) => hasOwnProperty(prop, key))]
27
27
 
28
28
  return value === undefined ? defaultValue : value
29
29
  }
@@ -1,8 +1,6 @@
1
1
  import React, { forwardRef } from 'react'
2
2
  import PropTypes from 'prop-types'
3
-
4
- // Prototype-safe alternative to (linter-forbidden) someObject.hasOwnProperty()
5
- const hasOwnProperty = (object, prop) => Object.prototype.hasOwnProperty.call(object, prop)
3
+ import hasOwnProperty from './hasOwnProperty'
6
4
 
7
5
  /**
8
6
  * Higher-order component that has no effect unless an additional prop `LinkRouter` is passed.
package/src/utils/ssr.js DELETED
@@ -1,35 +0,0 @@
1
- import { AppRegistry } from 'react-native'
2
- /** @typedef {import('react').ComponentType} ReactComponent */
3
- /** @typedef {import('react').ReactElement} ReactElement */
4
-
5
- /**
6
- * Registers the app's root component with React Native Web and generates
7
- * the main <style> tag containing React Native Web stylesheet styles.
8
- *
9
- * @param {ReactComponent} AppRoot
10
- * @param {string} [appName]
11
- * @returns {ReactElement[]}
12
- */
13
-
14
- export const getReactNativeWebSSRStyles = (AppRoot, appName = 'app') => {
15
- AppRegistry.registerComponent(appName, () => AppRoot)
16
- const { getStyleElement } = AppRegistry.getApplication(appName)
17
- return [getStyleElement()]
18
- }
19
-
20
- /**
21
- * Gets style tags for each currently supported CSS-in-JS library and returns
22
- * them alongside any existing style tags.
23
- *
24
- * @param {ReactComponent} AppRoot
25
- * @param {string} [appName]
26
- * @param {ReactElement[]} [existingStyles]
27
- * @returns {ReactElement[]}
28
- */
29
- export const getSSRStyles = (AppRoot, appName = 'app', existingStyles = []) => {
30
- return [
31
- ...existingStyles,
32
- ...getReactNativeWebSSRStyles(AppRoot, appName)
33
- // if any other CSS-in-JS is added e.g. styled-components generate and add its styles here
34
- ]
35
- }