@telus-uds/components-base 3.19.0 → 3.21.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 (39) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/lib/cjs/Button/ButtonDropdown.js +1 -0
  3. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +8 -21
  4. package/lib/cjs/Link/LinkBase.js +8 -9
  5. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +8 -8
  6. package/lib/cjs/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
  7. package/lib/cjs/Spacer/Spacer.js +65 -5
  8. package/lib/cjs/StackView/StackView.js +62 -12
  9. package/lib/cjs/Tabs/TabsDropdown.js +4 -5
  10. package/lib/cjs/utils/index.js +8 -0
  11. package/lib/cjs/utils/ssr-media-query/index.js +7 -0
  12. package/lib/cjs/utils/ssr-media-query/utils/use-all-viewport-tokens.js +53 -0
  13. package/lib/cjs/utils/useMediaQuerySpacing.js +121 -0
  14. package/lib/esm/Button/ButtonDropdown.js +1 -0
  15. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +8 -21
  16. package/lib/esm/Link/LinkBase.js +8 -9
  17. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +8 -8
  18. package/lib/esm/Responsive/ResponsiveWithMediaQueryStyleSheet.js +1 -1
  19. package/lib/esm/Spacer/Spacer.js +66 -6
  20. package/lib/esm/StackView/StackView.js +63 -13
  21. package/lib/esm/Tabs/TabsDropdown.js +4 -5
  22. package/lib/esm/utils/index.js +1 -0
  23. package/lib/esm/utils/ssr-media-query/index.js +2 -1
  24. package/lib/esm/utils/ssr-media-query/utils/use-all-viewport-tokens.js +48 -0
  25. package/lib/esm/utils/useMediaQuerySpacing.js +116 -0
  26. package/lib/package.json +5 -5
  27. package/package.json +5 -5
  28. package/src/Button/ButtonDropdown.jsx +1 -0
  29. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +9 -16
  30. package/src/Link/LinkBase.jsx +11 -9
  31. package/src/MultiSelectFilter/MultiSelectFilter.jsx +9 -8
  32. package/src/Responsive/ResponsiveWithMediaQueryStyleSheet.jsx +1 -1
  33. package/src/Spacer/Spacer.jsx +54 -7
  34. package/src/StackView/StackView.jsx +62 -9
  35. package/src/Tabs/TabsDropdown.jsx +10 -9
  36. package/src/utils/index.js +1 -0
  37. package/src/utils/ssr-media-query/index.js +2 -1
  38. package/src/utils/ssr-media-query/utils/use-all-viewport-tokens.js +32 -0
  39. package/src/utils/useMediaQuerySpacing.js +124 -0
@@ -66,35 +66,21 @@ const ExpandCollapseMiniControl = /*#__PURE__*/React.forwardRef((_ref, ref) => {
66
66
  const isHovered = hover || linkHover;
67
67
  const iconBaselineOffset = 0;
68
68
  const hoverTranslateY = 4;
69
-
70
- // Calculate baseline alignment to vertically center icon with text
71
- // This combines font and icon metrics with adjustments for visual balance
72
- const fontBaseline = fontSize / hoverTranslateY; // Quarter of font size - adjusts for text's visual center point
73
- const iconBaseline = iconSize / hoverTranslateY; // Quarter of icon size - adjusts for icon's visual center point
74
- const staticOffset = hoverTranslateY; // Fixed downward adjustment to fine-tune vertical alignment
75
- const sizeCompensation = -Math.abs(iconSize - fontSize); // Compensates when icon and text sizes differ significantly
76
-
69
+ const fontBaseline = fontSize / hoverTranslateY;
70
+ const iconBaseline = iconSize / hoverTranslateY;
71
+ const staticOffset = hoverTranslateY;
72
+ const sizeCompensation = -Math.abs(iconSize - fontSize);
77
73
  const baselineAlignment = fontBaseline + iconBaseline - staticOffset + sizeCompensation;
78
- if (Platform.OS !== 'web') {
79
- // For native platforms, use baseline alignment with optional offset
80
- return {
81
- iconTranslateY: baselineAlignment + iconBaselineOffset
82
- };
83
- }
74
+ const mobileAdjustment = Platform.OS !== 'web' ? -2 : 0;
84
75
  if (isHovered) {
85
- // Apply animation offset to the baseline-aligned position
86
- // When expanded: move icon UP (1.3 the hover distance for clear movement)
87
- // When collapsed: move icon DOWN (single hover distance)
88
76
  const hoverMovementDistance = 1.3;
89
77
  const animationOffset = expanded ? -(hoverTranslateY * hoverMovementDistance) : hoverTranslateY;
90
78
  return {
91
- iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset
79
+ iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset + mobileAdjustment
92
80
  };
93
81
  }
94
-
95
- // Default state uses baseline alignment with optional offset
96
82
  return {
97
- iconTranslateY: baselineAlignment + iconBaselineOffset
83
+ iconTranslateY: baselineAlignment + iconBaselineOffset + mobileAdjustment
98
84
  };
99
85
  };
100
86
  return /*#__PURE__*/_jsx(Link, {
@@ -105,6 +91,7 @@ const ExpandCollapseMiniControl = /*#__PURE__*/React.forwardRef((_ref, ref) => {
105
91
  ...linkTokens,
106
92
  ...getTokens(linkState),
107
93
  iconSize,
94
+ blockFontSize: fontSize,
108
95
  blockLineHeight: lineHeight
109
96
  }),
110
97
  ref: ref,
@@ -180,7 +180,8 @@ const LinkBase = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
180
180
  const themeTokens = resolveLinkTokens(linkState);
181
181
  const outerBorderStyles = selectOuterBorderStyles(themeTokens);
182
182
  const decorationStyles = selectDecorationStyles(themeTokens);
183
- return [outerBorderStyles, staticStyles.outerBorderStyles, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
183
+ const mobileCompensation = null;
184
+ return [outerBorderStyles, mobileCompensation, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
184
185
  },
185
186
  children: linkState => {
186
187
  const themeTokens = resolveLinkTokens(linkState);
@@ -193,10 +194,12 @@ const LinkBase = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
193
194
  const {
194
195
  iconSpace
195
196
  } = themeTokens;
197
+ const isTextOnlyLink = !IconComponent && !icon && accessibilityRole === 'link';
198
+ const adjustedIconSpace = Platform.OS !== 'web' && isTextOnlyLink ? 0 : iconSpace;
196
199
  return /*#__PURE__*/_jsx(IconText, {
197
200
  icon: IconComponent,
198
201
  iconPosition: iconPosition,
199
- space: iconSpace,
202
+ space: adjustedIconSpace,
200
203
  iconProps: {
201
204
  ...iconProps,
202
205
  tokens: iconTokens,
@@ -263,15 +266,11 @@ const staticStyles = StyleSheet.create({
263
266
  }
264
267
  })
265
268
  },
266
- outerBorderStyles: {
269
+ outerBorderCompensation: {
267
270
  ...(Platform.OS !== 'web' && {
268
- margin: 0,
269
271
  marginHorizontal: 2,
270
- padding: 0
271
- }),
272
- ...(Platform.OS === 'android' && {
273
- paddingHorizontal: 2,
274
- paddingTop: 2
272
+ paddingHorizontal: Platform.OS === 'android' ? 2 : 0,
273
+ paddingTop: Platform.OS === 'android' ? 2 : 0
275
274
  })
276
275
  }
277
276
  });
@@ -64,7 +64,6 @@ const selectContainerStyle = (windowHeight, windowWidth) => ({
64
64
  width: windowWidth
65
65
  });
66
66
  const TOTAL_COLUMNS = 12;
67
- const MAX_ITEMS_THRESHOLD = 12;
68
67
  const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
69
68
  let {
70
69
  label,
@@ -171,12 +170,13 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
171
170
  });
172
171
  const colSizeNotMobile = items.length > rowLimit ? 2 : 1;
173
172
  const colSize = viewport !== 'xs' ? colSizeNotMobile : 1;
174
- const itemsLengthNotMobile = items.length > 24 ? items.length / 2 : rowLimit;
175
- const rowLength = viewport !== 'xs' ? itemsLengthNotMobile : items.length;
173
+ let rowLength = items.length;
174
+ if (viewport !== 'xs' && colSize === 2) {
175
+ rowLength = Math.ceil(items.length / 2);
176
+ }
176
177
  React.useEffect(() => {
177
- if (colSize === 1) return setMaxWidth(false);
178
- return colSize === 2 && setMaxWidth(true);
179
- }, [colSize]);
178
+ setMaxWidth(items.length >= rowLimit);
179
+ }, [items.length, rowLimit]);
180
180
  React.useEffect(() => setCheckedIds(currentValues ?? []), [currentValues]);
181
181
  const uniqueFields = ['id', 'label'];
182
182
  if (!containUniqueFields(items, uniqueFields)) {
@@ -423,14 +423,14 @@ const MultiSelectFilter = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
423
423
  dismissWhenPressedOutside: dismissWhenPressedOutside,
424
424
  onClose: onClose,
425
425
  overlaidPosition: overlaidPosition,
426
- maxHeight: items.length > MAX_ITEMS_THRESHOLD ? true : maxHeight,
426
+ maxHeight: items.length >= rowLimit ? true : maxHeight,
427
427
  maxHeightSize: maxHeightSize,
428
428
  maxWidthSize: maxWidthSize,
429
429
  minHeight: minHeight,
430
430
  minWidth: minWidth,
431
431
  tokens: {
432
432
  ...tokens,
433
- maxWidth: items.length > MAX_ITEMS_THRESHOLD ? true : maxWidth,
433
+ maxWidth: items.length >= rowLimit ? true : maxWidth,
434
434
  borderColor: containerBorderColor
435
435
  },
436
436
  copy: copy,
@@ -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
  };
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import StyleSheet from "react-native-web/dist/exports/StyleSheet";
4
3
  import View from "react-native-web/dist/exports/View";
5
- import { a11yProps, selectSystemProps, spacingProps, useSpacingScale, viewProps } from '../utils';
4
+ import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
+ import { a11yProps, selectSystemProps, spacingProps, useSpacingScale, viewProps, StyleSheet as StyleSheetUtils, createMediaQueryStyles } from '../utils';
6
+ import useMediaQuerySpacing from '../utils/useMediaQuerySpacing';
7
+ import useTheme from '../ThemeProvider/useTheme';
6
8
 
7
9
  /**
8
10
  * @typedef {import('../utils/props/spacingProps.js').SpacingValue} SpacingValue
@@ -60,13 +62,66 @@ const Spacer = /*#__PURE__*/React.forwardRef((_ref, ref) => {
60
62
  let {
61
63
  space = 1,
62
64
  direction = 'column',
65
+ dataSet,
63
66
  ...rest
64
67
  } = _ref;
65
- const size = useSpacingScale(space);
66
- const sizeStyle = selectSizeStyle(size, direction);
68
+ const {
69
+ themeOptions: {
70
+ enableMediaQueryStyleSheet
71
+ }
72
+ } = useTheme();
73
+ const {
74
+ sizeByViewport
75
+ } = useMediaQuerySpacing(space);
76
+ const fallbackSize = useSpacingScale(space);
77
+ const sizeStyle = selectSizeStyle(fallbackSize, direction);
78
+ let spacerStyles;
79
+ let dataSetValue = dataSet;
80
+ if (enableMediaQueryStyleSheet) {
81
+ const sizeKey = direction === 'row' ? 'width' : 'height';
82
+ const stylesByViewport = {
83
+ xs: {
84
+ [sizeKey]: sizeByViewport.xs,
85
+ ...staticStyles.stretch
86
+ },
87
+ sm: {
88
+ [sizeKey]: sizeByViewport.sm,
89
+ ...staticStyles.stretch
90
+ },
91
+ md: {
92
+ [sizeKey]: sizeByViewport.md,
93
+ ...staticStyles.stretch
94
+ },
95
+ lg: {
96
+ [sizeKey]: sizeByViewport.lg,
97
+ ...staticStyles.stretch
98
+ },
99
+ xl: {
100
+ [sizeKey]: sizeByViewport.xl,
101
+ ...staticStyles.stretch
102
+ }
103
+ };
104
+ const mediaQueryStyles = createMediaQueryStyles(stylesByViewport);
105
+ const {
106
+ ids,
107
+ styles
108
+ } = StyleSheetUtils.create({
109
+ spacer: {
110
+ ...mediaQueryStyles
111
+ }
112
+ });
113
+ spacerStyles = styles.spacer;
114
+ dataSetValue = {
115
+ media: ids.spacer,
116
+ ...dataSet
117
+ };
118
+ } else {
119
+ spacerStyles = [staticStyles.stretch, sizeStyle];
120
+ }
67
121
  return /*#__PURE__*/_jsx(View, {
68
122
  ref: ref,
69
- style: [staticStyles.stretch, sizeStyle],
123
+ style: spacerStyles,
124
+ dataSet: dataSetValue,
70
125
  ...selectProps(rest)
71
126
  });
72
127
  });
@@ -85,7 +140,12 @@ Spacer.propTypes = {
85
140
  * - `'column'` (default) applies space vertically; has a fixed height and not width.
86
141
  * - `'row'` applies space horizontally; has a fixed width and not height.
87
142
  */
88
- direction: PropTypes.oneOf(['column', 'row'])
143
+ direction: PropTypes.oneOf(['column', 'row']),
144
+ /**
145
+ * Data attributes to be applied to the element. When media query stylesheet is enabled,
146
+ * this will include media query IDs for responsive styling.
147
+ */
148
+ dataSet: PropTypes.object
89
149
  };
90
150
  const staticStyles = StyleSheet.create({
91
151
  stretch: {
@@ -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;
@@ -9,6 +9,7 @@ export { default as info } from './info';
9
9
  export { default as useCopy } from './useCopy';
10
10
  export { default as useHash } from './useHash';
11
11
  export { default as useSpacingScale } from './useSpacingScale';
12
+ export { default as useMediaQuerySpacing } from './useMediaQuerySpacing';
12
13
  export { default as useResponsiveProp } from './useResponsiveProp';
13
14
  export { default as useOverlaidPosition } from './useOverlaidPosition';
14
15
  export { default as useSafeLayoutEffect } from './useSafeLayoutEffect';
@@ -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;
@@ -0,0 +1,116 @@
1
+ import { useThemeTokens } from '../ThemeProvider';
2
+ import { resolveResponsiveProp } from './useResponsiveProp';
3
+
4
+ /**
5
+ * @typedef {import('@telus-uds/system-constants/viewports').Viewport} Viewport
6
+ * @typedef {import('./props/spacingProps.js').SpacingValue} SpacingValue
7
+ * @typedef {import('./props/spacingProps.js').SpacingIndex} SpacingIndex
8
+ * @typedef {import('./props/spacingProps.js').SpacingObject} SpacingObject
9
+ */
10
+
11
+ /**
12
+ * A utility hook that simplifies implementing media query-based responsive spacing.
13
+ *
14
+ * This hook handles the complexity of:
15
+ * - Detecting if a space value is responsive (has viewport keys)
16
+ * - Fetching theme tokens for each viewport
17
+ * - Resolving the correct space index for each viewport
18
+ * - Extracting actual pixel values from theme tokens
19
+ *
20
+ * ## Usage
21
+ *
22
+ * ```jsx
23
+ * const { sizeByViewport } = useMediaQuerySpacing(space, 'spacingScale')
24
+ *
25
+ * // Use sizeByViewport to create media query styles
26
+ * const stylesByViewport = {
27
+ * xs: { padding: sizeByViewport.xs },
28
+ * sm: { padding: sizeByViewport.sm },
29
+ * md: { padding: sizeByViewport.md },
30
+ * lg: { padding: sizeByViewport.lg },
31
+ * xl: { padding: sizeByViewport.xl }
32
+ * }
33
+ * const mediaQueryStyles = createMediaQueryStyles(stylesByViewport)
34
+ * ```
35
+ *
36
+ * ## Parameters
37
+ *
38
+ * @param {SpacingValue} spaceValue - A spacing value (number or responsive object with viewport keys)
39
+ * @param {string} tokenKey - The theme token key to use (e.g., 'spacingScale', 'Typography')
40
+ * @param {object} [tokens={}] - Additional tokens to pass to useThemeTokens
41
+ * @param {object} [variant={}] - Variant to pass to useThemeTokens
42
+ *
43
+ * ## Returns
44
+ *
45
+ * @returns {{
46
+ * spaceIndexByViewport: { xs: number, sm: number, md: number, lg: number, xl: number },
47
+ * sizeByViewport: { xs: number, sm: number, md: number, lg: number, xl: number },
48
+ * tokensByViewport: { xs: object, sm: object, md: object, lg: object, xl: object }
49
+ * }}
50
+ *
51
+ * - `spaceIndexByViewport`: The resolved space index for each viewport
52
+ * - `sizeByViewport`: The actual pixel/number values for each viewport
53
+ * - `tokensByViewport`: The full theme tokens for each viewport (for advanced use cases)
54
+ */
55
+ const useMediaQuerySpacing = function (spaceValue) {
56
+ let tokenKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'spacingScale';
57
+ let tokens = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
58
+ let variant = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
59
+ const isResponsive = typeof spaceValue === 'object' && spaceValue !== null && !spaceValue.space && !spaceValue.options;
60
+ const getSpaceIndex = viewport => {
61
+ if (isResponsive) {
62
+ return resolveResponsiveProp(spaceValue, viewport);
63
+ }
64
+ if (typeof spaceValue === 'number') {
65
+ return spaceValue;
66
+ }
67
+ return spaceValue?.space ?? 1;
68
+ };
69
+ const spaceIndexByViewport = {
70
+ xs: getSpaceIndex('xs'),
71
+ sm: getSpaceIndex('sm'),
72
+ md: getSpaceIndex('md'),
73
+ lg: getSpaceIndex('lg'),
74
+ xl: getSpaceIndex('xl')
75
+ };
76
+ const tokensXs = useThemeTokens(tokenKey, tokens, variant, {
77
+ space: spaceIndexByViewport.xs,
78
+ viewport: 'xs'
79
+ });
80
+ const tokensSm = useThemeTokens(tokenKey, tokens, variant, {
81
+ space: spaceIndexByViewport.sm,
82
+ viewport: 'sm'
83
+ });
84
+ const tokensMd = useThemeTokens(tokenKey, tokens, variant, {
85
+ space: spaceIndexByViewport.md,
86
+ viewport: 'md'
87
+ });
88
+ const tokensLg = useThemeTokens(tokenKey, tokens, variant, {
89
+ space: spaceIndexByViewport.lg,
90
+ viewport: 'lg'
91
+ });
92
+ const tokensXl = useThemeTokens(tokenKey, tokens, variant, {
93
+ space: spaceIndexByViewport.xl,
94
+ viewport: 'xl'
95
+ });
96
+ const sizeByViewport = {
97
+ xs: tokensXs.size ?? 0,
98
+ sm: tokensSm.size ?? 0,
99
+ md: tokensMd.size ?? 0,
100
+ lg: tokensLg.size ?? 0,
101
+ xl: tokensXl.size ?? 0
102
+ };
103
+ const tokensByViewport = {
104
+ xs: tokensXs,
105
+ sm: tokensSm,
106
+ md: tokensMd,
107
+ lg: tokensLg,
108
+ xl: tokensXl
109
+ };
110
+ return {
111
+ spaceIndexByViewport,
112
+ sizeByViewport,
113
+ tokensByViewport
114
+ };
115
+ };
116
+ export default useMediaQuerySpacing;
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.15.0",
15
+ "@telus-uds/system-theme-tokens": "^4.15.1",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -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.19.0",
87
+ "version": "3.21.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.15.0",
15
+ "@telus-uds/system-theme-tokens": "^4.15.1",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -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.19.0",
87
+ "version": "3.21.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -56,6 +56,7 @@ const selectDescriptionTextStyles = (tokens) => ({
56
56
  fontName: tokens?.descriptionFontName,
57
57
  fontSize: tokens?.descriptionFontSize,
58
58
  fontWeight: tokens?.descriptionFontWeight,
59
+ lineHeight: tokens?.descriptionLineHeight,
59
60
  fontColor: tokens?.color
60
61
  }),
61
62
  paddingBottom: tokens?.descriptionTextPaddingBottom