@telus-uds/components-base 3.21.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 CHANGED
@@ -1,9 +1,22 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Tue, 21 Oct 2025 14:46:26 GMT and should not be manually modified.
3
+ This log was last generated on Wed, 29 Oct 2025 07:40:46 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 3.22.0
8
+
9
+ Wed, 29 Oct 2025 07:40:46 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Carousel`: Add `loopDuration` prop (oscar.palencia@telus.com)
14
+ - `Button`: add RNMQ support (guillermo.peitzner@telus.com)
15
+
16
+ ### Patches
17
+
18
+ - `Interactive Card`: extra spacing fixed in interactive Cards when padding tokens are set to none (35577399+JoshHC@users.noreply.github.com)
19
+
7
20
  ## 3.21.0
8
21
 
9
22
  Tue, 21 Oct 2025 14:46:26 GMT
@@ -22,16 +22,23 @@ const Button = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
22
22
  ...props
23
23
  } = _ref;
24
24
  const viewport = (0, _ViewportProvider.useViewport)();
25
- const buttonVariant = {
25
+ const {
26
+ themeOptions: {
27
+ enableMediaQueryStyleSheet
28
+ }
29
+ } = (0, _ThemeProvider.useTheme)();
30
+ const buttonVariant = enableMediaQueryStyleSheet ? variant : {
26
31
  viewport,
27
32
  ...variant
28
33
  };
29
- const getTokens = (0, _ThemeProvider.useThemeTokensCallback)('Button', tokens, buttonVariant);
34
+ const useTokens = enableMediaQueryStyleSheet ? _ThemeProvider.useResponsiveThemeTokensCallback : _ThemeProvider.useThemeTokensCallback;
35
+ const getTokens = useTokens('Button', tokens, buttonVariant);
30
36
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ButtonBase.default, {
31
37
  ...props,
32
38
  tokens: getTokens,
33
39
  accessibilityRole: accessibilityRole,
34
- ref: ref
40
+ ref: ref,
41
+ viewport: viewport
35
42
  });
36
43
  });
37
44
  Button.displayName = 'Button';
@@ -257,6 +257,13 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
257
257
  iconProps,
258
258
  ...rawRest
259
259
  } = _ref12;
260
+ const {
261
+ themeOptions
262
+ } = (0, _ThemeProvider.useTheme)();
263
+ const {
264
+ viewport
265
+ } = rawRest;
266
+ const enableMediaQueryStyleSheet = themeOptions.enableMediaQueryStyleSheet && viewport;
260
267
  const {
261
268
  onPress,
262
269
  ...rest
@@ -268,15 +275,55 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
268
275
  };
269
276
  const resolveButtonTokens = pressableState => (0, _utils.resolvePressableTokens)(tokens, pressableState, extraButtonState);
270
277
  const systemProps = selectProps(rest);
278
+ let layoutMediaQueryStyles;
279
+ let flexAndWidthStylesIds;
280
+ if (enableMediaQueryStyleSheet) {
281
+ const defaultPressableState = {
282
+ pressed: false,
283
+ hovered: false,
284
+ focused: false
285
+ };
286
+ const defaultTokensByViewport = resolveButtonTokens(defaultPressableState);
287
+ const layoutTokensByViewport = Object.entries(defaultTokensByViewport).reduce((acc, _ref13) => {
288
+ let [vp, viewportTokens] = _ref13;
289
+ const flexAndWidthStyles = viewportTokens.width === '100%' && viewportTokens.flex === 1 ? selectFlexAndWidthStyles(viewportTokens) : {};
290
+ acc[vp] = {
291
+ ...staticStyles.row,
292
+ ...selectWebOnlyStyles(inactive, viewportTokens, systemProps),
293
+ ...(Object.keys(flexAndWidthStyles).length > 0 ? flexAndWidthStyles : {}),
294
+ ...selectOuterSizeStyles(viewportTokens)
295
+ };
296
+ return acc;
297
+ }, {});
298
+ const mediaQueryStyles = (0, _utils.createMediaQueryStyles)(layoutTokensByViewport);
299
+ const {
300
+ ids,
301
+ styles
302
+ } = _utils.StyleSheet.create({
303
+ layout: {
304
+ ...mediaQueryStyles
305
+ }
306
+ });
307
+ layoutMediaQueryStyles = styles.layout;
308
+ flexAndWidthStylesIds = ids.layout;
309
+ }
271
310
  const getPressableStyle = pressableState => {
311
+ if (enableMediaQueryStyleSheet) {
312
+ const themeTokens = resolveButtonTokens(pressableState)[viewport];
313
+ return [layoutMediaQueryStyles, selectOuterContainerStyles(themeTokens)];
314
+ }
272
315
  const themeTokens = resolveButtonTokens(pressableState);
273
- // 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
274
316
  const flexAndWidthStyles = themeTokens.width === '100%' && themeTokens.flex === 1 ? selectFlexAndWidthStyles(themeTokens) : {};
275
317
  return [staticStyles.row, selectWebOnlyStyles(inactive, themeTokens, systemProps), selectOuterContainerStyles(themeTokens), ...(Object.keys(flexAndWidthStyles).length > 0 ? [flexAndWidthStyles] : []), selectOuterSizeStyles(themeTokens)];
276
318
  };
277
- const {
278
- themeOptions
279
- } = (0, _ThemeProvider.useTheme)();
319
+ const dataSetProp = flexAndWidthStylesIds || rawRest.dataSet ? {
320
+ dataSet: {
321
+ ...(flexAndWidthStylesIds ? {
322
+ media: flexAndWidthStylesIds
323
+ } : {}),
324
+ ...rawRest.dataSet
325
+ }
326
+ } : {};
280
327
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Pressable.default, {
281
328
  ref: ref,
282
329
  href: href,
@@ -288,8 +335,9 @@ const ButtonBase = /*#__PURE__*/_react.default.forwardRef((_ref12, ref) => {
288
335
  disabled: inactive,
289
336
  hrefAttrs: hrefAttrs,
290
337
  ...systemProps,
338
+ ...dataSetProp,
291
339
  children: pressableState => {
292
- const themeTokens = resolveButtonTokens(pressableState);
340
+ const themeTokens = enableMediaQueryStyleSheet ? resolveButtonTokens(pressableState)[viewport] : resolveButtonTokens(pressableState);
293
341
  const containerStyles = selectInnerContainerStyles(themeTokens);
294
342
  const borderStyles = selectBorderStyles(themeTokens);
295
343
  const textStyles = [selectTextStyles(themeTokens, themeOptions), staticStyles.text, _Platform.default.select({
@@ -181,7 +181,9 @@ const staticStyles = _StyleSheet.default.create({
181
181
  },
182
182
  linkContainer: {
183
183
  flex: 1,
184
- display: 'flex'
184
+ display: 'flex',
185
+ alignItems: 'stretch',
186
+ justifyContent: 'flex-start'
185
187
  }
186
188
  });
187
189
  PressableCardBase.displayName = 'PressableCardBase';
@@ -358,6 +358,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
358
358
  copy,
359
359
  slideDuration = 0,
360
360
  transitionDuration = 0,
361
+ loopDuration = transitionDuration,
361
362
  autoPlay = false,
362
363
  enablePeeking = false,
363
364
  ...rest
@@ -477,6 +478,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
477
478
  }
478
479
  }, [pan, animatedX, heroPan, heroAnimatedX, enableHero, viewport, enablePeeking]);
479
480
  const animate = _react.default.useCallback((panToAnimate, toValue, toIndex) => {
481
+ const applicableTransitionDuration = isLastSlide && toIndex === 0 ? loopDuration : transitionDuration;
480
482
  const handleAnimationEndToIndex = function () {
481
483
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
482
484
  args[_key] = arguments[_key];
@@ -494,14 +496,14 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
494
496
  ...springConfig,
495
497
  toValue,
496
498
  useNativeDriver: false,
497
- duration: transitionDuration * 1000
499
+ duration: applicableTransitionDuration * 1000
498
500
  }).start(handleAnimationEndToIndex);
499
501
  } else if (enablePeeking || enableDisplayMultipleItemsPerSlide) {
500
502
  _Animated.default.timing(panToAnimate, {
501
503
  ...springConfig,
502
504
  toValue,
503
505
  useNativeDriver: false,
504
- duration: transitionDuration ? transitionDuration * 1000 : 1000
506
+ duration: applicableTransitionDuration ? applicableTransitionDuration * 1000 : 1000
505
507
  }).start(handleAnimationEndToIndex);
506
508
  } else {
507
509
  _Animated.default.spring(panToAnimate, {
@@ -510,7 +512,7 @@ const Carousel = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
510
512
  useNativeDriver: false
511
513
  }).start(handleAnimationEndToIndex);
512
514
  }
513
- }, [springConfig, handleAnimationEnd, transitionDuration, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
515
+ }, [springConfig, handleAnimationEnd, transitionDuration, loopDuration, isLastSlide, isAutoPlayEnabled, enablePeeking, enableDisplayMultipleItemsPerSlide]);
514
516
  const stopAutoplay = _react.default.useCallback(() => {
515
517
  if (autoPlayRef?.current) {
516
518
  clearTimeout(autoPlayRef?.current);
@@ -1204,6 +1206,12 @@ Carousel.propTypes = {
1204
1206
  * - `autoPlay` and `slideDuration` are required to be set for this to work
1205
1207
  */
1206
1208
  transitionDuration: _propTypes.default.number,
1209
+ /**
1210
+ * Time it takes in seconds to transition from last slide to first slide
1211
+ * - Default value equals `transitionDuration`'s value
1212
+ * - `autoPlay` and `transitionDuration` are required to be set for this to work
1213
+ */
1214
+ loopDuration: _propTypes.default.number,
1207
1215
  /**
1208
1216
  * If set to `true`, the Carousel will show the previous and next slides
1209
1217
  * - Default value is `false`
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  var _exportNames = {
7
7
  useTheme: true,
8
8
  useSetTheme: true,
9
- useResponsiveThemeTokens: true
9
+ useResponsiveThemeTokens: true,
10
+ useResponsiveThemeTokensCallback: true
10
11
  };
11
12
  exports.default = void 0;
12
13
  Object.defineProperty(exports, "useResponsiveThemeTokens", {
@@ -15,6 +16,12 @@ Object.defineProperty(exports, "useResponsiveThemeTokens", {
15
16
  return _useResponsiveThemeTokens.default;
16
17
  }
17
18
  });
19
+ Object.defineProperty(exports, "useResponsiveThemeTokensCallback", {
20
+ enumerable: true,
21
+ get: function () {
22
+ return _useResponsiveThemeTokensCallback.default;
23
+ }
24
+ });
18
25
  Object.defineProperty(exports, "useSetTheme", {
19
26
  enumerable: true,
20
27
  get: function () {
@@ -31,6 +38,7 @@ var _ThemeProvider = _interopRequireDefault(require("./ThemeProvider"));
31
38
  var _useTheme = _interopRequireDefault(require("./useTheme"));
32
39
  var _useSetTheme = _interopRequireDefault(require("./useSetTheme"));
33
40
  var _useResponsiveThemeTokens = _interopRequireDefault(require("./useResponsiveThemeTokens"));
41
+ var _useResponsiveThemeTokensCallback = _interopRequireDefault(require("./useResponsiveThemeTokensCallback"));
34
42
  var _useThemeTokens = require("./useThemeTokens");
35
43
  Object.keys(_useThemeTokens).forEach(function (key) {
36
44
  if (key === "default" || key === "__esModule") return;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _systemConstants = require("@telus-uds/system-constants");
9
+ var _useTheme = _interopRequireDefault(require("./useTheme"));
10
+ var _utils = require("./utils");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ const getResponsiveThemeTokens = function (_ref, tokensProp) {
13
+ let {
14
+ rules = [],
15
+ tokens: defaultThemeTokens = {}
16
+ } = _ref;
17
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
18
+ let states = arguments.length > 3 ? arguments[3] : undefined;
19
+ const appearances = (0, _utils.mergeAppearances)(variants, states);
20
+ const tokensByViewport = Object.fromEntries(_systemConstants.viewports.keys.map(viewport => [viewport, {
21
+ ...defaultThemeTokens
22
+ }]));
23
+
24
+ // Go through each rule and collect them for the corresponding viewport if they apply
25
+ rules.forEach(rule => {
26
+ if (doesRuleApply(rule, appearances)) {
27
+ // If the rule does not have a viewport specified, we collect it in all viewports
28
+ let targetViewports = rule.if.viewport || _systemConstants.viewports.keys;
29
+ if (!Array.isArray(targetViewports)) {
30
+ targetViewports = [targetViewports];
31
+ }
32
+ targetViewports.forEach(viewport => {
33
+ tokensByViewport[viewport] = {
34
+ ...tokensByViewport[viewport],
35
+ ...rule.tokens
36
+ };
37
+ });
38
+ }
39
+ });
40
+ Object.keys(tokensByViewport).forEach(viewport => {
41
+ tokensByViewport[viewport] = (0, _utils.resolveThemeTokens)(tokensByViewport[viewport], appearances, tokensProp);
42
+ });
43
+ return tokensByViewport;
44
+ };
45
+ const doesRuleApply = (rule, appearances) => Object.entries(rule.if).every(condition => doesConditionApply(condition, appearances));
46
+ const doesConditionApply = (_ref2, appearances) => {
47
+ let [key, value] = _ref2;
48
+ if (key === 'viewport') {
49
+ return true;
50
+ }
51
+ // use null rather than undefined so we can serialise the value in themes
52
+ const appearanceValue = appearances[key] ?? null;
53
+ return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue;
54
+ };
55
+
56
+ /**
57
+ * @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
58
+ * @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
59
+ * @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
60
+ */
61
+
62
+ /**
63
+ * Returns a memoised tokens getter function that gets responsive tokens for all viewports,
64
+ * similar to calling useResponsiveThemeTokens but with the callback pattern of useThemeTokensCallback.
65
+ *
66
+ * Scenarios where `useResponsiveThemeTokensCallback` should be used:
67
+ *
68
+ * - Where responsive tokens are to be obtained from state that is accessible only in scopes like callbacks
69
+ * and render functions, where calling useResponsiveThemeTokens directly would be disallowed by React's hook rules.
70
+ * - When using media query stylesheets and need to resolve tokens based on dynamic state (e.g., pressed, hovered)
71
+ * that changes at runtime.
72
+ * - Passing a responsive tokens getter down via a child component's `tokens` prop, applying rules using the
73
+ * child component's current state.
74
+ *
75
+ * The function returned may be called with an object of state appearances to get an object
76
+ * of tokens for each viewport, which can then be passed to createMediaQueryStyles.
77
+ *
78
+ * @example
79
+ * // Resolving responsive tokens inside Pressable's style function, based on Pressable state
80
+ * const PressMe = ({ tokens, variant, children }) => {
81
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('PressMe', tokens, variant)
82
+ * const getPressableStyle = ({ pressed }) => {
83
+ * const responsiveTokens = getResponsiveTokens({ pressed })
84
+ * const mediaQueryStyles = createMediaQueryStyles(responsiveTokens)
85
+ * return mediaQueryStyles
86
+ * }
87
+ * return <Pressable style={getPressableStyle}>{children}</Pressable>
88
+ * }
89
+ *
90
+ * @example
91
+ * // Setting the theme in a parent and resolving it in a child based on child's state
92
+ * const MenuButton = ({ tokens, variant, ...buttonProps }) => {
93
+ * // Define what theme, variant etc we want in this component...
94
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('Button', tokens, variant)
95
+ * // ...resolve them in another component based on its state (e.g. press, hover...)
96
+ * return <ButtonBase tokens={getResponsiveTokens} accessibilityRole="menuitem" {...buttonProps} />
97
+ * }
98
+ *
99
+ * @typedef {Object} ResponsiveObject
100
+ * @property {TokensSet} xs
101
+ * @property {TokensSet} sm
102
+ * @property {TokensSet} md
103
+ * @property {TokensSet} lg
104
+ * @property {TokensSet} xl
105
+ *
106
+ * @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
107
+ * @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
108
+ * @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
109
+ * @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => ResponsiveObject}
110
+ * - callback function that returns an overridable responsive tokens object for current state. Only pass
111
+ * tokenOverrides in rare cases where tokens overrides are also generated outside hook scope.
112
+ */
113
+ const useResponsiveThemeTokensCallback = function (componentName) {
114
+ let tokens = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
115
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
116
+ const theme = (0, _useTheme.default)();
117
+ const componentTheme = (0, _utils.getComponentTheme)(theme, componentName);
118
+ const getResponsiveThemeTokensCallback = (0, _react.useCallback)((states, tokenOverrides) => {
119
+ const resolvedTokens = (0, _utils.resolveThemeTokens)(tokens, (0, _utils.mergeAppearances)(variants, states), tokenOverrides);
120
+ return getResponsiveThemeTokens(componentTheme, resolvedTokens, variants, states);
121
+ }, [componentTheme, tokens, variants]);
122
+ return getResponsiveThemeTokensCallback;
123
+ };
124
+ var _default = exports.default = useResponsiveThemeTokensCallback;
package/lib/cjs/index.js CHANGED
@@ -93,6 +93,7 @@ var _exportNames = {
93
93
  applyTextStyles: true,
94
94
  applyShadowToken: true,
95
95
  useResponsiveThemeTokens: true,
96
+ useResponsiveThemeTokensCallback: true,
96
97
  Portal: true
97
98
  };
98
99
  Object.defineProperty(exports, "A11yInfoProvider", {
@@ -605,6 +606,12 @@ Object.defineProperty(exports, "useResponsiveThemeTokens", {
605
606
  return _ThemeProvider.useResponsiveThemeTokens;
606
607
  }
607
608
  });
609
+ Object.defineProperty(exports, "useResponsiveThemeTokensCallback", {
610
+ enumerable: true,
611
+ get: function () {
612
+ return _ThemeProvider.useResponsiveThemeTokensCallback;
613
+ }
614
+ });
608
615
  Object.defineProperty(exports, "useSetTheme", {
609
616
  enumerable: true,
610
617
  get: function () {
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import ButtonBase from './ButtonBase';
3
3
  import buttonPropTypes, { textAndA11yText } from './propTypes';
4
- import { useThemeTokensCallback } from '../ThemeProvider';
4
+ import { useThemeTokensCallback, useResponsiveThemeTokensCallback, useTheme } from '../ThemeProvider';
5
5
  import { a11yProps } from '../utils/props';
6
6
  import { useViewport } from '../ViewportProvider';
7
7
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -13,16 +13,23 @@ const Button = /*#__PURE__*/React.forwardRef((_ref, ref) => {
13
13
  ...props
14
14
  } = _ref;
15
15
  const viewport = useViewport();
16
- const buttonVariant = {
16
+ const {
17
+ themeOptions: {
18
+ enableMediaQueryStyleSheet
19
+ }
20
+ } = useTheme();
21
+ const buttonVariant = enableMediaQueryStyleSheet ? variant : {
17
22
  viewport,
18
23
  ...variant
19
24
  };
20
- const getTokens = useThemeTokensCallback('Button', tokens, buttonVariant);
25
+ const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokensCallback : useThemeTokensCallback;
26
+ const getTokens = useTokens('Button', tokens, buttonVariant);
21
27
  return /*#__PURE__*/_jsx(ButtonBase, {
22
28
  ...props,
23
29
  tokens: getTokens,
24
30
  accessibilityRole: accessibilityRole,
25
- ref: ref
31
+ ref: ref,
32
+ viewport: viewport
26
33
  });
27
34
  });
28
35
  Button.displayName = 'Button';
@@ -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`
@@ -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';
package/lib/package.json CHANGED
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.21.0",
87
+ "version": "3.22.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.21.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
  )
@@ -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
- const { themeOptions } = useTheme()
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 = resolveButtonTokens(pressableState)
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 = [
@@ -204,7 +204,9 @@ const staticStyles = StyleSheet.create({
204
204
  },
205
205
  linkContainer: {
206
206
  flex: 1,
207
- display: 'flex'
207
+ display: 'flex',
208
+ alignItems: 'stretch',
209
+ justifyContent: 'flex-start'
208
210
  }
209
211
  })
210
212
 
@@ -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: transitionDuration * 1000
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: transitionDuration ? transitionDuration * 1000 : 1000
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
- <Text>
107
- {renderAmount(
108
- amount,
109
- amountTypographyTokens,
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
- {isPriceBaseline &&
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}
@@ -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
@@ -83,7 +83,8 @@ export {
83
83
  applyOuterBorder,
84
84
  applyTextStyles,
85
85
  applyShadowToken,
86
- useResponsiveThemeTokens
86
+ useResponsiveThemeTokens,
87
+ useResponsiveThemeTokensCallback
87
88
  } from './ThemeProvider'
88
89
 
89
90
  export * from './utils'