@telus-uds/components-base 0.0.2-prerelease.1 → 0.0.2-prerelease.5

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 +36 -0
  2. package/__fixtures__/testTheme.js +264 -84
  3. package/__tests__/Box/Box.test.jsx +81 -58
  4. package/__tests__/Card/Card.test.jsx +63 -0
  5. package/__tests__/Divider/Divider.test.jsx +26 -5
  6. package/__tests__/Feedback/Feedback.test.jsx +42 -0
  7. package/__tests__/FlexGrid/Col.test.jsx +5 -0
  8. package/__tests__/Pagination/Pagination.test.jsx +160 -0
  9. package/__tests__/Spacer/Spacer.test.jsx +63 -0
  10. package/__tests__/StackView/StackView.test.jsx +242 -0
  11. package/__tests__/StackView/StackWrap.test.jsx +47 -0
  12. package/__tests__/StackView/getStackedContent.test.jsx +295 -0
  13. package/__tests__/TextInput/TextInput.test.jsx +146 -0
  14. package/__tests__/ThemeProvider/useThemeTokens.test.jsx +5 -3
  15. package/__tests__/utils/spacing.test.jsx +273 -0
  16. package/__tests__/utils/useUniqueId.test.js +31 -0
  17. package/babel.config.json +8 -0
  18. package/jest.config.js +7 -6
  19. package/lib/A11yInfoProvider/index.js +2 -2
  20. package/lib/A11yText/index.js +1 -3
  21. package/lib/ActivityIndicator/Spinner.web.js +3 -5
  22. package/lib/Box/Box.js +117 -82
  23. package/lib/Button/Button.js +1 -3
  24. package/lib/Button/ButtonBase.js +9 -21
  25. package/lib/Button/ButtonGroup.js +14 -25
  26. package/lib/Button/ButtonLink.js +1 -3
  27. package/lib/Card/Card.js +103 -0
  28. package/lib/Card/index.js +2 -0
  29. package/lib/Divider/Divider.js +40 -4
  30. package/lib/ExpandCollapse/Accordion.js +1 -3
  31. package/lib/ExpandCollapse/Control.js +3 -5
  32. package/lib/ExpandCollapse/Panel.js +2 -4
  33. package/lib/Feedback/Feedback.js +110 -0
  34. package/lib/Feedback/index.js +2 -0
  35. package/lib/FlexGrid/Col/Col.js +3 -5
  36. package/lib/FlexGrid/FlexGrid.js +1 -3
  37. package/lib/FlexGrid/Row/Row.js +1 -3
  38. package/lib/FlexGrid/providers/GutterContext.js +1 -1
  39. package/lib/Icon/Icon.js +1 -1
  40. package/lib/InputLabel/InputLabel.js +86 -0
  41. package/lib/InputLabel/LabelContent.native.js +8 -0
  42. package/lib/InputLabel/LabelContent.web.js +17 -0
  43. package/lib/InputLabel/index.js +2 -0
  44. package/lib/Link/ChevronLink.js +1 -3
  45. package/lib/Link/Link.js +1 -3
  46. package/lib/Link/LinkBase.js +11 -7
  47. package/lib/Link/TextButton.js +1 -3
  48. package/lib/Pagination/PageButton.js +85 -0
  49. package/lib/Pagination/Pagination.js +118 -0
  50. package/lib/Pagination/SideButton.js +108 -0
  51. package/lib/Pagination/dictionary.js +18 -0
  52. package/lib/Pagination/index.js +2 -0
  53. package/lib/Pagination/useCopy.js +10 -0
  54. package/lib/Pagination/usePagination.js +70 -0
  55. package/lib/SideNav/Item.js +4 -6
  56. package/lib/SideNav/ItemsGroup.js +11 -11
  57. package/lib/SideNav/SideNav.js +2 -4
  58. package/lib/Spacer/Spacer.js +98 -0
  59. package/lib/Spacer/index.js +2 -0
  60. package/lib/StackView/StackView.js +105 -0
  61. package/lib/StackView/StackWrap.js +32 -0
  62. package/lib/StackView/StackWrap.native.js +3 -0
  63. package/lib/StackView/StackWrapBox.js +85 -0
  64. package/lib/StackView/StackWrapGap.js +45 -0
  65. package/lib/StackView/common.js +30 -0
  66. package/lib/StackView/getStackedContent.js +111 -0
  67. package/lib/StackView/index.js +5 -0
  68. package/lib/TextInput/TextInput.js +337 -0
  69. package/lib/TextInput/index.js +2 -0
  70. package/lib/ThemeProvider/ThemeProvider.js +2 -2
  71. package/lib/ThemeProvider/useThemeTokens.js +34 -6
  72. package/lib/ThemeProvider/utils/theme-tokens.js +37 -9
  73. package/lib/ToggleSwitch/ToggleSwitch.js +17 -47
  74. package/lib/Typography/Typography.js +1 -7
  75. package/lib/ViewportProvider/index.js +1 -1
  76. package/lib/index.js +8 -1
  77. package/lib/utils/index.js +2 -1
  78. package/lib/utils/input.js +3 -1
  79. package/lib/utils/propTypes.js +103 -8
  80. package/lib/utils/spacing/index.js +2 -0
  81. package/lib/utils/spacing/useSpacingScale.js +102 -0
  82. package/lib/utils/spacing/utils.js +32 -0
  83. package/lib/utils/useUniqueId.js +12 -0
  84. package/package.json +6 -9
  85. package/release-context.json +4 -4
  86. package/src/Box/Box.jsx +117 -80
  87. package/src/Button/ButtonBase.jsx +8 -21
  88. package/src/Button/ButtonGroup.jsx +13 -17
  89. package/src/Card/Card.jsx +101 -0
  90. package/src/Card/index.js +3 -0
  91. package/src/Divider/Divider.jsx +38 -3
  92. package/src/ExpandCollapse/Control.jsx +2 -3
  93. package/src/Feedback/Feedback.jsx +99 -0
  94. package/src/Feedback/index.js +3 -0
  95. package/src/FlexGrid/Col/Col.jsx +4 -2
  96. package/src/Icon/Icon.jsx +2 -1
  97. package/src/InputLabel/InputLabel.jsx +99 -0
  98. package/src/InputLabel/LabelContent.native.jsx +6 -0
  99. package/src/InputLabel/LabelContent.web.jsx +13 -0
  100. package/src/InputLabel/index.js +3 -0
  101. package/src/Link/LinkBase.jsx +9 -3
  102. package/src/Pagination/PageButton.jsx +80 -0
  103. package/src/Pagination/Pagination.jsx +135 -0
  104. package/src/Pagination/SideButton.jsx +93 -0
  105. package/src/Pagination/dictionary.js +18 -0
  106. package/src/Pagination/index.js +3 -0
  107. package/src/Pagination/useCopy.js +7 -0
  108. package/src/Pagination/usePagination.js +69 -0
  109. package/src/SideNav/Item.jsx +3 -3
  110. package/src/SideNav/ItemsGroup.jsx +11 -13
  111. package/src/Spacer/Spacer.jsx +91 -0
  112. package/src/Spacer/index.js +3 -0
  113. package/src/StackView/StackView.jsx +103 -0
  114. package/src/StackView/StackWrap.jsx +33 -0
  115. package/src/StackView/StackWrap.native.jsx +4 -0
  116. package/src/StackView/StackWrapBox.jsx +82 -0
  117. package/src/StackView/StackWrapGap.jsx +39 -0
  118. package/src/StackView/common.jsx +28 -0
  119. package/src/StackView/getStackedContent.jsx +106 -0
  120. package/src/StackView/index.js +6 -0
  121. package/src/TextInput/TextInput.jsx +325 -0
  122. package/src/TextInput/index.js +3 -0
  123. package/src/ThemeProvider/useThemeTokens.js +34 -7
  124. package/src/ThemeProvider/utils/theme-tokens.js +37 -8
  125. package/src/ToggleSwitch/ToggleSwitch.jsx +23 -43
  126. package/src/Typography/Typography.jsx +0 -4
  127. package/src/index.js +8 -1
  128. package/src/utils/index.js +1 -0
  129. package/src/utils/input.js +2 -1
  130. package/src/utils/propTypes.js +105 -16
  131. package/src/utils/spacing/index.js +3 -0
  132. package/src/utils/spacing/useSpacingScale.js +93 -0
  133. package/src/utils/spacing/utils.js +28 -0
  134. package/src/utils/useUniqueId.js +14 -0
  135. package/stories/A11yText/A11yText.stories.jsx +11 -5
  136. package/stories/ActivityIndicator/ActivityIndicator.stories.jsx +11 -2
  137. package/stories/Box/Box.stories.jsx +46 -17
  138. package/stories/Button/Button.stories.jsx +17 -21
  139. package/stories/Button/ButtonGroup.stories.jsx +2 -1
  140. package/stories/Button/ButtonLink.stories.jsx +6 -4
  141. package/stories/Card/Card.stories.jsx +62 -0
  142. package/stories/Divider/Divider.stories.jsx +26 -2
  143. package/stories/ExpandCollapse/ExpandCollapse.stories.jsx +74 -79
  144. package/stories/Feedback/Feedback.stories.jsx +97 -0
  145. package/stories/FlexGrid/01 FlexGrid.stories.jsx +20 -7
  146. package/stories/Icon/Icon.stories.jsx +11 -3
  147. package/stories/InputLabel/InputLabel.stories.jsx +37 -0
  148. package/stories/Link/ChevronLink.stories.jsx +20 -4
  149. package/stories/Link/Link.stories.jsx +24 -3
  150. package/stories/Link/TextButton.stories.jsx +24 -3
  151. package/stories/Pagination/Pagination.stories.jsx +64 -0
  152. package/stories/SideNav/SideNav.stories.jsx +17 -2
  153. package/stories/Spacer/Spacer.stories.jsx +33 -0
  154. package/stories/StackView/StackView.stories.jsx +65 -0
  155. package/stories/StackView/StackWrap.stories.jsx +52 -0
  156. package/stories/TextInput/TextInput.stories.jsx +103 -0
  157. package/stories/ToggleSwitch/ToggleSwitch.stories.jsx +16 -3
  158. package/stories/Typography/Typography.stories.jsx +12 -3
  159. package/stories/platform-supports.web.jsx +1 -1
  160. package/stories/supports.jsx +113 -13
  161. package/babel.config.js +0 -3
@@ -1,10 +1,21 @@
1
1
  import { useCallback } from 'react'
2
2
  import useTheme from './useTheme'
3
- import { getComponentTheme, getThemeTokens } from './utils'
3
+ import { getComponentTheme, getThemeTokens, resolveTokens, mergeAppearances } from './utils'
4
+ /**
5
+ * @typedef {import('../utils/propTypes.js').AppearanceSet} AppearanceSet
6
+ * @typedef {import('../utils/propTypes.js').TokensProp} TokensProp
7
+ * @typedef {import('../utils/propTypes.js').TokensSet} TokensSet
8
+ */
4
9
 
5
10
  /**
6
11
  * Returns a complete set of theme tokens for a component based on which of the
7
12
  * component's theme rules apply to the current set of theme appearances.
13
+ *
14
+ * @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
15
+ * @param {TokensProp} [tokens] - every themed component should accept an optional `tokens` prop allowing theme tokens to be overridden
16
+ * @param {AppearanceSet} [variants] - every themed component should accept an optional `variants` prop specifying theme variants
17
+ * @param {AppearanceSet} [states] - optional object containing current theme appearances dictated by user action or context
18
+ * @returns {TokensSet} - the currently-applicable resolved set of theme tokens to apply
8
19
  */
9
20
  export const useThemeTokens = (componentName, tokens = {}, variants = {}, states = {}) => {
10
21
  const theme = useTheme()
@@ -14,16 +25,32 @@ export const useThemeTokens = (componentName, tokens = {}, variants = {}, states
14
25
  }
15
26
 
16
27
  /**
17
- * Returns a memoised function that behaves the same as useThemeTokens hook,
18
- * allowing tokens to be obtained inside callbacks and such other places where
19
- * calling useThemeTokens directly would be disallowed by React's hook rules.
28
+ * Returns a memoised tokens getter function that gets tokens similar to calling useThemeTokens.
29
+ * Scenarios where useThemeTokensCallback should be used instead of useThemeTokens include:
30
+ *
31
+ * - Where tokens to be obtained from state accessible only in scopes like callbacks and render functions,
32
+ * where calling useThemeTokens directly would be disallowed by React's hook rules.
33
+ * - Passing a tokens getter down via a child component's `tokens` prop, applying rules using the
34
+ * child component's current state. Consider wrapping the returned tokens in `selectTokens()`.
35
+ *
36
+ * @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
37
+ * @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
38
+ * @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
39
+ * @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => TokensSet}
40
+ * - callback function that returning an overridable tokens set for current state. Only pass
41
+ * tokenOverrides in rare cases where tokens overrides are also generated outside hook scope,
42
+ * e.g. if one theme tokens callback needs to pass certain token overrides to another.
20
43
  */
21
- export const useThemeTokensCallback = (componentName) => {
44
+ export const useThemeTokensCallback = (componentName, tokens = {}, variants = {}) => {
22
45
  const theme = useTheme()
23
46
  const componentTheme = getComponentTheme(theme, componentName)
24
47
  const getThemeTokensCallback = useCallback(
25
- (tokens, variants, states) => getThemeTokens(componentTheme, tokens, variants, states),
26
- [componentTheme]
48
+ (states, tokenOverrides) => {
49
+ const appearances = mergeAppearances(variants, states)
50
+ const resolvedTokens = resolveTokens(tokens, tokenOverrides, appearances)
51
+ return getThemeTokens(componentTheme, resolvedTokens, appearances)
52
+ },
53
+ [componentTheme, tokens, variants]
27
54
  )
28
55
  return getThemeTokensCallback
29
56
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @typedef {import('../../utils/propTypes.js').AppearanceSet} AppearanceSet
3
+ * @typedef {import('../../utils/propTypes.js').TokensProp} TokensProp
4
+ * @typedef {import('../../utils/propTypes.js').TokensSet} TokensSet
5
+ */
6
+
1
7
  /**
2
8
  * General utilities around working with theme tokens
3
9
  */
@@ -30,24 +36,48 @@ export const doesThemeConditionApply = ([key, value], appearances) => {
30
36
  export const doesThemeRuleApply = (rule, appearances) =>
31
37
  Object.entries(rule.if).every((condition) => doesThemeConditionApply(condition, appearances))
32
38
 
33
- const mergeTokens = (tokensProp, themeTokens, appearances) => {
34
- const overrideTokens = typeof tokensProp === 'function' ? tokensProp(appearances) : tokensProp
35
- return Object.entries(overrideTokens).reduce(
39
+ /**
40
+ * Turns a tokens prop and an optional tokens override prop (either or both of which may be a tokens getter function)
41
+ * into one tokens set object where overrides are applied over the resolved default tokens.
42
+ *
43
+ * @param {TokensProp} defaultTokens - a set of tokens or tokens getter function which may be overridden
44
+ * @param {TokensProp} [tokenOverrides] - optional set of tokens or tokens getter function to override the default
45
+ * @param {AppearanceSet} [appearances] - optional appearance set to pass to tokens getter functions
46
+ * @returns {TokensSet} - object containing resolved tokens with overrides applied
47
+ */
48
+ export const resolveTokens = (defaultTokens, tokenOverrides, appearances = {}) => {
49
+ const resolve = (tokens) => (typeof tokens === 'function' ? tokens(appearances) : tokens)
50
+ if (!tokenOverrides) return resolve(defaultTokens)
51
+
52
+ return Object.entries(resolve(tokenOverrides)).reduce(
36
53
  (mergedTokens, [tokenName, tokenValue]) =>
37
54
  tokenValue === undefined ? mergedTokens : { ...mergedTokens, [tokenName]: tokenValue },
38
- themeTokens
55
+ resolve(defaultTokens)
39
56
  )
40
57
  }
41
58
 
59
+ /**
60
+ * Merges variants over states. Must be merged in that order to allow static showcases of a state,
61
+ * e.g. `<Button variant={{ pressed: true }} />` where button's pressed state is `false` by default.
62
+ * Returns an empty object if both variants and states are undefined.
63
+ *
64
+ * @param {AppearanceSet} [variants]
65
+ * @param {AppearanceSet} [states]
66
+ * @returns {AppearanceSet}
67
+ */
68
+ export const mergeAppearances = (variants = {}, states) =>
69
+ states ? { ...states, ...variants } : variants
70
+
42
71
  export const getThemeTokens = (
43
72
  { rules = [], tokens: defaultThemeTokens = {} },
44
73
  tokensProp,
45
74
  variants = {},
46
- states = {}
75
+ states
47
76
  ) => {
48
- const appearances = { ...states, ...variants }
77
+ const appearances = mergeAppearances(variants, states)
49
78
  // TODO: if in dev mode, validate the appearances and provided propTokens
50
79
 
80
+ // Get the theme's default tokens set and merge tokens from applicable theme rules over it
51
81
  const themeTokens = rules.reduce(
52
82
  (mergedTokens, rule) =>
53
83
  doesThemeRuleApply(rule, appearances)
@@ -58,8 +88,7 @@ export const getThemeTokens = (
58
88
  : mergedTokens,
59
89
  defaultThemeTokens
60
90
  )
61
-
62
- return tokensProp ? mergeTokens(tokensProp, themeTokens, appearances) : themeTokens
91
+ return resolveTokens(themeTokens, tokensProp, appearances)
63
92
  }
64
93
 
65
94
  export const toArray = (strOrArr) => (Array.isArray(strOrArr) ? strOrArr : [strOrArr])
@@ -4,43 +4,29 @@ import { Platform, View, StyleSheet } from 'react-native'
4
4
 
5
5
  import ButtonBase from '../Button/ButtonBase'
6
6
  import { useThemeTokensCallback, applyShadowToken } from '../ThemeProvider'
7
- import { a11yProps, pressProps, variantProp, getTokensPropType } from '../utils/propTypes'
7
+ import {
8
+ a11yProps,
9
+ pressProps,
10
+ variantProp,
11
+ getTokensPropType,
12
+ selectTokens
13
+ } from '../utils/propTypes'
8
14
  import { useInputValue } from '../utils/input'
9
15
 
10
- const selectButtonTokens = ({
11
- borderColor,
12
- borderWidth,
13
- borderRadius,
14
- outerBorderColor,
15
- outerBorderWidth,
16
- outerBorderGap,
17
- outerBorderRadius,
18
- outerBackgroundColor,
19
- backgroundColor,
20
- opacity,
21
- paddingLeft,
22
- paddingRight,
23
- paddingTop,
24
- paddingBottom,
25
- shadow
26
- }) => ({
27
- borderColor,
28
- borderWidth,
29
- borderRadius,
30
- outerBorderColor,
31
- outerBorderWidth,
32
- outerBorderGap,
33
- outerBorderRadius,
34
- outerBackgroundColor,
35
- backgroundColor,
36
- opacity,
37
- paddingLeft,
38
- paddingRight,
39
- paddingTop,
40
- paddingBottom,
41
- shadow,
42
- width: null // make it wrap around our track width
16
+ const selectButtonTokens = (tokens) =>
17
+ selectTokens('Button', {
18
+ ...tokens,
19
+ // Width tokens are applied to our inner track. Disable Button width token so it wraps our track width.
20
+ width: null
21
+ })
22
+
23
+ // Map and rename icon-specific tokens to name used within Icon
24
+ const selectIconTokens = ({ iconSize, iconColor, iconOpacity }) => ({
25
+ opacity: iconOpacity,
26
+ size: iconSize,
27
+ color: iconColor
43
28
  })
29
+
44
30
  const selectTrackStyles = ({ trackBorderWidth, trackBorderColor, trackBorderRadius, width }) => ({
45
31
  borderWidth: trackBorderWidth,
46
32
  borderColor: trackBorderColor,
@@ -68,11 +54,6 @@ const selectSwitchStyles = ({
68
54
  web: { transition: 'transform 200ms' }
69
55
  })
70
56
  })
71
- const selectIconTokens = ({ iconSize, iconColor, iconOpacity }) => ({
72
- opacity: iconOpacity,
73
- size: iconSize,
74
- color: iconColor
75
- })
76
57
 
77
58
  const ToggleSwitch = ({
78
59
  value,
@@ -83,7 +64,7 @@ const ToggleSwitch = ({
83
64
  variant,
84
65
  accessibilityRole = 'switch'
85
66
  }) => {
86
- const getTokens = useThemeTokensCallback('ToggleSwitch')
67
+ const getTokens = useThemeTokensCallback('ToggleSwitch', tokens, variant)
87
68
 
88
69
  const { currentValue, setValue } = useInputValue({
89
70
  value,
@@ -93,8 +74,7 @@ const ToggleSwitch = ({
93
74
 
94
75
  const handlePress = () => setValue(!currentValue)
95
76
 
96
- const getButtonTokens = (buttonState) =>
97
- selectButtonTokens(getTokens(tokens, variant, buttonState))
77
+ const getButtonTokens = (buttonState) => selectButtonTokens(getTokens(buttonState))
98
78
 
99
79
  return (
100
80
  <ButtonBase
@@ -106,7 +86,7 @@ const ToggleSwitch = ({
106
86
  onPress={handlePress}
107
87
  >
108
88
  {(buttonState) => {
109
- const themeTokens = getTokens(tokens, variant, buttonState)
89
+ const themeTokens = getTokens(buttonState)
110
90
  const IconComponent = themeTokens.icon
111
91
  const switchStyles = selectSwitchStyles(themeTokens)
112
92
  const trackStyles = selectTrackStyles(themeTokens)
@@ -33,8 +33,6 @@ const selectTextStyles = ({
33
33
  color,
34
34
  lineHeight,
35
35
  fontName,
36
- marginTop,
37
- marginBottom,
38
36
  textAlign,
39
37
  textTransform
40
38
  }) =>
@@ -44,8 +42,6 @@ const selectTextStyles = ({
44
42
  color,
45
43
  lineHeight,
46
44
  fontName,
47
- marginTop,
48
- marginBottom,
49
45
  textAlign,
50
46
  textTransform
51
47
  })
package/src/index.js CHANGED
@@ -1,19 +1,26 @@
1
1
  export { default as ActivityIndicator } from './ActivityIndicator'
2
2
  export { default as Box } from './Box'
3
3
  export * from './Button'
4
+ export { default as Card } from './Card'
4
5
  export { default as Divider } from './Divider'
5
6
  export { default as ExpandCollapse, Accordion } from './ExpandCollapse'
7
+ export { default as Feedback } from './Feedback'
6
8
  export { default as FlexGrid } from './FlexGrid'
7
9
  export { default as Icon } from './Icon'
8
10
  export * from './Icon'
9
11
  export * from './Link'
12
+ export { default as Pagination } from './Pagination'
10
13
  export { default as SideNav } from './SideNav'
14
+ export { default as Spacer } from './Spacer'
15
+ export { default as StackView } from './StackView'
16
+ export * from './StackView'
17
+ export { default as TextInput } from './TextInput'
11
18
  export { default as ToggleSwitch } from './ToggleSwitch'
12
19
  export { default as Typography } from './Typography'
13
20
 
14
21
  export { default as A11yInfoProvider, useA11yInfo } from './A11yInfoProvider'
15
22
  export { default as BaseProvider } from './BaseProvider'
16
23
  export { default as ViewportProvider, useViewport } from './ViewportProvider'
17
- export { default as ThemeProvider, useTheme, useSetTheme } from './ThemeProvider'
24
+ export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens } from './ThemeProvider'
18
25
 
19
26
  export * from './utils'
@@ -1,3 +1,4 @@
1
1
  export * from './animation'
2
2
  export * from './input'
3
3
  export * from './propTypes'
4
+ export * from './spacing'
@@ -55,6 +55,7 @@ Consumers of this hook must be one of:
55
55
  * currentValue: any
56
56
  * setValue: (value: any) => void
57
57
  * resetValue: () => void
58
+ * isControlled: bool
58
59
  * }}
59
60
  */
60
61
 
@@ -81,7 +82,7 @@ export const useInputValue = (props = {}, hookName = 'useInputValue') => {
81
82
  )
82
83
  const resetValue = useCallback(() => setValue(initializedValue), [initializedValue, setValue])
83
84
 
84
- return { currentValue, setValue, resetValue }
85
+ return { currentValue, setValue, resetValue, isControlled }
85
86
  }
86
87
 
87
88
  /**
@@ -2,6 +2,10 @@ import PropTypes from 'prop-types'
2
2
  import { Linking, Platform } from 'react-native'
3
3
  import { tokenKeys } from '@telus-uds/tools-theme'
4
4
 
5
+ /**
6
+ * @typedef {{[key: string]: string|number|boolean}} AppearanceSet
7
+ * @typedef {AppearanceSet} VariantProp
8
+ */
5
9
  export const variantProp = {
6
10
  /**
7
11
  * 'variant' is an optional object prop on all themable components.
@@ -17,7 +21,10 @@ export const variantProp = {
17
21
  )
18
22
  }
19
23
 
20
- const tokenValueType = PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
24
+ // Tokens can be primitive values (e.g. `'rgba(0,0,0,0'`, `12`), or objects of such values
25
+ // such as tokens that describe shadow (e.g. shadow: { inset: true, color: 'rgba(...)' ... })
26
+ const tokenValue = PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
27
+ const tokenValueType = PropTypes.oneOfType([tokenValue, PropTypes.objectOf(tokenValue)])
21
28
 
22
29
  const getTokenNames = (componentName) => {
23
30
  const componentTokenNames = tokenKeys[componentName]
@@ -40,15 +47,18 @@ export const selectTokens = (componentName, tokens) => {
40
47
  return filteredTokens
41
48
  }
42
49
 
50
+ /**
51
+ * @typedef {string|number|boolean|{[key: string]:string|number|boolean}} TokenValue
52
+ * @typedef {{[key: string]: TokenValue}} TokensSet
53
+ * @typedef {(AppearanceSet) => TokensSet} TokensGetter
54
+ * @typedef {TokensSet|TokensGetter} TokensProp
55
+ */
43
56
  /**
44
57
  * 'tokens' is an optional object prop on all themable components. Its keys must match the
45
58
  * token keys in the component's theme schema.
46
59
  *
47
60
  * This prop is intended to be used as an 'escape hatch' for difficult or exceptional cases
48
61
  * where the main theming system doesn't apply. It is intentionally permissive about values.
49
- *
50
- * Be careful about passing a token key with value `undefined`, as this will override any tokens from
51
- * the theme. If a key is set on a `tokens` prop object, tokens from the theme will be overridden.
52
62
  */
53
63
  export const getTokensPropType = (componentName) =>
54
64
  PropTypes.oneOfType([
@@ -251,19 +261,87 @@ export const linkProps = {
251
261
  }
252
262
  }
253
263
 
254
- export const levelsPropType = PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
264
+ const getByViewport = (propType) => ({
265
+ xs: propType,
266
+ sm: propType,
267
+ md: propType,
268
+ lg: propType,
269
+ xl: propType
270
+ })
271
+ /**
272
+ * Utilities for props that allow different values to be applied at different viewports.
273
+ *
274
+ * These should apply viewport inheritance such that if a viewport is undefined, the value is
275
+ * taken from the next smallest viewport for which a value is available. For example, a
276
+ * responsive prop { xs: 2, lg: 3 } should apply 2 at sizes sm and md, and 3 at size xl.
277
+ *
278
+ * @property {Function} getByViewport - returns an object where each each viewport has a key
279
+ * containing the provided argument.
280
+ * @property {Function} getTypeByViewport - returns a PropTypes shape validator for an object where
281
+ * each viewport has a key containing the provided proptype.
282
+ * @property {Function} getTypeOptionallyByViewport - returns a PropTypes validator that accepts
283
+ * either the provided proptype on its own, or the output of getTypeByViewport
284
+ */
285
+ export const responsiveProps = {
286
+ getByViewport,
287
+ getTypeByViewport: (propType) => PropTypes.shape(getByViewport(propType)),
288
+ getTypeOptionallyByViewport: (propType) =>
289
+ PropTypes.oneOfType([propType, PropTypes.shape(getByViewport(propType))])
290
+ }
255
291
 
256
- export const responsivePropTypeFactory = (propType) =>
257
- PropTypes.oneOfType([
258
- propType,
259
- PropTypes.shape({
260
- xs: propType,
261
- sm: propType,
262
- md: propType,
263
- lg: propType,
264
- xl: propType
265
- })
266
- ])
292
+ /**
293
+ * @typedef {0|1|2|3|4|5|6|7|8|9|10|11} SpacingIndex - value used to select a size on the spacing scale
294
+ *
295
+ * @typedef SpacingOptions
296
+ * @property {VariantProp} [SpacingOptions.variant] - optional theme scale variants e.g. compact, wide
297
+ * @property {number} [SpacingOptions.size] - optional override to force a particular pixel size
298
+ * @property {number} [SpacingOptions.subtract] - optional number to subtract from final pixel size
299
+ *
300
+ * @typedef SpacingObject
301
+ * @property {SpacingIndex} [SpacingObject.xs] - space scale index to use above xs viewport
302
+ * @property {SpacingIndex} [SpacingObject.sm] - space scale index to use above sm viewport
303
+ * @property {SpacingIndex} [SpacingObject.md] - space scale index to use above md viewport
304
+ * @property {SpacingIndex} [SpacingObject.lg] - space scale index to use above lg viewport
305
+ * @property {SpacingIndex} [SpacingObject.xl] - space scale index to use above xl viewport
306
+ * @property {SpacingIndex} [SpacingObject.space] - space scale index to use at all viewports
307
+ * @property {SpacingOptions} [SpacingObject.options] - optional options for this spacing
308
+ *
309
+ * @typedef {SpacingIndex|SpacingObject} SpacingValue
310
+ */
311
+ const spacingScale = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
312
+ const spacingIndexPropType = PropTypes.oneOf(spacingScale)
313
+ const spacingObjectPropType = PropTypes.shape({
314
+ ...responsiveProps.getByViewport(spacingIndexPropType),
315
+ space: spacingIndexPropType,
316
+ options: PropTypes.shape({
317
+ variant: variantProp.propType,
318
+ size: PropTypes.number
319
+ })
320
+ })
321
+ /**
322
+ * Components and utilities that assign fixed space between components use a theme-defined spacing scale.
323
+ *
324
+ * They typically take one or more props of the {@link SpacingValue} type and turn it into a pixel size value
325
+ * using the hook `useSpacingScale`, which resolves any options or responsive behaviour and returns the
326
+ * appropriate value from the theme spacing scale.
327
+ *
328
+ * - see /ADRs/inter-component-spacing/README.md - ADR on this structure
329
+ * - see /src/utils/spacing/useSpacingScale.js - hook that processes spacing values
330
+ * - @see {@link SpacingIndex} - themes provide spacing scales of up to 12 sizes with optional theme rules.
331
+ * - @see {@link SpacingValue} - either a simple number referencing an index position on the spacing
332
+ * scale, or an object with an optional `options` key and one or more spacing indexes keyed either by
333
+ * viewports or `space` to apply at all viewports.
334
+ */
335
+ export const spacingProps = {
336
+ scale: spacingScale,
337
+ types: {
338
+ spacingIndex: spacingIndexPropType,
339
+ spacingObject: spacingObjectPropType,
340
+
341
+ // Most spacing components and utilities take this prop / arg type:
342
+ spacingValue: PropTypes.oneOfType([spacingIndexPropType, spacingObjectPropType])
343
+ }
344
+ }
267
345
 
268
346
  /**
269
347
  * Returns a prop type validator which checks whether a prop is either a component or an array of components of a given
@@ -347,3 +425,14 @@ export const componentPropType = (passedName, checkDisplayName = false) => {
347
425
 
348
426
  return validate
349
427
  }
428
+
429
+ export const copyPropTypes = PropTypes.oneOf(['en', 'fr'])
430
+
431
+ export const paddingProp = {
432
+ propType: PropTypes.shape({
433
+ paddingBottom: PropTypes.number,
434
+ paddingLeft: PropTypes.number,
435
+ paddingRight: PropTypes.number,
436
+ paddingTop: PropTypes.number
437
+ })
438
+ }
@@ -0,0 +1,3 @@
1
+ export { default as useSpacingScale } from './useSpacingScale'
2
+
3
+ export * from './utils'
@@ -0,0 +1,93 @@
1
+ import { useViewport } from '../../ViewportProvider'
2
+ import { useThemeTokens } from '../../ThemeProvider'
3
+ import { resolveSpacingValue, resolveSpacingOptions } from './utils'
4
+ /**
5
+ * @typedef {import('@telus-uds/system-constants/viewports').Viewport} Viewport
6
+ * @typedef {import('../propTypes.js').SpacingValue} SpacingValue
7
+ * @typedef {import('../propTypes.js').SpacingIndex} SpacingIndex
8
+ * @typedef {import('../propTypes.js').SpacingObject} SpacingObject
9
+ * @typedef {import('../propTypes.js').SpacingOptions} SpacingOptions
10
+ */
11
+
12
+ /**
13
+ * Pass a {@link SpacingValue}, which is one of:
14
+ *
15
+ * - A number 0-11 ({@link SpacingIndex}) pointing to an index on the theme's spacing scale
16
+ * - Or, an object ({@link SpacingObject}), with optional properties:
17
+ * - `xs`, `sm`, `md`, `lg`, `xl`: {@link SpacingIndex} to apply at or above these specified {@link Viewport}
18
+ * - `options`: an optional {@link SpacingOptions} object, see below
19
+ * - `space`: a {@link SpacingIndex} to apply to all viewports (can be used alongside `options`)
20
+ *
21
+ * ## Example
22
+ *
23
+ * If the theme's spacing scale is `[0, 4, 8, 12, 16, 24, 48]` with no theme rules:
24
+ *
25
+ * - `useSpacingScale(0)` returns `0`
26
+ * - `useSpacingScale(2)` returns `8`
27
+ * - `useSpacingScale({ xs: 3, lg: 4 })` returns `12` at 'xs', 'sm' or 'md' viewports, and `16` at 'lg' or 'xl'.
28
+ *
29
+ * These viewport properties are intended to support case-specific responsive layout changes, for example, where a
30
+ * grid item drops or adds spacing on a particular side at viewports where the parent changes the layout shape.
31
+ *
32
+ * ## Theming
33
+ *
34
+ * A theme's `'spacingScale'` has theme rules and appearances same as components, and may support `viewport`.
35
+ * For example, a theme with the following rule would change index [2] above from `8` to `12` on large viewports:
36
+ *
37
+ * ```json
38
+ * { if: { space: 2, viewport: ['lg', 'xl'] }, tokens: { size: 12 }}
39
+ * ```
40
+ *
41
+ * Setting responsive spacing in the theme is the preferred way to adapt the aesthetic tightness or looseness of
42
+ * a theme to the available space without changing the shape of the layout itself.
43
+ *
44
+ * ## Options
45
+ *
46
+ * Space values passed as objects may have an `options` key, with the following optional properties:
47
+ *
48
+ * - `variant`: Themes may choose to have spacing scale variants, same as variants in component themes.
49
+ * For example, if a theme rule contains `{ if: { space: 2, compact: true }, tokens: { size: 6 }}`,
50
+ * this tighter spacing can be accessed with:
51
+ *
52
+ * ```jsx
53
+ * const compactSize = useSpacingScale({ space: 2, options: { variant: { compact: true }}})`
54
+ * ```
55
+ *
56
+ * - `subtract`: Sometimes on-brand spacing needs to be reduced by another value to achieve an on-brand result.
57
+ * For example, a component with a variable border may want to subtract its border width from its padding so
58
+ * the total distance from content edge to bounding box is a valid theme value, regardless of border width:
59
+ *
60
+ * ```jsx
61
+ * const padding = useSpacingScale({ space: 2, options: { subtract: themeTokens.borderWidth }})
62
+ * ```
63
+ *
64
+ * - `size`: In exceptional cases, the theme's spacing scale may be bypassed by passing a `size` option.
65
+ * This can result in layouts that may be rejected for being off-brand so should only be used as a
66
+ * last resort for fixing layout problems (e.g. when working around non-branded embedded content).
67
+ * Where possible, fixing layout issues using a spacing scale value and the `subtract` option is preferred.
68
+ *
69
+ * ```jsx
70
+ * // Comments should be included when `size` is used, stating why this off-brand size is needed
71
+ * const iframeOffset = useSpacingScale({ options: { size: 13 }})`
72
+ * ```
73
+ *
74
+ * ## References
75
+ *
76
+ * `/ADRs/inter-component-spacing/README.md` - ADR on this structure
77
+ *
78
+ * @param {SpacingValue} [spaceValue] - a {@link SpacingIndex} number, or a {@link SpacingObject}
79
+ * @returns {number}
80
+ */
81
+ const useSpacingScale = (spaceValue) => {
82
+ // In future, may need to consider window height as well as width, particularly for native apps,
83
+ // e.g. to ensure designs don't look lost on large, tall, not-so-wide portrait screens.
84
+ const viewport = useViewport()
85
+
86
+ const { tokens, variant, overridden, subtract = 0 } = resolveSpacingOptions(spaceValue)
87
+ const space = overridden ? null : resolveSpacingValue(spaceValue, viewport)
88
+
89
+ const { size } = useThemeTokens('spacingScale', tokens, variant, { space, viewport })
90
+ return Math.max(size - subtract, 0)
91
+ }
92
+
93
+ export default useSpacingScale
@@ -0,0 +1,28 @@
1
+ import { viewports as systemViewports } from '@telus-uds/system-constants'
2
+
3
+ export const resolveSpacingValue = (space, viewport) => {
4
+ // If spacing value has been passed as undefined or nullish, get the 0-index
5
+ if (!space) return 0
6
+
7
+ // Pass through simple non-responsive numbers (which may be in objects alongside options)
8
+ if (typeof space === 'number') return space
9
+ if (typeof space.space === 'number') return space.space
10
+
11
+ // Get the appropriate space value for the current viewport.
12
+ // If no viewports available (e.g. SSR), default to the smallest.
13
+ // If no values are found (e.g. empty space object), default to 0.
14
+ return (viewport && systemViewports.inherit(space)[viewport]) ?? space.xs ?? 0
15
+ }
16
+
17
+ export const resolveSpacingOptions = (space) => {
18
+ if (!space?.options) return {}
19
+
20
+ const { size, variant, subtract = 0 } = space.options
21
+ const overridden = typeof size === 'number'
22
+
23
+ // Might need an option that adapts the size value by current user's system font scale, so that
24
+ // meaningful spacing between items doesn't disappear on the highest a11y font scale settings.
25
+ // https://github.com/telus/universal-design-system/issues/583
26
+
27
+ return { tokens: { size }, variant, overridden, subtract }
28
+ }
@@ -0,0 +1,14 @@
1
+ import { useState } from 'react'
2
+
3
+ let id = 0
4
+
5
+ function useUniqueId(prefix = '') {
6
+ const [uniqueId] = useState(() => {
7
+ id += 1
8
+ return `${prefix}-${id}`
9
+ })
10
+
11
+ return uniqueId
12
+ }
13
+
14
+ export default useUniqueId
@@ -4,6 +4,7 @@ import { StyleSheet, Text, View } from 'react-native'
4
4
 
5
5
  import A11yText from '../../lib/A11yText'
6
6
  import { Button, Typography } from '../../lib'
7
+ import { EachParentType, parentTypesParams } from '../supports'
7
8
 
8
9
  const defaultArgs = {
9
10
  text: 'This text is for screen readers,',
@@ -31,6 +32,10 @@ const Template = (args) => (
31
32
  </>
32
33
  )
33
34
 
35
+ export const Default = (args) => <A11yText {...args} />
36
+ Default.storyName = 'A11yText'
37
+ Default.args = defaultArgs
38
+
34
39
  export const A11yTextInText = Template.bind({})
35
40
  A11yTextInText.args = defaultArgs
36
41
 
@@ -50,12 +55,13 @@ export const A11yTextInButton = (args) => (
50
55
  )
51
56
  A11yTextInButton.args = defaultArgs
52
57
 
53
- export const A11yTextInRow = (args) => (
54
- <View style={styles.row}>
55
- <Template {...args} />
56
- </View>
58
+ export const ParentTypes = (args) => (
59
+ <EachParentType {...args}>
60
+ {({ label }) => <Template {...args} key={label} text={label} />}
61
+ </EachParentType>
57
62
  )
58
- A11yTextInRow.args = defaultArgs
63
+ ParentTypes.args = defaultArgs
64
+ ParentTypes.parameters = parentTypesParams
59
65
 
60
66
  const styles = StyleSheet.create({
61
67
  // Use borders so any hairline gaps created by A11yText are visible