@telus-uds/components-base 1.72.0 → 1.74.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 (71) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/lib/Box/Box.js +17 -6
  3. package/lib/ExpandCollapse/Panel.js +1 -1
  4. package/lib/FlexGrid/Col/Col.js +42 -19
  5. package/lib/FlexGrid/FlexGrid.js +40 -17
  6. package/lib/FlexGrid/Row/Row.js +45 -22
  7. package/lib/Footnote/Footnote.js +328 -0
  8. package/lib/Footnote/FootnoteLink.js +108 -0
  9. package/lib/Footnote/dictionary.js +19 -0
  10. package/lib/Footnote/index.js +12 -0
  11. package/lib/Listbox/ListboxGroup.js +7 -1
  12. package/lib/MultiSelectFilter/MultiSelectFilter.js +1 -0
  13. package/lib/Notification/Notification.js +13 -5
  14. package/lib/OrderedList/ItemBase.js +7 -1
  15. package/lib/Responsive/Responsive.js +32 -14
  16. package/lib/Responsive/ResponsiveProp.js +46 -0
  17. package/lib/Responsive/ResponsiveWithMediaQueryStyleSheet.js +75 -0
  18. package/lib/ThemeProvider/ThemeProvider.js +5 -2
  19. package/lib/ThemeProvider/index.js +9 -1
  20. package/lib/ThemeProvider/useResponsiveThemeTokens.js +89 -0
  21. package/lib/Typography/Typography.js +50 -22
  22. package/lib/index.js +8 -0
  23. package/lib/server.js +40 -0
  24. package/lib/utils/ssr-media-query/utils/create-media-query-styles.js +39 -6
  25. package/lib-module/Box/Box.js +17 -6
  26. package/lib-module/ExpandCollapse/Panel.js +1 -1
  27. package/lib-module/FlexGrid/Col/Col.js +42 -19
  28. package/lib-module/FlexGrid/FlexGrid.js +40 -17
  29. package/lib-module/FlexGrid/Row/Row.js +45 -22
  30. package/lib-module/Footnote/Footnote.js +319 -0
  31. package/lib-module/Footnote/FootnoteLink.js +101 -0
  32. package/lib-module/Footnote/dictionary.js +12 -0
  33. package/lib-module/Footnote/index.js +4 -0
  34. package/lib-module/Listbox/ListboxGroup.js +7 -1
  35. package/lib-module/MultiSelectFilter/MultiSelectFilter.js +1 -0
  36. package/lib-module/Notification/Notification.js +13 -5
  37. package/lib-module/OrderedList/ItemBase.js +7 -1
  38. package/lib-module/Responsive/Responsive.js +32 -15
  39. package/lib-module/Responsive/ResponsiveProp.js +39 -0
  40. package/lib-module/Responsive/ResponsiveWithMediaQueryStyleSheet.js +67 -0
  41. package/lib-module/ThemeProvider/ThemeProvider.js +5 -2
  42. package/lib-module/ThemeProvider/index.js +1 -0
  43. package/lib-module/ThemeProvider/useResponsiveThemeTokens.js +81 -0
  44. package/lib-module/Typography/Typography.js +52 -24
  45. package/lib-module/index.js +1 -0
  46. package/lib-module/server.js +4 -0
  47. package/lib-module/utils/ssr-media-query/utils/create-media-query-styles.js +36 -6
  48. package/package.json +13 -2
  49. package/src/Box/Box.jsx +35 -17
  50. package/src/ExpandCollapse/Panel.jsx +1 -1
  51. package/src/FlexGrid/Col/Col.jsx +42 -13
  52. package/src/FlexGrid/FlexGrid.jsx +40 -11
  53. package/src/FlexGrid/Row/Row.jsx +40 -16
  54. package/src/Footnote/Footnote.jsx +316 -0
  55. package/src/Footnote/FootnoteLink.jsx +95 -0
  56. package/src/Footnote/dictionary.js +12 -0
  57. package/src/Footnote/index.js +6 -0
  58. package/src/Listbox/ListboxGroup.jsx +9 -2
  59. package/src/MultiSelectFilter/MultiSelectFilter.jsx +2 -0
  60. package/src/Notification/Notification.jsx +15 -3
  61. package/src/OrderedList/ItemBase.jsx +14 -2
  62. package/src/Responsive/Responsive.jsx +31 -12
  63. package/src/Responsive/ResponsiveProp.jsx +33 -0
  64. package/src/Responsive/ResponsiveWithMediaQueryStyleSheet.jsx +60 -0
  65. package/src/ThemeProvider/ThemeProvider.jsx +5 -2
  66. package/src/ThemeProvider/index.js +1 -0
  67. package/src/ThemeProvider/useResponsiveThemeTokens.js +85 -0
  68. package/src/Typography/Typography.jsx +77 -24
  69. package/src/index.js +1 -0
  70. package/src/server.js +4 -0
  71. package/src/utils/ssr-media-query/utils/create-media-query-styles.js +21 -6
@@ -0,0 +1,95 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View, StyleSheet, Platform, TouchableWithoutFeedback } from 'react-native'
4
+ import dictionary from './dictionary'
5
+ import { htmlAttrs, selectSystemProps, useCopy, viewProps, wrapStringsInText } from '../utils'
6
+ import { applyTextStyles, useThemeTokens } from '../ThemeProvider'
7
+
8
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps])
9
+
10
+ // The top property varies between devices due to how devices render the viewport.
11
+ const selectTextStyle = ({ fontSize, lineHeight }) => ({
12
+ fontSize,
13
+ lineHeight: lineHeight !== 1 ? lineHeight : fontSize * 2,
14
+ ...Platform.select({
15
+ ios: {
16
+ top: -fontSize / 2
17
+ },
18
+ android: {
19
+ top: fontSize / 4
20
+ },
21
+ web: {
22
+ top: -fontSize / 2.8
23
+ }
24
+ })
25
+ })
26
+
27
+ const FootnoteLink = ({ copy = 'en', content = [], onClick, tokens, variant = {}, ...rest }) => {
28
+ const themeTokens = useThemeTokens('FootnoteLink', tokens, variant)
29
+ const textStyles = applyTextStyles(themeTokens)
30
+
31
+ const numbers = Array.isArray(content) ? content : [content]
32
+ const refs = numbers.map(() => React.createRef())
33
+
34
+ const handleOnClick = (index) => {
35
+ onClick(numbers[index], refs[index])
36
+ }
37
+ const getCopy = useCopy({ dictionary, copy })
38
+
39
+ return (
40
+ <>
41
+ {numbers.map((num, index) => (
42
+ <View
43
+ accessibilityLabel={getCopy('a11yLabel')}
44
+ key={num}
45
+ ref={refs[index]}
46
+ {...selectProps(rest)}
47
+ >
48
+ <TouchableWithoutFeedback onPress={handleOnClick} accessibilityRole="button">
49
+ {wrapStringsInText(`${num}${index !== numbers.length - 1 ? ',' : ''}`, {
50
+ style: {
51
+ ...textStyles,
52
+ ...staticStyles.text,
53
+ ...selectTextStyle(themeTokens)
54
+ }
55
+ })}
56
+ </TouchableWithoutFeedback>
57
+ </View>
58
+ ))}
59
+ </>
60
+ )
61
+ }
62
+
63
+ const copyShape = PropTypes.shape({ a11yLabel: PropTypes.string.isRequired })
64
+
65
+ FootnoteLink.propTypes = {
66
+ ...selectedSystemPropTypes,
67
+ /**
68
+ * Use the `copy` prop to either select provided English or French copy by passing 'en' or 'fr' respectively.
69
+ * To provide your own, pass a JSON object with the key `a11yLabel`.
70
+ */
71
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), copyShape]),
72
+ /**
73
+ * The footnote number, or multiple numbers if passed as an array.
74
+ * If using an array, a comma-separated group of numbers will be rendered as superscript.
75
+ */
76
+ content: PropTypes.oneOfType([
77
+ PropTypes.number,
78
+ PropTypes.arrayOf(PropTypes.number),
79
+ PropTypes.string,
80
+ PropTypes.arrayOf(PropTypes.string)
81
+ ]).isRequired,
82
+ /**
83
+ * A callback function to handle the click of a FootnoteLink.
84
+ */
85
+ onClick: PropTypes.func.isRequired
86
+ }
87
+
88
+ export default FootnoteLink
89
+
90
+ const staticStyles = StyleSheet.create({
91
+ text: {
92
+ position: 'relative',
93
+ textDecorationLine: 'underline'
94
+ }
95
+ })
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ a11yLabel: 'Read legal footnote',
4
+ close: 'close',
5
+ heading: 'Terms and conditions'
6
+ },
7
+ fr: {
8
+ a11yLabel: 'Lire la note de bas de page légale',
9
+ close: 'fermer',
10
+ heading: 'Modalités et conditions'
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ import Footnote from './Footnote'
2
+ import FootnoteLink from './FootnoteLink'
3
+
4
+ Footnote.Link = FootnoteLink
5
+
6
+ export default Footnote
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react-native-a11y/has-valid-accessibility-role */
2
2
  import React, { forwardRef } from 'react'
3
3
  import PropTypes from 'prop-types'
4
- import { View, StyleSheet } from 'react-native'
4
+ import { View, StyleSheet, Platform } from 'react-native'
5
5
  import { withLinkRouter } from '../utils'
6
6
  import ExpandCollapse from '../ExpandCollapse'
7
7
  import ListboxItem from './ListboxItem'
@@ -20,6 +20,13 @@ const styles = StyleSheet.create({
20
20
  }
21
21
  })
22
22
 
23
+ const getAccessibilityRole = () =>
24
+ Platform.select({
25
+ ios: 'listitem',
26
+ android: 'none',
27
+ web: 'listitem'
28
+ })
29
+
23
30
  const ListboxGroup = forwardRef(
24
31
  (
25
32
  {
@@ -39,7 +46,7 @@ const ListboxGroup = forwardRef(
39
46
 
40
47
  // TODO: implement keyboard navigation via refs for grouped items separately here
41
48
  return (
42
- <View id="test" style={styles.groupWrapper} accessibilityRole="listitem">
49
+ <View id="test" style={styles.groupWrapper} accessibilityRole={getAccessibilityRole()}>
43
50
  <ExpandCollapse.Panel
44
51
  panelId={id ?? label}
45
52
  controlTokens={{
@@ -89,6 +89,8 @@ const MultiSelectFilter = ({
89
89
  return colSize === 2 && setMaxWidth(true)
90
90
  }, [colSize])
91
91
 
92
+ useEffect(() => setCheckedIds(currentValues ?? []), [currentValues])
93
+
92
94
  const {
93
95
  headerFontColor,
94
96
  headerFontSize,
@@ -22,8 +22,20 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
22
22
 
23
23
  const selectContainerStyles = (tokens) => ({ ...tokens })
24
24
 
25
- const selectTextStyles = (tokens, themeOptions) =>
26
- applyTextStyles({ ...selectTokens('Typography', tokens), themeOptions })
25
+ const selectTextStyles = (tokens, themeOptions, isDismissible) => {
26
+ const textTokens = selectTokens('Typography', tokens)
27
+ const styles = {
28
+ ...textTokens,
29
+ themeOptions,
30
+ overflow: 'hidden'
31
+ }
32
+
33
+ if (!isDismissible) {
34
+ styles.flexShrink = 1
35
+ }
36
+
37
+ return applyTextStyles(styles)
38
+ }
27
39
 
28
40
  const selectIconProps = ({ iconSize, iconColor }) => ({
29
41
  size: iconSize,
@@ -113,7 +125,7 @@ const Notification = forwardRef(
113
125
  return null
114
126
  }
115
127
 
116
- const textStyles = selectTextStyles(themeTokens, themeOptions)
128
+ const textStyles = selectTextStyles(themeTokens, themeOptions, dismissible)
117
129
 
118
130
  const content = wrapStringsInText(
119
131
  typeof children === 'function' ? children({ textStyles, variant }) : children,
@@ -1,10 +1,22 @@
1
1
  /* eslint-disable react-native-a11y/has-valid-accessibility-role */
2
2
  import React, { forwardRef } from 'react'
3
3
  import PropTypes from 'prop-types'
4
- import { View, StyleSheet } from 'react-native'
4
+ import { View, StyleSheet, Platform } from 'react-native'
5
+
6
+ const getAccessibilityRole = () =>
7
+ Platform.select({
8
+ ios: 'listitem',
9
+ android: 'none',
10
+ web: 'listitem'
11
+ })
5
12
 
6
13
  const Item = forwardRef(({ children, style, ...rest }, ref) => (
7
- <View accessibilityRole="listitem" ref={ref} style={[staticStyles.container, style]} {...rest}>
14
+ <View
15
+ accessibilityRole={getAccessibilityRole()}
16
+ ref={ref}
17
+ style={[staticStyles.container, style]}
18
+ {...rest}
19
+ >
8
20
  {children}
9
21
  </View>
10
22
  ))
@@ -1,29 +1,42 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { viewports } from '@telus-uds/system-constants'
4
- import { useResponsiveProp } from '../utils'
3
+ import { useTheme } from '../ThemeProvider'
4
+ import ResponsiveProp from './ResponsiveProp'
5
+ import ResponsiveWithMediaQueryStyleSheet from './ResponsiveWithMediaQueryStyleSheet'
5
6
 
6
7
  /**
7
8
  * Responsive conditionally renders children based on whether the viewport matches the provided
8
9
  * min and max viewports.
9
10
  *
10
- * In SSR, like other viewport utilities, it treats the viewport as `xs` both in SSR itself and
11
+ * If enableMediaQueryStyleSheet themeOption is set to false in ThemeProvider, then in SSR,
12
+ * like other viewport utilities, it treats the viewport as `xs` both in SSR itself and
11
13
  * during first hydration on the client side; then if the viewport is not `xs`, it re-renders
12
14
  * after hydration. This may cause a layout shift on devices other than the narrowest.
15
+ *
16
+ * If enableMediaQueryStyleSheet themeOption is set to true in ThemeProvider, then media query stylesheet
17
+ * is used to hide and show children of `Responsive` within a View.
13
18
  */
14
19
 
15
- const Responsive = ({ min = 'xs', max, children }) => {
16
- // Start returning children at the 'min' viewport or greater
17
- const byViewports = { [min]: children }
18
- if (max && max !== 'xl') {
19
- // Stop returning children at the viewport one above 'max' or greater
20
- const maxIndex = viewports.keys.indexOf(max)
21
- const maxPlusOne = maxIndex >= 0 ? viewports.keys[maxIndex + 1] : null
22
- if (maxPlusOne) byViewports[maxPlusOne] = null
20
+ const Responsive = ({ min = 'xs', max, inheritedStyles = [], children }) => {
21
+ const {
22
+ themeOptions: { enableMediaQueryStyleSheet }
23
+ } = useTheme()
24
+ if (enableMediaQueryStyleSheet) {
25
+ return (
26
+ <ResponsiveWithMediaQueryStyleSheet inheritedStyles={inheritedStyles} min={min} max={max}>
27
+ {children}
28
+ </ResponsiveWithMediaQueryStyleSheet>
29
+ )
23
30
  }
24
- return <>{useResponsiveProp(byViewports, null)}</>
31
+ return (
32
+ <ResponsiveProp min={min} max={max}>
33
+ {children}
34
+ </ResponsiveProp>
35
+ )
25
36
  }
26
37
 
38
+ Responsive.displayName = 'Responsive'
39
+
27
40
  Responsive.propTypes = {
28
41
  /**
29
42
  * To hide children of `Responsive` if the current viewport is smaller than `min`
@@ -33,6 +46,12 @@ Responsive.propTypes = {
33
46
  * To hide children of `Responsive` if the current viewport is larger than `max`
34
47
  */
35
48
  max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
49
+ /**
50
+ * Styles to be inherited by `Responsive`.
51
+ * It should be an array of style property names.
52
+ * Note: This prop is only applicable when `enableMediaQueryStylesheet` is set to true in the `ThemeProvider`.
53
+ */
54
+ inheritedStyles: PropTypes.arrayOf(PropTypes.string),
36
55
  children: PropTypes.node.isRequired
37
56
  }
38
57
 
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { viewports } from '@telus-uds/system-constants'
5
+ import { useResponsiveProp } from '../utils'
6
+
7
+ const ResponsiveProp = ({ min = 'xs', max, children }) => {
8
+ const byViewports = { [min]: children }
9
+ if (max && max !== 'xl') {
10
+ // Stop returning children at the viewport one above 'max' or greater
11
+ const maxIndex = viewports.keys.indexOf(max)
12
+ const maxPlusOne = maxIndex >= 0 ? viewports.keys[maxIndex + 1] : null
13
+ if (maxPlusOne) byViewports[maxPlusOne] = null
14
+ }
15
+ const responsiveProp = useResponsiveProp(byViewports, null)
16
+ return <>{responsiveProp}</>
17
+ }
18
+
19
+ ResponsiveProp.displayName = 'Responsive'
20
+
21
+ ResponsiveProp.propTypes = {
22
+ /**
23
+ * To hide children of `Responsive` if the current viewport is smaller than `min`
24
+ */
25
+ min: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
26
+ /**
27
+ * To hide children of `Responsive` if the current viewport is larger than `max`
28
+ */
29
+ max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
30
+ children: PropTypes.node.isRequired
31
+ }
32
+
33
+ export default ResponsiveProp
@@ -0,0 +1,60 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { viewports } from '@telus-uds/system-constants'
4
+ import { BaseView, StyleSheet, createMediaQueryStyles } from '../utils'
5
+
6
+ function generateResponsiveStyles(min, max) {
7
+ const viewportsArray = viewports.keys
8
+ const minIndex = viewportsArray.indexOf(min)
9
+ const maxIndex = viewportsArray.indexOf(max)
10
+ let hiddenViewports = []
11
+
12
+ if (minIndex !== -1) {
13
+ hiddenViewports = viewportsArray.slice(0, minIndex)
14
+ }
15
+ if (maxIndex !== -1) {
16
+ hiddenViewports = hiddenViewports.concat(viewportsArray.slice(maxIndex + 1))
17
+ }
18
+
19
+ const styles = {}
20
+
21
+ viewportsArray.forEach((viewport) => {
22
+ if (hiddenViewports.includes(viewport)) {
23
+ styles[viewport] = { display: 'none' }
24
+ }
25
+ })
26
+ return createMediaQueryStyles(styles, false)
27
+ }
28
+ const ResponsiveWithMediaQueryStyleSheet = ({ min, max, inheritedStyles = [], children }) => {
29
+ const { ids, styles } = StyleSheet.create({
30
+ responsive: {
31
+ ...inheritedStyles.reduce((acc, prop) => {
32
+ acc[prop] = 'inherit'
33
+ return acc
34
+ }, {}),
35
+ ...generateResponsiveStyles(min, max)
36
+ }
37
+ })
38
+ return (
39
+ <BaseView style={styles.responsive} dataSet={ids.responsive && { media: ids.responsive }}>
40
+ {children}
41
+ </BaseView>
42
+ )
43
+ }
44
+
45
+ ResponsiveWithMediaQueryStyleSheet.displayName = 'Responsive'
46
+
47
+ ResponsiveWithMediaQueryStyleSheet.propTypes = {
48
+ /**
49
+ * To hide children of `Responsive` if the current viewport is smaller than `min`
50
+ */
51
+ min: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
52
+ /**
53
+ * To hide children of `Responsive` if the current viewport is larger than `max`
54
+ */
55
+ max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
56
+ inheritedStyles: PropTypes.arrayOf(PropTypes.string),
57
+ children: PropTypes.node.isRequired
58
+ }
59
+
60
+ export default ResponsiveWithMediaQueryStyleSheet
@@ -16,7 +16,8 @@ const defaultThemeOptions = {
16
16
  // TODO: switch `forceZIndex` to be false by default in the next major version
17
17
  forceZIndex: true,
18
18
  // TODO: switch `enableHelmetSSR` to be false by default in the next major version
19
- enableHelmetSSR: true
19
+ enableHelmetSSR: true,
20
+ enableMediaQueryStyleSheet: false
20
21
  }
21
22
 
22
23
  const ThemeProvider = ({ children, defaultTheme, themeOptions = {} }) => {
@@ -65,12 +66,14 @@ ThemeProvider.propTypes = {
65
66
  * - `enableHelmetSSR`: on Web SSR, allows React Helmet to run during server-side rendering. This should be
66
67
  * disabled unless a web app has been specifically configured to stop React Helmet accumulating
67
68
  * instances (which may cause a memory leak). See React Helmet's docs: https://github.com/nfl/react-helmet
69
+ * - `enableMediaQueryStyleSheet`: enables the use of Media Query StyleSheet.
68
70
  */
69
71
  themeOptions: PropTypes.shape({
70
72
  forceAbsoluteFontSizing: PropTypes.bool,
71
73
  forceZIndex: PropTypes.bool,
72
74
  enableHelmetSSR: PropTypes.bool,
73
- contentMaxWidth: responsiveProps.getTypeOptionallyByViewport(PropTypes.number)
75
+ contentMaxWidth: responsiveProps.getTypeOptionallyByViewport(PropTypes.number),
76
+ enableMediaQueryStyleSheet: PropTypes.bool
74
77
  })
75
78
  }
76
79
 
@@ -2,6 +2,7 @@ import ThemeProvider from './ThemeProvider'
2
2
 
3
3
  export { default as useTheme } from './useTheme'
4
4
  export { default as useSetTheme } from './useSetTheme'
5
+ export { default as useResponsiveThemeTokens } from './useResponsiveThemeTokens'
5
6
  export * from './useThemeTokens'
6
7
 
7
8
  export * from './utils'
@@ -0,0 +1,85 @@
1
+ import { viewports } from '@telus-uds/system-constants'
2
+ import useTheme from './useTheme'
3
+ import { getComponentTheme, mergeAppearances, resolveThemeTokens } from './utils'
4
+
5
+ const getResponsiveThemeTokens = (
6
+ { rules = [], tokens: defaultThemeTokens = {} },
7
+ tokensProp,
8
+ variants = {},
9
+ states
10
+ ) => {
11
+ const appearances = mergeAppearances(variants, states)
12
+
13
+ const tokensByViewport = Object.fromEntries(
14
+ viewports.keys.map((viewport) => [viewport, { ...defaultThemeTokens }])
15
+ )
16
+
17
+ // Go through each rule and collect them for the corresponding viewport if they apply
18
+ rules.forEach((rule) => {
19
+ if (doesRuleApply(rule, appearances)) {
20
+ // If the rule does not have a viewport specified, we collect it in all viewports
21
+ let targetViewports = rule.if.viewport || viewports.keys
22
+ if (!Array.isArray(targetViewports)) {
23
+ targetViewports = [targetViewports]
24
+ }
25
+ targetViewports.forEach((viewport) => {
26
+ tokensByViewport[viewport] = { ...tokensByViewport[viewport], ...rule.tokens }
27
+ })
28
+ }
29
+ })
30
+
31
+ Object.keys(tokensByViewport).forEach((viewport) => {
32
+ tokensByViewport[viewport] = resolveThemeTokens(
33
+ tokensByViewport[viewport],
34
+ appearances,
35
+ tokensProp
36
+ )
37
+ })
38
+
39
+ return tokensByViewport
40
+ }
41
+
42
+ const doesRuleApply = (rule, appearances) =>
43
+ Object.entries(rule.if).every((condition) => doesConditionApply(condition, appearances))
44
+
45
+ const doesConditionApply = ([key, value], appearances) => {
46
+ if (key === 'viewport') {
47
+ return true
48
+ }
49
+ // use null rather than undefined so we can serialise the value in themes
50
+ const appearanceValue = appearances[key] ?? null
51
+ return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue
52
+ }
53
+
54
+ /**
55
+ * @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
56
+ * @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
57
+ * @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
58
+ *
59
+ * Returns a complete set of tokens for a component for each viewport based on which of the
60
+ * component's theme rules apply to the current set of theme appearances.
61
+ * Pass the returned output to createMediaQueryStyles to generate media query styles for use inside
62
+ * the media query stylesheet from './utils/ssr-media-query'.
63
+ *
64
+ * @typedef {Object} ResponsiveObject
65
+ * @property {TokensSet} xs
66
+ * @property {TokensSet} sm
67
+ * @property {TokensSet} md
68
+ * @property {TokensSet} lg
69
+ * @property {TokensSet} xl
70
+ *
71
+ * @param { string } componentName
72
+ * @param { TokensProp } tokens
73
+ * @param { AppearanceSet } variants
74
+ * @param { AppearanceSet } states
75
+ * @returns { ResponsiveObject }
76
+ */
77
+
78
+ const useResponsiveThemeTokens = (componentName, tokens = {}, variants = {}, states = {}) => {
79
+ const theme = useTheme()
80
+ const componentTheme = getComponentTheme(theme, componentName)
81
+ const themeTokens = getResponsiveThemeTokens(componentTheme, tokens, variants, states)
82
+ return themeTokens
83
+ }
84
+
85
+ export default useResponsiveThemeTokens
@@ -2,8 +2,7 @@ import React, { forwardRef } from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { Text, View } from 'react-native'
4
4
 
5
- import { useTheme, useThemeTokens } from '../ThemeProvider'
6
- import { useViewport } from '../ViewportProvider'
5
+ import { useResponsiveThemeTokens, useTheme, useThemeTokens } from '../ThemeProvider'
7
6
  import { applyTextStyles } from '../ThemeProvider/utils'
8
7
  import {
9
8
  a11yProps,
@@ -15,17 +14,29 @@ import {
15
14
  textTags,
16
15
  textProps,
17
16
  viewProps,
18
- getA11yPropsFromHtmlTag
17
+ getA11yPropsFromHtmlTag,
18
+ StyleSheet,
19
+ createMediaQueryStyles
19
20
  } from '../utils'
21
+ import { useViewport } from '../ViewportProvider'
20
22
  /**
21
23
  * @typedef {import('../utils/a11y/semantics').TextTag} TextTag
22
24
  */
23
25
 
24
26
  const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([a11yProps, viewProps])
25
27
  const [selectTextProps, selectedTextPropTypes] = selectSystemProps([textProps])
26
-
27
28
  const selectTextStyles = (
28
- { fontWeight, fontSize, color, lineHeight, fontName, textAlign, textTransform, letterSpacing },
29
+ {
30
+ fontWeight,
31
+ fontSize,
32
+ color,
33
+ lineHeight,
34
+ fontName,
35
+ textAlign,
36
+ textTransform,
37
+ letterSpacing,
38
+ textDecorationLine
39
+ },
29
40
  themeOptions
30
41
  ) =>
31
42
  applyTextStyles({
@@ -37,7 +48,8 @@ const selectTextStyles = (
37
48
  themeOptions,
38
49
  textAlign,
39
50
  textTransform,
40
- letterSpacing
51
+ letterSpacing,
52
+ textDecorationLine
41
53
  })
42
54
 
43
55
  // General-purpose flexible theme-neutral base component for text
@@ -59,19 +71,61 @@ const Typography = forwardRef(
59
71
  ref
60
72
  ) => {
61
73
  const viewport = useViewport()
62
- const { superScriptFontSize, ...themeTokens } = useThemeTokens('Typography', tokens, variant, {
63
- viewport
64
- })
65
74
  const { themeOptions } = useTheme()
66
75
 
76
+ const { enableMediaQueryStyleSheet } = themeOptions
77
+
78
+ const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokens : useThemeTokens
79
+ const themeTokens = useTokens(
80
+ 'Typography',
81
+ tokens,
82
+ variant,
83
+ !enableMediaQueryStyleSheet && { viewport }
84
+ )
85
+ const maxFontSizeMultiplier = enableMediaQueryStyleSheet
86
+ ? getMaxFontMultiplier(themeTokens[viewport])
87
+ : getMaxFontMultiplier(themeTokens)
88
+ const textDecorationLine = strikeThrough ? 'line-through' : 'none'
89
+
90
+ let textStyles
91
+ let mediaIds
92
+
93
+ if (enableMediaQueryStyleSheet) {
94
+ const transformedThemeTokens = Object.entries(themeTokens).reduce(
95
+ (acc, [vp, viewportTokens]) => {
96
+ acc[vp] = selectTextStyles(
97
+ {
98
+ textAlign: align,
99
+ textDecorationLine,
100
+ ...viewportTokens
101
+ },
102
+ themeOptions
103
+ )
104
+ return acc
105
+ },
106
+ {}
107
+ )
108
+ const mediaQueryStyles = createMediaQueryStyles(transformedThemeTokens)
109
+ const { ids, styles } = StyleSheet.create({
110
+ text: mediaQueryStyles
111
+ })
112
+ textStyles = styles.text
113
+ mediaIds = ids.text
114
+ } else {
115
+ textStyles = selectTextStyles(
116
+ {
117
+ textAlign: align,
118
+ textDecorationLine,
119
+ ...themeTokens
120
+ },
121
+ themeOptions
122
+ )
123
+ }
124
+
67
125
  const resolvedTextProps = {
68
126
  ...selectTextProps(rest),
69
- style: selectTextStyles(
70
- align ? { ...themeTokens, textAlign: align } : themeTokens,
71
- themeOptions
72
- ),
73
127
  dataSet,
74
- maxFontSizeMultiplier: getMaxFontMultiplier(themeTokens)
128
+ maxFontSizeMultiplier
75
129
  }
76
130
 
77
131
  const containerProps = {
@@ -83,7 +137,7 @@ const Typography = forwardRef(
83
137
  const resetTagStyling = (child) => {
84
138
  if (typeof child === 'object' && (child?.type === 'sub' || child?.type === 'sup')) {
85
139
  const childStyles = child?.props?.style || {}
86
- const supFontSize = childStyles.fontSize ?? superScriptFontSize
140
+ const supFontSize = childStyles.fontSize ?? themeTokens.superScriptFontSize
87
141
  const sanitizedChild = React.cloneElement(child, {
88
142
  style: {
89
143
  ...childStyles,
@@ -91,10 +145,8 @@ const Typography = forwardRef(
91
145
  lineHeight: 0
92
146
  }
93
147
  })
94
-
95
148
  return sanitizedChild
96
149
  }
97
-
98
150
  return child
99
151
  }
100
152
 
@@ -106,19 +158,20 @@ const Typography = forwardRef(
106
158
  return resetTagStyling(children)
107
159
  }
108
160
 
109
- const textDecorationLine = strikeThrough ? 'line-through' : 'none'
110
- const textStyles = resolvedTextProps.style
111
- ? { ...resolvedTextProps.style, textDecorationLine }
112
- : { textDecorationLine }
113
-
114
161
  return block ? (
115
162
  <View ref={ref} {...containerProps}>
116
- <Text {...resolvedTextProps} style={textStyles}>
163
+ <Text {...resolvedTextProps} style={textStyles} dataSet={mediaIds && { media: mediaIds }}>
117
164
  {sanitizeChildren(children)}
118
165
  </Text>
119
166
  </View>
120
167
  ) : (
121
- <Text ref={ref} {...containerProps} {...resolvedTextProps} style={textStyles}>
168
+ <Text
169
+ ref={ref}
170
+ {...containerProps}
171
+ {...resolvedTextProps}
172
+ style={textStyles}
173
+ dataSet={mediaIds && { media: mediaIds }}
174
+ >
122
175
  {sanitizeChildren(children)}
123
176
  </Text>
124
177
  )
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as ExpandCollapse, Accordion } from './ExpandCollapse'
17
17
  export { default as Feedback } from './Feedback'
18
18
  export { default as Fieldset } from './Fieldset'
19
19
  export { default as FlexGrid } from './FlexGrid'
20
+ export { default as Footnote } from './Footnote'
20
21
  export { default as HorizontalScroll } from './HorizontalScroll'
21
22
  export * from './HorizontalScroll'
22
23
  export { default as Icon } from './Icon'
package/src/server.js ADDED
@@ -0,0 +1,4 @@
1
+ export { default as selectSystemProps } from './utils/props/selectSystemProps'
2
+ export { getTokensPropType } from './utils/props/tokens'
3
+ export { default as htmlAttrs } from './utils/htmlAttrs'
4
+ export { getComponentTheme, getThemeTokens } from './ThemeProvider/utils/theme-tokens'