@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/lib/cjs/Button/Button.js +10 -3
  3. package/lib/cjs/Button/ButtonBase.js +53 -5
  4. package/lib/cjs/Card/PressableCardBase.js +3 -1
  5. package/lib/cjs/Carousel/Carousel.js +11 -3
  6. package/lib/cjs/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
  7. package/lib/cjs/StackView/StackView.js +62 -12
  8. package/lib/cjs/Tabs/TabsDropdown.js +4 -5
  9. package/lib/cjs/ThemeProvider/index.js +9 -1
  10. package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
  11. package/lib/cjs/index.js +7 -0
  12. package/lib/cjs/utils/ssr-media-query/index.js +7 -0
  13. package/lib/cjs/utils/ssr-media-query/utils/use-all-viewport-tokens.js +53 -0
  14. package/lib/esm/Button/Button.js +11 -4
  15. package/lib/esm/Button/ButtonBase.js +54 -6
  16. package/lib/esm/Card/PressableCardBase.js +3 -1
  17. package/lib/esm/Carousel/Carousel.js +11 -3
  18. package/lib/esm/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
  19. package/lib/esm/StackView/StackView.js +63 -13
  20. package/lib/esm/Tabs/TabsDropdown.js +4 -5
  21. package/lib/esm/ThemeProvider/index.js +1 -0
  22. package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
  23. package/lib/esm/index.js +1 -1
  24. package/lib/esm/utils/ssr-media-query/index.js +2 -1
  25. package/lib/esm/utils/ssr-media-query/utils/use-all-viewport-tokens.js +48 -0
  26. package/lib/package.json +4 -4
  27. package/package.json +4 -4
  28. package/src/Button/Button.jsx +24 -4
  29. package/src/Button/ButtonBase.jsx +61 -4
  30. package/src/Card/PressableCardBase.jsx +3 -1
  31. package/src/Carousel/Carousel.jsx +13 -2
  32. package/src/PriceLockup/utils/renderPrice.jsx +15 -17
  33. package/src/Responsive/ResponsiveWithMediaQueryStyleSheet.jsx +1 -1
  34. package/src/StackView/StackView.jsx +62 -9
  35. package/src/Tabs/TabsDropdown.jsx +10 -9
  36. package/src/ThemeProvider/index.js +1 -0
  37. package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
  38. package/src/index.js +2 -1
  39. package/src/utils/ssr-media-query/index.js +2 -1
  40. package/src/utils/ssr-media-query/utils/use-all-viewport-tokens.js +32 -0
@@ -6,7 +6,7 @@ import StyleSheet from "react-native-web/dist/exports/StyleSheet";
6
6
  import Platform from "react-native-web/dist/exports/Platform";
7
7
  import { applyTextStyles, applyShadowToken, applyOuterBorder, useTheme } from '../ThemeProvider';
8
8
  import buttonPropTypes from './propTypes';
9
- import { a11yProps, clickProps, focusHandlerProps, getCursorStyle, linkProps, resolvePressableState, resolvePressableTokens, selectSystemProps, viewProps, wrapStringsInText, withLinkRouter, contentfulProps } from '../utils';
9
+ import { a11yProps, clickProps, focusHandlerProps, getCursorStyle, linkProps, resolvePressableState, resolvePressableTokens, selectSystemProps, viewProps, wrapStringsInText, withLinkRouter, contentfulProps, createMediaQueryStyles, StyleSheet as StyleSheetUtils } from '../utils';
10
10
  import { IconText } from '../Icon';
11
11
  import { jsx as _jsx } from "react/jsx-runtime";
12
12
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, focusHandlerProps, linkProps, viewProps, contentfulProps]);
@@ -250,6 +250,13 @@ const ButtonBase = /*#__PURE__*/React.forwardRef((_ref12, ref) => {
250
250
  iconProps,
251
251
  ...rawRest
252
252
  } = _ref12;
253
+ const {
254
+ themeOptions
255
+ } = useTheme();
256
+ const {
257
+ viewport
258
+ } = rawRest;
259
+ const enableMediaQueryStyleSheet = themeOptions.enableMediaQueryStyleSheet && viewport;
253
260
  const {
254
261
  onPress,
255
262
  ...rest
@@ -261,15 +268,55 @@ const ButtonBase = /*#__PURE__*/React.forwardRef((_ref12, ref) => {
261
268
  };
262
269
  const resolveButtonTokens = pressableState => resolvePressableTokens(tokens, pressableState, extraButtonState);
263
270
  const systemProps = selectProps(rest);
271
+ let layoutMediaQueryStyles;
272
+ let flexAndWidthStylesIds;
273
+ if (enableMediaQueryStyleSheet) {
274
+ const defaultPressableState = {
275
+ pressed: false,
276
+ hovered: false,
277
+ focused: false
278
+ };
279
+ const defaultTokensByViewport = resolveButtonTokens(defaultPressableState);
280
+ const layoutTokensByViewport = Object.entries(defaultTokensByViewport).reduce((acc, _ref13) => {
281
+ let [vp, viewportTokens] = _ref13;
282
+ const flexAndWidthStyles = viewportTokens.width === '100%' && viewportTokens.flex === 1 ? selectFlexAndWidthStyles(viewportTokens) : {};
283
+ acc[vp] = {
284
+ ...staticStyles.row,
285
+ ...selectWebOnlyStyles(inactive, viewportTokens, systemProps),
286
+ ...(Object.keys(flexAndWidthStyles).length > 0 ? flexAndWidthStyles : {}),
287
+ ...selectOuterSizeStyles(viewportTokens)
288
+ };
289
+ return acc;
290
+ }, {});
291
+ const mediaQueryStyles = createMediaQueryStyles(layoutTokensByViewport);
292
+ const {
293
+ ids,
294
+ styles
295
+ } = StyleSheetUtils.create({
296
+ layout: {
297
+ ...mediaQueryStyles
298
+ }
299
+ });
300
+ layoutMediaQueryStyles = styles.layout;
301
+ flexAndWidthStylesIds = ids.layout;
302
+ }
264
303
  const getPressableStyle = pressableState => {
304
+ if (enableMediaQueryStyleSheet) {
305
+ const themeTokens = resolveButtonTokens(pressableState)[viewport];
306
+ return [layoutMediaQueryStyles, selectOuterContainerStyles(themeTokens)];
307
+ }
265
308
  const themeTokens = resolveButtonTokens(pressableState);
266
- // 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
267
309
  const flexAndWidthStyles = themeTokens.width === '100%' && themeTokens.flex === 1 ? selectFlexAndWidthStyles(themeTokens) : {};
268
310
  return [staticStyles.row, selectWebOnlyStyles(inactive, themeTokens, systemProps), selectOuterContainerStyles(themeTokens), ...(Object.keys(flexAndWidthStyles).length > 0 ? [flexAndWidthStyles] : []), selectOuterSizeStyles(themeTokens)];
269
311
  };
270
- const {
271
- themeOptions
272
- } = useTheme();
312
+ const dataSetProp = flexAndWidthStylesIds || rawRest.dataSet ? {
313
+ dataSet: {
314
+ ...(flexAndWidthStylesIds ? {
315
+ media: flexAndWidthStylesIds
316
+ } : {}),
317
+ ...rawRest.dataSet
318
+ }
319
+ } : {};
273
320
  return /*#__PURE__*/_jsx(Pressable, {
274
321
  ref: ref,
275
322
  href: href,
@@ -281,8 +328,9 @@ const ButtonBase = /*#__PURE__*/React.forwardRef((_ref12, ref) => {
281
328
  disabled: inactive,
282
329
  hrefAttrs: hrefAttrs,
283
330
  ...systemProps,
331
+ ...dataSetProp,
284
332
  children: pressableState => {
285
- const themeTokens = resolveButtonTokens(pressableState);
333
+ const themeTokens = enableMediaQueryStyleSheet ? resolveButtonTokens(pressableState)[viewport] : resolveButtonTokens(pressableState);
286
334
  const containerStyles = selectInnerContainerStyles(themeTokens);
287
335
  const borderStyles = selectBorderStyles(themeTokens);
288
336
  const textStyles = [selectTextStyles(themeTokens, themeOptions), staticStyles.text, Platform.select({
@@ -173,7 +173,9 @@ const staticStyles = StyleSheet.create({
173
173
  },
174
174
  linkContainer: {
175
175
  flex: 1,
176
- display: 'flex'
176
+ display: 'flex',
177
+ alignItems: 'stretch',
178
+ justifyContent: 'flex-start'
177
179
  }
178
180
  });
179
181
  PressableCardBase.displayName = 'PressableCardBase';
@@ -351,6 +351,7 @@ const Carousel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
351
351
  copy,
352
352
  slideDuration = 0,
353
353
  transitionDuration = 0,
354
+ loopDuration = transitionDuration,
354
355
  autoPlay = false,
355
356
  enablePeeking = false,
356
357
  ...rest
@@ -470,6 +471,7 @@ const Carousel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
470
471
  }
471
472
  }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking]);
472
473
  const animate = React.useCallback((panToAnimate, toValue, toIndex) => {
474
+ const applicableTransitionDuration = isLastSlide && toIndex === 0 ? loopDuration : transitionDuration;
473
475
  const handleAnimationEndToIndex = function () {
474
476
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
475
477
  args[_key] = arguments[_key];
@@ -487,14 +489,14 @@ const Carousel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
487
489
  ...springConfig,
488
490
  toValue,
489
491
  useNativeDriver: false,
490
- duration: transitionDuration * 1000
492
+ duration: applicableTransitionDuration * 1000
491
493
  }).start(handleAnimationEndToIndex);
492
494
  } else if (enablePeeking || enableDisplayMultipleItemsPerSlide) {
493
495
  Animated.timing(panToAnimate, {
494
496
  ...springConfig,
495
497
  toValue,
496
498
  useNativeDriver: false,
497
- duration: transitionDuration ? transitionDuration * 1000 : 1000
499
+ duration: applicableTransitionDuration ? applicableTransitionDuration * 1000 : 1000
498
500
  }).start(handleAnimationEndToIndex);
499
501
  } else {
500
502
  Animated.spring(panToAnimate, {
@@ -503,7 +505,7 @@ const Carousel = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
503
505
  useNativeDriver: false
504
506
  }).start(handleAnimationEndToIndex);
505
507
  }
506
- }, [springConfig, handleAnimationEnd, transitionDuration, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
508
+ }, [springConfig, handleAnimationEnd, transitionDuration, loopDuration, isLastSlide, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
507
509
  const stopAutoplay = React.useCallback(() => {
508
510
  if (autoPlayRef?.current) {
509
511
  clearTimeout(autoPlayRef?.current);
@@ -1197,6 +1199,12 @@ Carousel.propTypes = {
1197
1199
  * - `autoPlay` and `slideDuration` are required to be set for this to work
1198
1200
  */
1199
1201
  transitionDuration: PropTypes.number,
1202
+ /**
1203
+ * Time it takes in seconds to transition from last slide to first slide
1204
+ * - Default value equals `transitionDuration`'s value
1205
+ * - `autoPlay` and `transitionDuration` are required to be set for this to work
1206
+ */
1207
+ loopDuration: PropTypes.number,
1200
1208
  /**
1201
1209
  * If set to `true`, the Carousel will show the previous and next slides
1202
1210
  * - Default value is `false`
@@ -61,7 +61,7 @@ ResponsiveWithMediaQueryStyleSheet.propTypes = {
61
61
  /**
62
62
  * To hide children of `Responsive` if the current viewport is larger than `max`
63
63
  */
64
- max: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
64
+ max: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
65
65
  inheritedStyles: PropTypes.arrayOf(PropTypes.string),
66
66
  children: PropTypes.node.isRequired
67
67
  };
@@ -2,9 +2,8 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import View from "react-native-web/dist/exports/View";
4
4
  import Divider from '../Divider';
5
- import { a11yProps, getA11yPropsFromHtmlTag, getTokensPropType, layoutTags, responsiveProps, selectSystemProps, spacingProps, useResponsiveProp, variantProp, viewProps } from '../utils';
6
- import { useThemeTokens } from '../ThemeProvider';
7
- import { useViewport } from '../ViewportProvider';
5
+ import { a11yProps, getA11yPropsFromHtmlTag, getTokensPropType, layoutTags, responsiveProps, selectSystemProps, spacingProps, useResponsiveProp, variantProp, viewProps, StyleSheet as StyleSheetUtils, createMediaQueryStyles, useAllViewportTokens } from '../utils';
6
+ import useTheme from '../ThemeProvider/useTheme';
8
7
  import getStackedContent from './getStackedContent';
9
8
  import { staticStyles, selectFlexStyles } from './common';
10
9
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -62,10 +61,15 @@ const StackView = /*#__PURE__*/React.forwardRef((_ref, ref) => {
62
61
  tokens,
63
62
  tag,
64
63
  accessibilityRole,
64
+ dataSet,
65
65
  ...rest
66
66
  } = _ref;
67
- const viewport = useViewport();
68
67
  const direction = useResponsiveProp(directionProp, 'column');
68
+ const {
69
+ themeOptions: {
70
+ enableMediaQueryStyleSheet
71
+ }
72
+ } = useTheme();
69
73
  const selectedProps = selectProps({
70
74
  accessibilityRole,
71
75
  ...getA11yPropsFromHtmlTag(tag, accessibilityRole),
@@ -76,17 +80,58 @@ const StackView = /*#__PURE__*/React.forwardRef((_ref, ref) => {
76
80
  divider,
77
81
  space
78
82
  });
79
- const themeTokens = useThemeTokens('StackView', tokens, variant, {
80
- viewport
81
- });
82
- const flexStyles = selectFlexStyles(themeTokens);
83
- const size = {
84
- width: themeTokens.width
85
- };
83
+ const allTokens = useAllViewportTokens('StackView', tokens, variant);
84
+ let stackViewStyles;
85
+ let dataSetValue = dataSet;
86
+ if (enableMediaQueryStyleSheet) {
87
+ const stylesByViewport = {
88
+ xs: {
89
+ ...selectFlexStyles(allTokens.xs),
90
+ width: allTokens.xs.width
91
+ },
92
+ sm: {
93
+ ...selectFlexStyles(allTokens.sm),
94
+ width: allTokens.sm.width
95
+ },
96
+ md: {
97
+ ...selectFlexStyles(allTokens.md),
98
+ width: allTokens.md.width
99
+ },
100
+ lg: {
101
+ ...selectFlexStyles(allTokens.lg),
102
+ width: allTokens.lg.width
103
+ },
104
+ xl: {
105
+ ...selectFlexStyles(allTokens.xl),
106
+ width: allTokens.xl.width
107
+ }
108
+ };
109
+ const mediaQueryStyles = createMediaQueryStyles(stylesByViewport);
110
+ const {
111
+ ids,
112
+ styles
113
+ } = StyleSheetUtils.create({
114
+ stackView: {
115
+ ...mediaQueryStyles
116
+ }
117
+ });
118
+ stackViewStyles = [staticStyles[direction], styles.stackView];
119
+ dataSetValue = {
120
+ media: ids.stackView,
121
+ ...dataSet
122
+ };
123
+ } else {
124
+ const flexStyles = selectFlexStyles(allTokens.current);
125
+ const size = {
126
+ width: allTokens.current.width
127
+ };
128
+ stackViewStyles = [flexStyles, staticStyles[direction], size];
129
+ }
86
130
  return /*#__PURE__*/_jsx(View, {
87
131
  ref: ref,
88
132
  ...selectedProps,
89
- style: [flexStyles, staticStyles[direction], size],
133
+ style: stackViewStyles,
134
+ dataSet: dataSetValue,
90
135
  children: content
91
136
  });
92
137
  });
@@ -121,6 +166,11 @@ StackView.propTypes = {
121
166
  * A StackView may take any children, but will have no effect if it is only passed one child or is passed children
122
167
  * wrapped in a component. If necessary, children may be wrapped in one React Fragment.
123
168
  */
124
- children: PropTypes.node
169
+ children: PropTypes.node,
170
+ /**
171
+ * Data attributes to be applied to the element. When media query stylesheet is enabled,
172
+ * this will include media query IDs for responsive styling.
173
+ */
174
+ dataSet: PropTypes.object
125
175
  };
126
176
  export default StackView;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
+ import Platform from "react-native-web/dist/exports/Platform";
3
4
  import Pressable from "react-native-web/dist/exports/Pressable";
4
5
  import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
6
  import Text from "react-native-web/dist/exports/Text";
@@ -226,20 +227,18 @@ const styles = StyleSheet.create({
226
227
  position: 'relative',
227
228
  width: '100%'
228
229
  },
229
- pressable: {
230
+ pressable: Platform.OS === 'web' ? {
230
231
  outlineWidth: 0,
231
232
  outlineStyle: 'none',
232
233
  outlineColor: 'transparent'
233
- },
234
+ } : {},
234
235
  buttonContent: {
235
236
  display: 'flex',
236
237
  flexDirection: 'row',
237
238
  alignItems: 'center',
238
239
  justifyContent: 'space-between',
239
240
  width: '100%',
240
- minHeight: 44,
241
- outline: 'none',
242
- boxSizing: 'border-box'
241
+ minHeight: 44
243
242
  }
244
243
  });
245
244
  export default TabsDropdown;
@@ -2,6 +2,7 @@ import ThemeProvider from './ThemeProvider';
2
2
  export { default as useTheme } from './useTheme';
3
3
  export { default as useSetTheme } from './useSetTheme';
4
4
  export { default as useResponsiveThemeTokens } from './useResponsiveThemeTokens';
5
+ export { default as useResponsiveThemeTokensCallback } from './useResponsiveThemeTokensCallback';
5
6
  export * from './useThemeTokens';
6
7
  export * from './utils';
7
8
  export default ThemeProvider;
@@ -0,0 +1,117 @@
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
+ const getResponsiveThemeTokens = function (_ref, tokensProp) {
6
+ let {
7
+ rules = [],
8
+ tokens: defaultThemeTokens = {}
9
+ } = _ref;
10
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
11
+ let states = arguments.length > 3 ? arguments[3] : undefined;
12
+ const appearances = mergeAppearances(variants, states);
13
+ const tokensByViewport = Object.fromEntries(viewports.keys.map(viewport => [viewport, {
14
+ ...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] = {
27
+ ...tokensByViewport[viewport],
28
+ ...rule.tokens
29
+ };
30
+ });
31
+ }
32
+ });
33
+ Object.keys(tokensByViewport).forEach(viewport => {
34
+ tokensByViewport[viewport] = resolveThemeTokens(tokensByViewport[viewport], appearances, tokensProp);
35
+ });
36
+ return tokensByViewport;
37
+ };
38
+ const doesRuleApply = (rule, appearances) => Object.entries(rule.if).every(condition => doesConditionApply(condition, appearances));
39
+ const doesConditionApply = (_ref2, appearances) => {
40
+ let [key, value] = _ref2;
41
+ if (key === 'viewport') {
42
+ return true;
43
+ }
44
+ // use null rather than undefined so we can serialise the value in themes
45
+ const appearanceValue = appearances[key] ?? null;
46
+ return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue;
47
+ };
48
+
49
+ /**
50
+ * @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
51
+ * @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
52
+ * @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
53
+ */
54
+
55
+ /**
56
+ * Returns a memoised tokens getter function that gets responsive tokens for all viewports,
57
+ * similar to calling useResponsiveThemeTokens but with the callback pattern of useThemeTokensCallback.
58
+ *
59
+ * Scenarios where `useResponsiveThemeTokensCallback` should be used:
60
+ *
61
+ * - Where responsive tokens are to be obtained from state that is accessible only in scopes like callbacks
62
+ * and render functions, where calling useResponsiveThemeTokens directly would be disallowed by React's hook rules.
63
+ * - When using media query stylesheets and need to resolve tokens based on dynamic state (e.g., pressed, hovered)
64
+ * that changes at runtime.
65
+ * - Passing a responsive tokens getter down via a child component's `tokens` prop, applying rules using the
66
+ * child component's current state.
67
+ *
68
+ * The function returned may be called with an object of state appearances to get an object
69
+ * of tokens for each viewport, which can then be passed to createMediaQueryStyles.
70
+ *
71
+ * @example
72
+ * // Resolving responsive tokens inside Pressable's style function, based on Pressable state
73
+ * const PressMe = ({ tokens, variant, children }) => {
74
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('PressMe', tokens, variant)
75
+ * const getPressableStyle = ({ pressed }) => {
76
+ * const responsiveTokens = getResponsiveTokens({ pressed })
77
+ * const mediaQueryStyles = createMediaQueryStyles(responsiveTokens)
78
+ * return mediaQueryStyles
79
+ * }
80
+ * return <Pressable style={getPressableStyle}>{children}</Pressable>
81
+ * }
82
+ *
83
+ * @example
84
+ * // Setting the theme in a parent and resolving it in a child based on child's state
85
+ * const MenuButton = ({ tokens, variant, ...buttonProps }) => {
86
+ * // Define what theme, variant etc we want in this component...
87
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('Button', tokens, variant)
88
+ * // ...resolve them in another component based on its state (e.g. press, hover...)
89
+ * return <ButtonBase tokens={getResponsiveTokens} accessibilityRole="menuitem" {...buttonProps} />
90
+ * }
91
+ *
92
+ * @typedef {Object} ResponsiveObject
93
+ * @property {TokensSet} xs
94
+ * @property {TokensSet} sm
95
+ * @property {TokensSet} md
96
+ * @property {TokensSet} lg
97
+ * @property {TokensSet} xl
98
+ *
99
+ * @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
100
+ * @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
101
+ * @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
102
+ * @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => ResponsiveObject}
103
+ * - callback function that returns an overridable responsive tokens object for current state. Only pass
104
+ * tokenOverrides in rare cases where tokens overrides are also generated outside hook scope.
105
+ */
106
+ const useResponsiveThemeTokensCallback = function (componentName) {
107
+ let tokens = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
108
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
109
+ const theme = useTheme();
110
+ const componentTheme = getComponentTheme(theme, componentName);
111
+ const getResponsiveThemeTokensCallback = useCallback((states, tokenOverrides) => {
112
+ const resolvedTokens = resolveThemeTokens(tokens, mergeAppearances(variants, states), tokenOverrides);
113
+ return getResponsiveThemeTokens(componentTheme, resolvedTokens, variants, states);
114
+ }, [componentTheme, tokens, variants]);
115
+ return getResponsiveThemeTokensCallback;
116
+ };
117
+ export default useResponsiveThemeTokensCallback;
package/lib/esm/index.js CHANGED
@@ -68,6 +68,6 @@ export { default as BaseProvider } from './BaseProvider';
68
68
  export { useHydrationContext } from './BaseProvider/HydrationContext';
69
69
  export { default as Validator } from './Validator';
70
70
  export { default as ViewportProvider, useViewport, ViewportContext } from './ViewportProvider';
71
- export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens } from './ThemeProvider';
71
+ export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens, useResponsiveThemeTokensCallback } from './ThemeProvider';
72
72
  export * from './utils';
73
73
  export { default as Portal } from './Portal';
@@ -1,6 +1,7 @@
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
  const StyleSheet = {
4
5
  create: createStyleSheet
5
6
  };
6
- export { StyleSheet, createMediaQueryStyles };
7
+ export { StyleSheet, createMediaQueryStyles, useAllViewportTokens };
@@ -0,0 +1,48 @@
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
+ const xs = useThemeTokens(componentName, tokens, variant, {
22
+ viewport: 'xs'
23
+ });
24
+ const sm = useThemeTokens(componentName, tokens, variant, {
25
+ viewport: 'sm'
26
+ });
27
+ const md = useThemeTokens(componentName, tokens, variant, {
28
+ viewport: 'md'
29
+ });
30
+ const lg = useThemeTokens(componentName, tokens, variant, {
31
+ viewport: 'lg'
32
+ });
33
+ const xl = useThemeTokens(componentName, tokens, variant, {
34
+ viewport: 'xl'
35
+ });
36
+ const current = useThemeTokens(componentName, tokens, variant, {
37
+ viewport
38
+ });
39
+ return {
40
+ xs,
41
+ sm,
42
+ md,
43
+ lg,
44
+ xl,
45
+ current
46
+ };
47
+ };
48
+ export default useAllViewportTokens;
package/lib/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "@testing-library/react": "^13.3.0",
35
35
  "@testing-library/react-hooks": "~7.0.1",
36
36
  "@testing-library/react-native": "11.0.0",
37
- "react-test-renderer": "~18.0.0",
37
+ "react-test-renderer": "^18.0.0",
38
38
  "webpack": "5.x"
39
39
  },
40
40
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "react": ">=18.2.0 <19.0.0",
60
60
  "react-dom": ">=18.2.0 <19.0.0",
61
61
  "react-native": "^0.74.5",
62
- "react-native-web": "^0.19.10",
63
- "react-native-svg": "15.7.1"
62
+ "react-native-svg": "15.7.1",
63
+ "react-native-web": "^0.19.10"
64
64
  },
65
65
  "react-native": "src/index.js",
66
66
  "repository": {
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.20.0",
87
+ "version": "3.22.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "@testing-library/react": "^13.3.0",
35
35
  "@testing-library/react-hooks": "~7.0.1",
36
36
  "@testing-library/react-native": "11.0.0",
37
- "react-test-renderer": "~18.0.0",
37
+ "react-test-renderer": "^18.0.0",
38
38
  "webpack": "5.x"
39
39
  },
40
40
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "react": ">=18.2.0 <19.0.0",
60
60
  "react-dom": ">=18.2.0 <19.0.0",
61
61
  "react-native": "^0.74.5",
62
- "react-native-web": "^0.19.10",
63
- "react-native-svg": "15.7.1"
62
+ "react-native-svg": "15.7.1",
63
+ "react-native-web": "^0.19.10"
64
64
  },
65
65
  "react-native": "src/index.js",
66
66
  "repository": {
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.20.0",
87
+ "version": "3.22.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -2,17 +2,37 @@ import React from 'react'
2
2
 
3
3
  import ButtonBase from './ButtonBase'
4
4
  import buttonPropTypes, { textAndA11yText } from './propTypes'
5
- import { useThemeTokensCallback } from '../ThemeProvider'
5
+ import {
6
+ useThemeTokensCallback,
7
+ useResponsiveThemeTokensCallback,
8
+ useTheme
9
+ } from '../ThemeProvider'
6
10
  import { a11yProps } from '../utils/props'
7
11
  import { useViewport } from '../ViewportProvider'
8
12
 
9
13
  const Button = React.forwardRef(
10
14
  ({ accessibilityRole = 'button', tokens, variant, ...props }, ref) => {
11
15
  const viewport = useViewport()
12
- const buttonVariant = { viewport, ...variant }
13
- const getTokens = useThemeTokensCallback('Button', tokens, buttonVariant)
16
+ const {
17
+ themeOptions: { enableMediaQueryStyleSheet }
18
+ } = useTheme()
19
+
20
+ const buttonVariant = enableMediaQueryStyleSheet ? variant : { viewport, ...variant }
21
+
22
+ const useTokens = enableMediaQueryStyleSheet
23
+ ? useResponsiveThemeTokensCallback
24
+ : useThemeTokensCallback
25
+
26
+ const getTokens = useTokens('Button', tokens, buttonVariant)
27
+
14
28
  return (
15
- <ButtonBase {...props} tokens={getTokens} accessibilityRole={accessibilityRole} ref={ref} />
29
+ <ButtonBase
30
+ {...props}
31
+ tokens={getTokens}
32
+ accessibilityRole={accessibilityRole}
33
+ ref={ref}
34
+ viewport={viewport}
35
+ />
16
36
  )
17
37
  }
18
38
  )