@telus-uds/components-base 3.20.0 → 3.22.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.
- package/CHANGELOG.md +27 -1
- package/lib/cjs/Button/Button.js +10 -3
- package/lib/cjs/Button/ButtonBase.js +53 -5
- package/lib/cjs/Card/PressableCardBase.js +3 -1
- package/lib/cjs/Carousel/Carousel.js +11 -3
- package/lib/cjs/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
- package/lib/cjs/StackView/StackView.js +62 -12
- package/lib/cjs/Tabs/TabsDropdown.js +4 -5
- package/lib/cjs/ThemeProvider/index.js +9 -1
- package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
- package/lib/cjs/index.js +7 -0
- package/lib/cjs/utils/ssr-media-query/index.js +7 -0
- package/lib/cjs/utils/ssr-media-query/utils/use-all-viewport-tokens.js +53 -0
- package/lib/esm/Button/Button.js +11 -4
- package/lib/esm/Button/ButtonBase.js +54 -6
- package/lib/esm/Card/PressableCardBase.js +3 -1
- package/lib/esm/Carousel/Carousel.js +11 -3
- package/lib/esm/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
- package/lib/esm/StackView/StackView.js +63 -13
- package/lib/esm/Tabs/TabsDropdown.js +4 -5
- package/lib/esm/ThemeProvider/index.js +1 -0
- package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
- package/lib/esm/index.js +1 -1
- package/lib/esm/utils/ssr-media-query/index.js +2 -1
- package/lib/esm/utils/ssr-media-query/utils/use-all-viewport-tokens.js +48 -0
- package/lib/package.json +4 -4
- package/package.json +4 -4
- package/src/Button/Button.jsx +24 -4
- package/src/Button/ButtonBase.jsx +61 -4
- package/src/Card/PressableCardBase.jsx +3 -1
- package/src/Carousel/Carousel.jsx +13 -2
- package/src/PriceLockup/utils/renderPrice.jsx +15 -17
- package/src/Responsive/ResponsiveWithMediaQueryStyleSheet.jsx +1 -1
- package/src/StackView/StackView.jsx +62 -9
- package/src/Tabs/TabsDropdown.jsx +10 -9
- package/src/ThemeProvider/index.js +1 -0
- package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
- package/src/index.js +2 -1
- package/src/utils/ssr-media-query/index.js +2 -1
- package/src/utils/ssr-media-query/utils/use-all-viewport-tokens.js +32 -0
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
viewProps,
|
|
17
17
|
wrapStringsInText,
|
|
18
18
|
withLinkRouter,
|
|
19
|
-
contentfulProps
|
|
19
|
+
contentfulProps,
|
|
20
|
+
createMediaQueryStyles,
|
|
21
|
+
StyleSheet as StyleSheetUtils
|
|
20
22
|
} from '../utils'
|
|
21
23
|
import { IconText } from '../Icon'
|
|
22
24
|
|
|
@@ -221,6 +223,9 @@ const ButtonBase = React.forwardRef(
|
|
|
221
223
|
},
|
|
222
224
|
ref
|
|
223
225
|
) => {
|
|
226
|
+
const { themeOptions } = useTheme()
|
|
227
|
+
const { viewport } = rawRest
|
|
228
|
+
const enableMediaQueryStyleSheet = themeOptions.enableMediaQueryStyleSheet && viewport
|
|
224
229
|
const { onPress, ...rest } = clickProps.toPressProps(rawRest)
|
|
225
230
|
const extraButtonState = { inactive, selected, iconPosition }
|
|
226
231
|
const resolveButtonTokens = (pressableState) =>
|
|
@@ -228,9 +233,49 @@ const ButtonBase = React.forwardRef(
|
|
|
228
233
|
|
|
229
234
|
const systemProps = selectProps(rest)
|
|
230
235
|
|
|
236
|
+
let layoutMediaQueryStyles
|
|
237
|
+
let flexAndWidthStylesIds
|
|
238
|
+
|
|
239
|
+
if (enableMediaQueryStyleSheet) {
|
|
240
|
+
const defaultPressableState = { pressed: false, hovered: false, focused: false }
|
|
241
|
+
const defaultTokensByViewport = resolveButtonTokens(defaultPressableState)
|
|
242
|
+
|
|
243
|
+
const layoutTokensByViewport = Object.entries(defaultTokensByViewport).reduce(
|
|
244
|
+
(acc, [vp, viewportTokens]) => {
|
|
245
|
+
const flexAndWidthStyles =
|
|
246
|
+
viewportTokens.width === '100%' && viewportTokens.flex === 1
|
|
247
|
+
? selectFlexAndWidthStyles(viewportTokens)
|
|
248
|
+
: {}
|
|
249
|
+
|
|
250
|
+
acc[vp] = {
|
|
251
|
+
...staticStyles.row,
|
|
252
|
+
...selectWebOnlyStyles(inactive, viewportTokens, systemProps),
|
|
253
|
+
...(Object.keys(flexAndWidthStyles).length > 0 ? flexAndWidthStyles : {}),
|
|
254
|
+
...selectOuterSizeStyles(viewportTokens)
|
|
255
|
+
}
|
|
256
|
+
return acc
|
|
257
|
+
},
|
|
258
|
+
{}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const mediaQueryStyles = createMediaQueryStyles(layoutTokensByViewport)
|
|
262
|
+
const { ids, styles } = StyleSheetUtils.create({
|
|
263
|
+
layout: {
|
|
264
|
+
...mediaQueryStyles
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
layoutMediaQueryStyles = styles.layout
|
|
269
|
+
flexAndWidthStylesIds = ids.layout
|
|
270
|
+
}
|
|
271
|
+
|
|
231
272
|
const getPressableStyle = (pressableState) => {
|
|
273
|
+
if (enableMediaQueryStyleSheet) {
|
|
274
|
+
const themeTokens = resolveButtonTokens(pressableState)[viewport]
|
|
275
|
+
return [layoutMediaQueryStyles, selectOuterContainerStyles(themeTokens)]
|
|
276
|
+
}
|
|
277
|
+
|
|
232
278
|
const themeTokens = resolveButtonTokens(pressableState)
|
|
233
|
-
// Only apply flex and width styles when they are explicitly set (e.g., from ButtonGroup with width: 'equal') to not to affect other use cases
|
|
234
279
|
const flexAndWidthStyles =
|
|
235
280
|
themeTokens.width === '100%' && themeTokens.flex === 1
|
|
236
281
|
? selectFlexAndWidthStyles(themeTokens)
|
|
@@ -244,7 +289,16 @@ const ButtonBase = React.forwardRef(
|
|
|
244
289
|
selectOuterSizeStyles(themeTokens)
|
|
245
290
|
]
|
|
246
291
|
}
|
|
247
|
-
|
|
292
|
+
|
|
293
|
+
const dataSetProp =
|
|
294
|
+
flexAndWidthStylesIds || rawRest.dataSet
|
|
295
|
+
? {
|
|
296
|
+
dataSet: {
|
|
297
|
+
...(flexAndWidthStylesIds ? { media: flexAndWidthStylesIds } : {}),
|
|
298
|
+
...rawRest.dataSet
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
: {}
|
|
248
302
|
|
|
249
303
|
return (
|
|
250
304
|
<Pressable
|
|
@@ -255,9 +309,12 @@ const ButtonBase = React.forwardRef(
|
|
|
255
309
|
disabled={inactive}
|
|
256
310
|
hrefAttrs={hrefAttrs}
|
|
257
311
|
{...systemProps}
|
|
312
|
+
{...dataSetProp}
|
|
258
313
|
>
|
|
259
314
|
{(pressableState) => {
|
|
260
|
-
const themeTokens =
|
|
315
|
+
const themeTokens = enableMediaQueryStyleSheet
|
|
316
|
+
? resolveButtonTokens(pressableState)[viewport]
|
|
317
|
+
: resolveButtonTokens(pressableState)
|
|
261
318
|
const containerStyles = selectInnerContainerStyles(themeTokens)
|
|
262
319
|
const borderStyles = selectBorderStyles(themeTokens)
|
|
263
320
|
const textStyles = [
|
|
@@ -399,6 +399,7 @@ const Carousel = React.forwardRef(
|
|
|
399
399
|
copy,
|
|
400
400
|
slideDuration = 0,
|
|
401
401
|
transitionDuration = 0,
|
|
402
|
+
loopDuration = transitionDuration,
|
|
402
403
|
autoPlay = false,
|
|
403
404
|
enablePeeking = false,
|
|
404
405
|
...rest
|
|
@@ -533,6 +534,8 @@ const Carousel = React.forwardRef(
|
|
|
533
534
|
|
|
534
535
|
const animate = React.useCallback(
|
|
535
536
|
(panToAnimate, toValue, toIndex) => {
|
|
537
|
+
const applicableTransitionDuration =
|
|
538
|
+
isLastSlide && toIndex === 0 ? loopDuration : transitionDuration
|
|
536
539
|
const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
|
|
537
540
|
if (reduceMotionRef.current || isSwiping.current) {
|
|
538
541
|
Animated.timing(panToAnimate, { toValue, duration: 1, useNativeDriver: false }).start(
|
|
@@ -543,14 +546,14 @@ const Carousel = React.forwardRef(
|
|
|
543
546
|
...springConfig,
|
|
544
547
|
toValue,
|
|
545
548
|
useNativeDriver: false,
|
|
546
|
-
duration:
|
|
549
|
+
duration: applicableTransitionDuration * 1000
|
|
547
550
|
}).start(handleAnimationEndToIndex)
|
|
548
551
|
} else if (enablePeeking || enableDisplayMultipleItemsPerSlide) {
|
|
549
552
|
Animated.timing(panToAnimate, {
|
|
550
553
|
...springConfig,
|
|
551
554
|
toValue,
|
|
552
555
|
useNativeDriver: false,
|
|
553
|
-
duration:
|
|
556
|
+
duration: applicableTransitionDuration ? applicableTransitionDuration * 1000 : 1000
|
|
554
557
|
}).start(handleAnimationEndToIndex)
|
|
555
558
|
} else {
|
|
556
559
|
Animated.spring(panToAnimate, {
|
|
@@ -564,6 +567,8 @@ const Carousel = React.forwardRef(
|
|
|
564
567
|
springConfig,
|
|
565
568
|
handleAnimationEnd,
|
|
566
569
|
transitionDuration,
|
|
570
|
+
loopDuration,
|
|
571
|
+
isLastSlide,
|
|
567
572
|
isAutoPlayEnabled,
|
|
568
573
|
enablePeeking,
|
|
569
574
|
enableDisplayMultipleItemsPerSlide
|
|
@@ -1390,6 +1395,12 @@ Carousel.propTypes = {
|
|
|
1390
1395
|
* - `autoPlay` and `slideDuration` are required to be set for this to work
|
|
1391
1396
|
*/
|
|
1392
1397
|
transitionDuration: PropTypes.number,
|
|
1398
|
+
/**
|
|
1399
|
+
* Time it takes in seconds to transition from last slide to first slide
|
|
1400
|
+
* - Default value equals `transitionDuration`'s value
|
|
1401
|
+
* - `autoPlay` and `transitionDuration` are required to be set for this to work
|
|
1402
|
+
*/
|
|
1403
|
+
loopDuration: PropTypes.number,
|
|
1393
1404
|
/**
|
|
1394
1405
|
* If set to `true`, the Carousel will show the previous and next slides
|
|
1395
1406
|
* - Default value is `false`
|
|
@@ -102,28 +102,26 @@ const renderPrice = (
|
|
|
102
102
|
)}
|
|
103
103
|
</Text>
|
|
104
104
|
) : null}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
<Text>
|
|
106
|
+
{renderAmount(
|
|
107
|
+
amount,
|
|
108
|
+
amountTypographyTokens,
|
|
109
|
+
strikeThrough,
|
|
110
|
+
a11yText,
|
|
111
|
+
fontColor,
|
|
112
|
+
themeTokens
|
|
113
|
+
)}
|
|
114
|
+
{isPriceBaseline &&
|
|
115
|
+
cents &&
|
|
116
|
+
renderAmount(
|
|
117
|
+
`${separator}${cents}`,
|
|
118
|
+
centsTypographyTokens,
|
|
110
119
|
strikeThrough,
|
|
111
120
|
a11yText,
|
|
112
121
|
fontColor,
|
|
113
122
|
themeTokens
|
|
114
123
|
)}
|
|
115
|
-
|
|
116
|
-
cents &&
|
|
117
|
-
renderAmount(
|
|
118
|
-
`${separator}${cents}`,
|
|
119
|
-
centsTypographyTokens,
|
|
120
|
-
strikeThrough,
|
|
121
|
-
a11yText,
|
|
122
|
-
fontColor,
|
|
123
|
-
themeTokens
|
|
124
|
-
)}
|
|
125
|
-
</Text>
|
|
126
|
-
}
|
|
124
|
+
</Text>
|
|
127
125
|
{cents && !isPriceBaseline
|
|
128
126
|
? renderTypography(`${separator}${cents}`, centsTypographyTokens, undefined, fontColor)
|
|
129
127
|
: null}
|
|
@@ -59,7 +59,7 @@ ResponsiveWithMediaQueryStyleSheet.propTypes = {
|
|
|
59
59
|
/**
|
|
60
60
|
* To hide children of `Responsive` if the current viewport is larger than `max`
|
|
61
61
|
*/
|
|
62
|
-
max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
|
|
62
|
+
max: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
|
|
63
63
|
inheritedStyles: PropTypes.arrayOf(PropTypes.string),
|
|
64
64
|
children: PropTypes.node.isRequired
|
|
65
65
|
}
|
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
spacingProps,
|
|
14
14
|
useResponsiveProp,
|
|
15
15
|
variantProp,
|
|
16
|
-
viewProps
|
|
16
|
+
viewProps,
|
|
17
|
+
StyleSheet as StyleSheetUtils,
|
|
18
|
+
createMediaQueryStyles,
|
|
19
|
+
useAllViewportTokens
|
|
17
20
|
} from '../utils'
|
|
18
|
-
import
|
|
19
|
-
import { useViewport } from '../ViewportProvider'
|
|
21
|
+
import useTheme from '../ThemeProvider/useTheme'
|
|
20
22
|
import getStackedContent from './getStackedContent'
|
|
21
23
|
import { staticStyles, selectFlexStyles } from './common'
|
|
22
24
|
|
|
@@ -75,24 +77,70 @@ const StackView = React.forwardRef(
|
|
|
75
77
|
tokens,
|
|
76
78
|
tag,
|
|
77
79
|
accessibilityRole,
|
|
80
|
+
dataSet,
|
|
78
81
|
...rest
|
|
79
82
|
},
|
|
80
83
|
ref
|
|
81
84
|
) => {
|
|
82
|
-
const viewport = useViewport()
|
|
83
85
|
const direction = useResponsiveProp(directionProp, 'column')
|
|
86
|
+
const {
|
|
87
|
+
themeOptions: { enableMediaQueryStyleSheet }
|
|
88
|
+
} = useTheme()
|
|
89
|
+
|
|
84
90
|
const selectedProps = selectProps({
|
|
85
91
|
accessibilityRole,
|
|
86
92
|
...getA11yPropsFromHtmlTag(tag, accessibilityRole),
|
|
87
93
|
...rest
|
|
88
94
|
})
|
|
89
95
|
const content = getStackedContent(children, { direction, divider, space })
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
96
|
+
|
|
97
|
+
const allTokens = useAllViewportTokens('StackView', tokens, variant)
|
|
98
|
+
|
|
99
|
+
let stackViewStyles
|
|
100
|
+
let dataSetValue = dataSet
|
|
101
|
+
|
|
102
|
+
if (enableMediaQueryStyleSheet) {
|
|
103
|
+
const stylesByViewport = {
|
|
104
|
+
xs: {
|
|
105
|
+
...selectFlexStyles(allTokens.xs),
|
|
106
|
+
width: allTokens.xs.width
|
|
107
|
+
},
|
|
108
|
+
sm: {
|
|
109
|
+
...selectFlexStyles(allTokens.sm),
|
|
110
|
+
width: allTokens.sm.width
|
|
111
|
+
},
|
|
112
|
+
md: {
|
|
113
|
+
...selectFlexStyles(allTokens.md),
|
|
114
|
+
width: allTokens.md.width
|
|
115
|
+
},
|
|
116
|
+
lg: {
|
|
117
|
+
...selectFlexStyles(allTokens.lg),
|
|
118
|
+
width: allTokens.lg.width
|
|
119
|
+
},
|
|
120
|
+
xl: {
|
|
121
|
+
...selectFlexStyles(allTokens.xl),
|
|
122
|
+
width: allTokens.xl.width
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const mediaQueryStyles = createMediaQueryStyles(stylesByViewport)
|
|
127
|
+
|
|
128
|
+
const { ids, styles } = StyleSheetUtils.create({
|
|
129
|
+
stackView: {
|
|
130
|
+
...mediaQueryStyles
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
stackViewStyles = [staticStyles[direction], styles.stackView]
|
|
135
|
+
dataSetValue = { media: ids.stackView, ...dataSet }
|
|
136
|
+
} else {
|
|
137
|
+
const flexStyles = selectFlexStyles(allTokens.current)
|
|
138
|
+
const size = { width: allTokens.current.width }
|
|
139
|
+
stackViewStyles = [flexStyles, staticStyles[direction], size]
|
|
140
|
+
}
|
|
93
141
|
|
|
94
142
|
return (
|
|
95
|
-
<View ref={ref} {...selectedProps} style={
|
|
143
|
+
<View ref={ref} {...selectedProps} style={stackViewStyles} dataSet={dataSetValue}>
|
|
96
144
|
{content}
|
|
97
145
|
</View>
|
|
98
146
|
)
|
|
@@ -132,7 +180,12 @@ StackView.propTypes = {
|
|
|
132
180
|
* A StackView may take any children, but will have no effect if it is only passed one child or is passed children
|
|
133
181
|
* wrapped in a component. If necessary, children may be wrapped in one React Fragment.
|
|
134
182
|
*/
|
|
135
|
-
children: PropTypes.node
|
|
183
|
+
children: PropTypes.node,
|
|
184
|
+
/**
|
|
185
|
+
* Data attributes to be applied to the element. When media query stylesheet is enabled,
|
|
186
|
+
* this will include media query IDs for responsive styling.
|
|
187
|
+
*/
|
|
188
|
+
dataSet: PropTypes.object
|
|
136
189
|
}
|
|
137
190
|
|
|
138
191
|
export default StackView
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
|
3
|
+
import { Platform, Pressable, StyleSheet, Text, View } from 'react-native'
|
|
4
4
|
import { useThemeTokensCallback, applyTextStyles, useTheme } from '../ThemeProvider'
|
|
5
5
|
import {
|
|
6
6
|
a11yProps,
|
|
@@ -245,20 +245,21 @@ const styles = StyleSheet.create({
|
|
|
245
245
|
position: 'relative',
|
|
246
246
|
width: '100%'
|
|
247
247
|
},
|
|
248
|
-
pressable:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
pressable:
|
|
249
|
+
Platform.OS === 'web'
|
|
250
|
+
? {
|
|
251
|
+
outlineWidth: 0,
|
|
252
|
+
outlineStyle: 'none',
|
|
253
|
+
outlineColor: 'transparent'
|
|
254
|
+
}
|
|
255
|
+
: {},
|
|
253
256
|
buttonContent: {
|
|
254
257
|
display: 'flex',
|
|
255
258
|
flexDirection: 'row',
|
|
256
259
|
alignItems: 'center',
|
|
257
260
|
justifyContent: 'space-between',
|
|
258
261
|
width: '100%',
|
|
259
|
-
minHeight: 44
|
|
260
|
-
outline: 'none',
|
|
261
|
-
boxSizing: 'border-box'
|
|
262
|
+
minHeight: 44
|
|
262
263
|
}
|
|
263
264
|
})
|
|
264
265
|
|
|
@@ -3,6 +3,7 @@ import ThemeProvider from './ThemeProvider'
|
|
|
3
3
|
export { default as useTheme } from './useTheme'
|
|
4
4
|
export { default as useSetTheme } from './useSetTheme'
|
|
5
5
|
export { default as useResponsiveThemeTokens } from './useResponsiveThemeTokens'
|
|
6
|
+
export { default as useResponsiveThemeTokensCallback } from './useResponsiveThemeTokensCallback'
|
|
6
7
|
export * from './useThemeTokens'
|
|
7
8
|
|
|
8
9
|
export * from './utils'
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useCallback } from 'react'
|
|
2
|
+
import { viewports } from '@telus-uds/system-constants'
|
|
3
|
+
import useTheme from './useTheme'
|
|
4
|
+
import { getComponentTheme, mergeAppearances, resolveThemeTokens } from './utils'
|
|
5
|
+
|
|
6
|
+
const getResponsiveThemeTokens = (
|
|
7
|
+
{ rules = [], tokens: defaultThemeTokens = {} },
|
|
8
|
+
tokensProp,
|
|
9
|
+
variants = {},
|
|
10
|
+
states
|
|
11
|
+
) => {
|
|
12
|
+
const appearances = mergeAppearances(variants, states)
|
|
13
|
+
|
|
14
|
+
const tokensByViewport = Object.fromEntries(
|
|
15
|
+
viewports.keys.map((viewport) => [viewport, { ...defaultThemeTokens }])
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// Go through each rule and collect them for the corresponding viewport if they apply
|
|
19
|
+
rules.forEach((rule) => {
|
|
20
|
+
if (doesRuleApply(rule, appearances)) {
|
|
21
|
+
// If the rule does not have a viewport specified, we collect it in all viewports
|
|
22
|
+
let targetViewports = rule.if.viewport || viewports.keys
|
|
23
|
+
if (!Array.isArray(targetViewports)) {
|
|
24
|
+
targetViewports = [targetViewports]
|
|
25
|
+
}
|
|
26
|
+
targetViewports.forEach((viewport) => {
|
|
27
|
+
tokensByViewport[viewport] = { ...tokensByViewport[viewport], ...rule.tokens }
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
Object.keys(tokensByViewport).forEach((viewport) => {
|
|
33
|
+
tokensByViewport[viewport] = resolveThemeTokens(
|
|
34
|
+
tokensByViewport[viewport],
|
|
35
|
+
appearances,
|
|
36
|
+
tokensProp
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return tokensByViewport
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const doesRuleApply = (rule, appearances) =>
|
|
44
|
+
Object.entries(rule.if).every((condition) => doesConditionApply(condition, appearances))
|
|
45
|
+
|
|
46
|
+
const doesConditionApply = ([key, value], appearances) => {
|
|
47
|
+
if (key === 'viewport') {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
// use null rather than undefined so we can serialise the value in themes
|
|
51
|
+
const appearanceValue = appearances[key] ?? null
|
|
52
|
+
return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
|
|
57
|
+
* @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
|
|
58
|
+
* @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns a memoised tokens getter function that gets responsive tokens for all viewports,
|
|
63
|
+
* similar to calling useResponsiveThemeTokens but with the callback pattern of useThemeTokensCallback.
|
|
64
|
+
*
|
|
65
|
+
* Scenarios where `useResponsiveThemeTokensCallback` should be used:
|
|
66
|
+
*
|
|
67
|
+
* - Where responsive tokens are to be obtained from state that is accessible only in scopes like callbacks
|
|
68
|
+
* and render functions, where calling useResponsiveThemeTokens directly would be disallowed by React's hook rules.
|
|
69
|
+
* - When using media query stylesheets and need to resolve tokens based on dynamic state (e.g., pressed, hovered)
|
|
70
|
+
* that changes at runtime.
|
|
71
|
+
* - Passing a responsive tokens getter down via a child component's `tokens` prop, applying rules using the
|
|
72
|
+
* child component's current state.
|
|
73
|
+
*
|
|
74
|
+
* The function returned may be called with an object of state appearances to get an object
|
|
75
|
+
* of tokens for each viewport, which can then be passed to createMediaQueryStyles.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Resolving responsive tokens inside Pressable's style function, based on Pressable state
|
|
79
|
+
* const PressMe = ({ tokens, variant, children }) => {
|
|
80
|
+
* const getResponsiveTokens = useResponsiveThemeTokensCallback('PressMe', tokens, variant)
|
|
81
|
+
* const getPressableStyle = ({ pressed }) => {
|
|
82
|
+
* const responsiveTokens = getResponsiveTokens({ pressed })
|
|
83
|
+
* const mediaQueryStyles = createMediaQueryStyles(responsiveTokens)
|
|
84
|
+
* return mediaQueryStyles
|
|
85
|
+
* }
|
|
86
|
+
* return <Pressable style={getPressableStyle}>{children}</Pressable>
|
|
87
|
+
* }
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Setting the theme in a parent and resolving it in a child based on child's state
|
|
91
|
+
* const MenuButton = ({ tokens, variant, ...buttonProps }) => {
|
|
92
|
+
* // Define what theme, variant etc we want in this component...
|
|
93
|
+
* const getResponsiveTokens = useResponsiveThemeTokensCallback('Button', tokens, variant)
|
|
94
|
+
* // ...resolve them in another component based on its state (e.g. press, hover...)
|
|
95
|
+
* return <ButtonBase tokens={getResponsiveTokens} accessibilityRole="menuitem" {...buttonProps} />
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* @typedef {Object} ResponsiveObject
|
|
99
|
+
* @property {TokensSet} xs
|
|
100
|
+
* @property {TokensSet} sm
|
|
101
|
+
* @property {TokensSet} md
|
|
102
|
+
* @property {TokensSet} lg
|
|
103
|
+
* @property {TokensSet} xl
|
|
104
|
+
*
|
|
105
|
+
* @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
|
|
106
|
+
* @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
|
|
107
|
+
* @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
|
|
108
|
+
* @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => ResponsiveObject}
|
|
109
|
+
* - callback function that returns an overridable responsive tokens object for current state. Only pass
|
|
110
|
+
* tokenOverrides in rare cases where tokens overrides are also generated outside hook scope.
|
|
111
|
+
*/
|
|
112
|
+
const useResponsiveThemeTokensCallback = (componentName, tokens = {}, variants = {}) => {
|
|
113
|
+
const theme = useTheme()
|
|
114
|
+
const componentTheme = getComponentTheme(theme, componentName)
|
|
115
|
+
const getResponsiveThemeTokensCallback = useCallback(
|
|
116
|
+
(states, tokenOverrides) => {
|
|
117
|
+
const resolvedTokens = resolveThemeTokens(
|
|
118
|
+
tokens,
|
|
119
|
+
mergeAppearances(variants, states),
|
|
120
|
+
tokenOverrides
|
|
121
|
+
)
|
|
122
|
+
return getResponsiveThemeTokens(componentTheme, resolvedTokens, variants, states)
|
|
123
|
+
},
|
|
124
|
+
[componentTheme, tokens, variants]
|
|
125
|
+
)
|
|
126
|
+
return getResponsiveThemeTokensCallback
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default useResponsiveThemeTokensCallback
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import createStyleSheet from './create-stylesheet'
|
|
2
2
|
import createMediaQueryStyles from './utils/create-media-query-styles'
|
|
3
|
+
import useAllViewportTokens from './utils/use-all-viewport-tokens'
|
|
3
4
|
|
|
4
5
|
const StyleSheet = {
|
|
5
6
|
create: createStyleSheet
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export { StyleSheet, createMediaQueryStyles }
|
|
9
|
+
export { StyleSheet, createMediaQueryStyles, useAllViewportTokens }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useThemeTokens } from '../../../ThemeProvider'
|
|
2
|
+
import { useViewport } from '../../../ViewportProvider'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to get theme tokens for all viewports at once.
|
|
6
|
+
* This is useful for components that need to support React Native Media Queries (RNMQ).
|
|
7
|
+
*
|
|
8
|
+
* All hooks are called unconditionally to comply with React's Rules of Hooks.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} componentName - The name of the component to get tokens for
|
|
11
|
+
* @param {object|function} tokens - Custom tokens or token function
|
|
12
|
+
* @param {object} variant - Variant configuration
|
|
13
|
+
* @returns {object} Object with tokens for each viewport (xs, sm, md, lg, xl, current)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const allTokens = useAllViewportTokens('StackView', tokens, variant)
|
|
17
|
+
* // Returns: { xs: {...}, sm: {...}, md: {...}, lg: {...}, xl: {...}, current: {...} }
|
|
18
|
+
*/
|
|
19
|
+
const useAllViewportTokens = (componentName, tokens, variant) => {
|
|
20
|
+
const viewport = useViewport()
|
|
21
|
+
|
|
22
|
+
const xs = useThemeTokens(componentName, tokens, variant, { viewport: 'xs' })
|
|
23
|
+
const sm = useThemeTokens(componentName, tokens, variant, { viewport: 'sm' })
|
|
24
|
+
const md = useThemeTokens(componentName, tokens, variant, { viewport: 'md' })
|
|
25
|
+
const lg = useThemeTokens(componentName, tokens, variant, { viewport: 'lg' })
|
|
26
|
+
const xl = useThemeTokens(componentName, tokens, variant, { viewport: 'xl' })
|
|
27
|
+
const current = useThemeTokens(componentName, tokens, variant, { viewport })
|
|
28
|
+
|
|
29
|
+
return { xs, sm, md, lg, xl, current }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default useAllViewportTokens
|