@telus-uds/components-base 3.19.0 → 3.20.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,24 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Fri, 03 Oct 2025 20:34:06 GMT and should not be manually modified.
3
+ This log was last generated on Fri, 10 Oct 2025 15:11:05 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 3.20.0
8
+
9
+ Fri, 10 Oct 2025 15:11:05 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - `Spacer`: add RNMQ support (guillermo.peitzner@telus.com)
14
+ - Bump @telus-uds/system-theme-tokens to v4.15.1
15
+
16
+ ### Patches
17
+
18
+ - `ButtonDropdown`: Small size variant updates. (oscar.palencia@telus.com)
19
+ - `MultiSelectfilter`: rowLimit prop issue causing display problems fixed (35577399+JoshHC@users.noreply.github.com)
20
+ - `ExpandCollapseMini`: extra padding removed (35577399+JoshHC@users.noreply.github.com)
21
+
7
22
  ## 3.19.0
8
23
 
9
24
  Fri, 03 Oct 2025 20:34:06 GMT
@@ -64,6 +64,7 @@ const selectDescriptionTextStyles = tokens => ({
64
64
  fontName: tokens?.descriptionFontName,
65
65
  fontSize: tokens?.descriptionFontSize,
66
66
  fontWeight: tokens?.descriptionFontWeight,
67
+ lineHeight: tokens?.descriptionLineHeight,
67
68
  fontColor: tokens?.color
68
69
  }),
69
70
  paddingBottom: tokens?.descriptionTextPaddingBottom
@@ -73,35 +73,21 @@ const ExpandCollapseMiniControl = /*#__PURE__*/_react.default.forwardRef((_ref,
73
73
  const isHovered = hover || linkHover;
74
74
  const iconBaselineOffset = 0;
75
75
  const hoverTranslateY = 4;
76
-
77
- // Calculate baseline alignment to vertically center icon with text
78
- // This combines font and icon metrics with adjustments for visual balance
79
- const fontBaseline = fontSize / hoverTranslateY; // Quarter of font size - adjusts for text's visual center point
80
- const iconBaseline = iconSize / hoverTranslateY; // Quarter of icon size - adjusts for icon's visual center point
81
- const staticOffset = hoverTranslateY; // Fixed downward adjustment to fine-tune vertical alignment
82
- const sizeCompensation = -Math.abs(iconSize - fontSize); // Compensates when icon and text sizes differ significantly
83
-
76
+ const fontBaseline = fontSize / hoverTranslateY;
77
+ const iconBaseline = iconSize / hoverTranslateY;
78
+ const staticOffset = hoverTranslateY;
79
+ const sizeCompensation = -Math.abs(iconSize - fontSize);
84
80
  const baselineAlignment = fontBaseline + iconBaseline - staticOffset + sizeCompensation;
85
- if (_Platform.default.OS !== 'web') {
86
- // For native platforms, use baseline alignment with optional offset
87
- return {
88
- iconTranslateY: baselineAlignment + iconBaselineOffset
89
- };
90
- }
81
+ const mobileAdjustment = _Platform.default.OS !== 'web' ? -2 : 0;
91
82
  if (isHovered) {
92
- // Apply animation offset to the baseline-aligned position
93
- // When expanded: move icon UP (1.3 the hover distance for clear movement)
94
- // When collapsed: move icon DOWN (single hover distance)
95
83
  const hoverMovementDistance = 1.3;
96
84
  const animationOffset = expanded ? -(hoverTranslateY * hoverMovementDistance) : hoverTranslateY;
97
85
  return {
98
- iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset
86
+ iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset + mobileAdjustment
99
87
  };
100
88
  }
101
-
102
- // Default state uses baseline alignment with optional offset
103
89
  return {
104
- iconTranslateY: baselineAlignment + iconBaselineOffset
90
+ iconTranslateY: baselineAlignment + iconBaselineOffset + mobileAdjustment
105
91
  };
106
92
  };
107
93
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Link.Link, {
@@ -112,6 +98,7 @@ const ExpandCollapseMiniControl = /*#__PURE__*/_react.default.forwardRef((_ref,
112
98
  ...linkTokens,
113
99
  ...getTokens(linkState),
114
100
  iconSize,
101
+ blockFontSize: fontSize,
115
102
  blockLineHeight: lineHeight
116
103
  }),
117
104
  ref: ref,
@@ -187,7 +187,8 @@ const LinkBase = /*#__PURE__*/_react.default.forwardRef((_ref6, ref) => {
187
187
  const themeTokens = resolveLinkTokens(linkState);
188
188
  const outerBorderStyles = selectOuterBorderStyles(themeTokens);
189
189
  const decorationStyles = selectDecorationStyles(themeTokens);
190
- return [outerBorderStyles, staticStyles.outerBorderStyles, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
190
+ const mobileCompensation = null;
191
+ return [outerBorderStyles, mobileCompensation, blockLeftStyle, decorationStyles, hasIcon && staticStyles.rowContainer];
191
192
  },
192
193
  children: linkState => {
193
194
  const themeTokens = resolveLinkTokens(linkState);
@@ -200,10 +201,12 @@ const LinkBase = /*#__PURE__*/_react.default.forwardRef((_ref6, ref) => {
200
201
  const {
201
202
  iconSpace
202
203
  } = themeTokens;
204
+ const isTextOnlyLink = !IconComponent && !icon && accessibilityRole === 'link';
205
+ const adjustedIconSpace = _Platform.default.OS !== 'web' && isTextOnlyLink ? 0 : iconSpace;
203
206
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.IconText, {
204
207
  icon: IconComponent,
205
208
  iconPosition: iconPosition,
206
- space: iconSpace,
209
+ space: adjustedIconSpace,
207
210
  iconProps: {
208
211
  ...iconProps,
209
212
  tokens: iconTokens,
@@ -270,15 +273,11 @@ const staticStyles = _StyleSheet.default.create({
270
273
  }
271
274
  })
272
275
  },
273
- outerBorderStyles: {
276
+ outerBorderCompensation: {
274
277
  ...(_Platform.default.OS !== 'web' && {
275
- margin: 0,
276
278
  marginHorizontal: 2,
277
- padding: 0
278
- }),
279
- ...(_Platform.default.OS === 'android' && {
280
- paddingHorizontal: 2,
281
- paddingTop: 2
279
+ paddingHorizontal: _Platform.default.OS === 'android' ? 2 : 0,
280
+ paddingTop: _Platform.default.OS === 'android' ? 2 : 0
282
281
  })
283
282
  }
284
283
  });
@@ -71,7 +71,6 @@ const selectContainerStyle = (windowHeight, windowWidth) => ({
71
71
  width: windowWidth
72
72
  });
73
73
  const TOTAL_COLUMNS = 12;
74
- const MAX_ITEMS_THRESHOLD = 12;
75
74
  const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) => {
76
75
  let {
77
76
  label,
@@ -178,12 +177,13 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
178
177
  });
179
178
  const colSizeNotMobile = items.length > rowLimit ? 2 : 1;
180
179
  const colSize = viewport !== 'xs' ? colSizeNotMobile : 1;
181
- const itemsLengthNotMobile = items.length > 24 ? items.length / 2 : rowLimit;
182
- const rowLength = viewport !== 'xs' ? itemsLengthNotMobile : items.length;
180
+ let rowLength = items.length;
181
+ if (viewport !== 'xs' && colSize === 2) {
182
+ rowLength = Math.ceil(items.length / 2);
183
+ }
183
184
  _react.default.useEffect(() => {
184
- if (colSize === 1) return setMaxWidth(false);
185
- return colSize === 2 && setMaxWidth(true);
186
- }, [colSize]);
185
+ setMaxWidth(items.length >= rowLimit);
186
+ }, [items.length, rowLimit]);
187
187
  _react.default.useEffect(() => setCheckedIds(currentValues ?? []), [currentValues]);
188
188
  const uniqueFields = ['id', 'label'];
189
189
  if (!(0, _utils.containUniqueFields)(items, uniqueFields)) {
@@ -430,14 +430,14 @@ const MultiSelectFilter = /*#__PURE__*/_react.default.forwardRef((_ref3, ref) =>
430
430
  dismissWhenPressedOutside: dismissWhenPressedOutside,
431
431
  onClose: onClose,
432
432
  overlaidPosition: overlaidPosition,
433
- maxHeight: items.length > MAX_ITEMS_THRESHOLD ? true : maxHeight,
433
+ maxHeight: items.length >= rowLimit ? true : maxHeight,
434
434
  maxHeightSize: maxHeightSize,
435
435
  maxWidthSize: maxWidthSize,
436
436
  minHeight: minHeight,
437
437
  minWidth: minWidth,
438
438
  tokens: {
439
439
  ...tokens,
440
- maxWidth: items.length > MAX_ITEMS_THRESHOLD ? true : maxWidth,
440
+ maxWidth: items.length >= rowLimit ? true : maxWidth,
441
441
  borderColor: containerBorderColor
442
442
  },
443
443
  copy: copy,
@@ -6,9 +6,11 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.default = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
- var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
10
9
  var _View = _interopRequireDefault(require("react-native-web/dist/cjs/exports/View"));
10
+ var _StyleSheet = _interopRequireDefault(require("react-native-web/dist/cjs/exports/StyleSheet"));
11
11
  var _utils = require("../utils");
12
+ var _useMediaQuerySpacing = _interopRequireDefault(require("../utils/useMediaQuerySpacing"));
13
+ var _useTheme = _interopRequireDefault(require("../ThemeProvider/useTheme"));
12
14
  var _jsxRuntime = require("react/jsx-runtime");
13
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
16
  /**
@@ -65,13 +67,66 @@ const Spacer = /*#__PURE__*/_react.default.forwardRef((_ref, ref) => {
65
67
  let {
66
68
  space = 1,
67
69
  direction = 'column',
70
+ dataSet,
68
71
  ...rest
69
72
  } = _ref;
70
- const size = (0, _utils.useSpacingScale)(space);
71
- const sizeStyle = selectSizeStyle(size, direction);
73
+ const {
74
+ themeOptions: {
75
+ enableMediaQueryStyleSheet
76
+ }
77
+ } = (0, _useTheme.default)();
78
+ const {
79
+ sizeByViewport
80
+ } = (0, _useMediaQuerySpacing.default)(space);
81
+ const fallbackSize = (0, _utils.useSpacingScale)(space);
82
+ const sizeStyle = selectSizeStyle(fallbackSize, direction);
83
+ let spacerStyles;
84
+ let dataSetValue = dataSet;
85
+ if (enableMediaQueryStyleSheet) {
86
+ const sizeKey = direction === 'row' ? 'width' : 'height';
87
+ const stylesByViewport = {
88
+ xs: {
89
+ [sizeKey]: sizeByViewport.xs,
90
+ ...staticStyles.stretch
91
+ },
92
+ sm: {
93
+ [sizeKey]: sizeByViewport.sm,
94
+ ...staticStyles.stretch
95
+ },
96
+ md: {
97
+ [sizeKey]: sizeByViewport.md,
98
+ ...staticStyles.stretch
99
+ },
100
+ lg: {
101
+ [sizeKey]: sizeByViewport.lg,
102
+ ...staticStyles.stretch
103
+ },
104
+ xl: {
105
+ [sizeKey]: sizeByViewport.xl,
106
+ ...staticStyles.stretch
107
+ }
108
+ };
109
+ const mediaQueryStyles = (0, _utils.createMediaQueryStyles)(stylesByViewport);
110
+ const {
111
+ ids,
112
+ styles
113
+ } = _utils.StyleSheet.create({
114
+ spacer: {
115
+ ...mediaQueryStyles
116
+ }
117
+ });
118
+ spacerStyles = styles.spacer;
119
+ dataSetValue = {
120
+ media: ids.spacer,
121
+ ...dataSet
122
+ };
123
+ } else {
124
+ spacerStyles = [staticStyles.stretch, sizeStyle];
125
+ }
72
126
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
73
127
  ref: ref,
74
- style: [staticStyles.stretch, sizeStyle],
128
+ style: spacerStyles,
129
+ dataSet: dataSetValue,
75
130
  ...selectProps(rest)
76
131
  });
77
132
  });
@@ -90,7 +145,12 @@ Spacer.propTypes = {
90
145
  * - `'column'` (default) applies space vertically; has a fixed height and not width.
91
146
  * - `'row'` applies space horizontally; has a fixed width and not height.
92
147
  */
93
- direction: _propTypes.default.oneOf(['column', 'row'])
148
+ direction: _propTypes.default.oneOf(['column', 'row']),
149
+ /**
150
+ * Data attributes to be applied to the element. When media query stylesheet is enabled,
151
+ * this will include media query IDs for responsive styling.
152
+ */
153
+ dataSet: _propTypes.default.object
94
154
  };
95
155
  const staticStyles = _StyleSheet.default.create({
96
156
  stretch: {
@@ -8,6 +8,7 @@ var _exportNames = {
8
8
  useCopy: true,
9
9
  useHash: true,
10
10
  useSpacingScale: true,
11
+ useMediaQuerySpacing: true,
11
12
  useResponsiveProp: true,
12
13
  useOverlaidPosition: true,
13
14
  useSafeLayoutEffect: true,
@@ -90,6 +91,12 @@ Object.defineProperty(exports, "useHash", {
90
91
  return _useHash.default;
91
92
  }
92
93
  });
94
+ Object.defineProperty(exports, "useMediaQuerySpacing", {
95
+ enumerable: true,
96
+ get: function () {
97
+ return _useMediaQuerySpacing.default;
98
+ }
99
+ });
93
100
  Object.defineProperty(exports, "useOverlaidPosition", {
94
101
  enumerable: true,
95
102
  get: function () {
@@ -226,6 +233,7 @@ var _info = _interopRequireDefault(require("./info"));
226
233
  var _useCopy = _interopRequireDefault(require("./useCopy"));
227
234
  var _useHash = _interopRequireDefault(require("./useHash"));
228
235
  var _useSpacingScale = _interopRequireDefault(require("./useSpacingScale"));
236
+ var _useMediaQuerySpacing = _interopRequireDefault(require("./useMediaQuerySpacing"));
229
237
  var _useResponsiveProp = _interopRequireWildcard(require("./useResponsiveProp"));
230
238
  Object.keys(_useResponsiveProp).forEach(function (key) {
231
239
  if (key === "default" || key === "__esModule") return;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _ThemeProvider = require("../ThemeProvider");
8
+ var _useResponsiveProp = require("./useResponsiveProp");
9
+ /**
10
+ * @typedef {import('@telus-uds/system-constants/viewports').Viewport} Viewport
11
+ * @typedef {import('./props/spacingProps.js').SpacingValue} SpacingValue
12
+ * @typedef {import('./props/spacingProps.js').SpacingIndex} SpacingIndex
13
+ * @typedef {import('./props/spacingProps.js').SpacingObject} SpacingObject
14
+ */
15
+
16
+ /**
17
+ * A utility hook that simplifies implementing media query-based responsive spacing.
18
+ *
19
+ * This hook handles the complexity of:
20
+ * - Detecting if a space value is responsive (has viewport keys)
21
+ * - Fetching theme tokens for each viewport
22
+ * - Resolving the correct space index for each viewport
23
+ * - Extracting actual pixel values from theme tokens
24
+ *
25
+ * ## Usage
26
+ *
27
+ * ```jsx
28
+ * const { sizeByViewport } = useMediaQuerySpacing(space, 'spacingScale')
29
+ *
30
+ * // Use sizeByViewport to create media query styles
31
+ * const stylesByViewport = {
32
+ * xs: { padding: sizeByViewport.xs },
33
+ * sm: { padding: sizeByViewport.sm },
34
+ * md: { padding: sizeByViewport.md },
35
+ * lg: { padding: sizeByViewport.lg },
36
+ * xl: { padding: sizeByViewport.xl }
37
+ * }
38
+ * const mediaQueryStyles = createMediaQueryStyles(stylesByViewport)
39
+ * ```
40
+ *
41
+ * ## Parameters
42
+ *
43
+ * @param {SpacingValue} spaceValue - A spacing value (number or responsive object with viewport keys)
44
+ * @param {string} tokenKey - The theme token key to use (e.g., 'spacingScale', 'Typography')
45
+ * @param {object} [tokens={}] - Additional tokens to pass to useThemeTokens
46
+ * @param {object} [variant={}] - Variant to pass to useThemeTokens
47
+ *
48
+ * ## Returns
49
+ *
50
+ * @returns {{
51
+ * spaceIndexByViewport: { xs: number, sm: number, md: number, lg: number, xl: number },
52
+ * sizeByViewport: { xs: number, sm: number, md: number, lg: number, xl: number },
53
+ * tokensByViewport: { xs: object, sm: object, md: object, lg: object, xl: object }
54
+ * }}
55
+ *
56
+ * - `spaceIndexByViewport`: The resolved space index for each viewport
57
+ * - `sizeByViewport`: The actual pixel/number values for each viewport
58
+ * - `tokensByViewport`: The full theme tokens for each viewport (for advanced use cases)
59
+ */
60
+ const useMediaQuerySpacing = function (spaceValue) {
61
+ let tokenKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'spacingScale';
62
+ let tokens = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
63
+ let variant = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
64
+ const isResponsive = typeof spaceValue === 'object' && spaceValue !== null && !spaceValue.space && !spaceValue.options;
65
+ const getSpaceIndex = viewport => {
66
+ if (isResponsive) {
67
+ return (0, _useResponsiveProp.resolveResponsiveProp)(spaceValue, viewport);
68
+ }
69
+ if (typeof spaceValue === 'number') {
70
+ return spaceValue;
71
+ }
72
+ return spaceValue?.space ?? 1;
73
+ };
74
+ const spaceIndexByViewport = {
75
+ xs: getSpaceIndex('xs'),
76
+ sm: getSpaceIndex('sm'),
77
+ md: getSpaceIndex('md'),
78
+ lg: getSpaceIndex('lg'),
79
+ xl: getSpaceIndex('xl')
80
+ };
81
+ const tokensXs = (0, _ThemeProvider.useThemeTokens)(tokenKey, tokens, variant, {
82
+ space: spaceIndexByViewport.xs,
83
+ viewport: 'xs'
84
+ });
85
+ const tokensSm = (0, _ThemeProvider.useThemeTokens)(tokenKey, tokens, variant, {
86
+ space: spaceIndexByViewport.sm,
87
+ viewport: 'sm'
88
+ });
89
+ const tokensMd = (0, _ThemeProvider.useThemeTokens)(tokenKey, tokens, variant, {
90
+ space: spaceIndexByViewport.md,
91
+ viewport: 'md'
92
+ });
93
+ const tokensLg = (0, _ThemeProvider.useThemeTokens)(tokenKey, tokens, variant, {
94
+ space: spaceIndexByViewport.lg,
95
+ viewport: 'lg'
96
+ });
97
+ const tokensXl = (0, _ThemeProvider.useThemeTokens)(tokenKey, tokens, variant, {
98
+ space: spaceIndexByViewport.xl,
99
+ viewport: 'xl'
100
+ });
101
+ const sizeByViewport = {
102
+ xs: tokensXs.size ?? 0,
103
+ sm: tokensSm.size ?? 0,
104
+ md: tokensMd.size ?? 0,
105
+ lg: tokensLg.size ?? 0,
106
+ xl: tokensXl.size ?? 0
107
+ };
108
+ const tokensByViewport = {
109
+ xs: tokensXs,
110
+ sm: tokensSm,
111
+ md: tokensMd,
112
+ lg: tokensLg,
113
+ xl: tokensXl
114
+ };
115
+ return {
116
+ spaceIndexByViewport,
117
+ sizeByViewport,
118
+ tokensByViewport
119
+ };
120
+ };
121
+ var _default = exports.default = useMediaQuerySpacing;
@@ -55,6 +55,7 @@ const selectDescriptionTextStyles = tokens => ({
55
55
  fontName: tokens?.descriptionFontName,
56
56
  fontSize: tokens?.descriptionFontSize,
57
57
  fontWeight: tokens?.descriptionFontWeight,
58
+ lineHeight: tokens?.descriptionLineHeight,
58
59
  fontColor: tokens?.color
59
60
  }),
60
61
  paddingBottom: tokens?.descriptionTextPaddingBottom
@@ -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,
@@ -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: {
@@ -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';
@@ -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",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.19.0",
87
+ "version": "3.20.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",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.19.0",
87
+ "version": "3.20.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
@@ -62,36 +62,28 @@ const ExpandCollapseMiniControl = React.forwardRef(
62
62
  const iconBaselineOffset = 0
63
63
  const hoverTranslateY = 4
64
64
 
65
- // Calculate baseline alignment to vertically center icon with text
66
- // This combines font and icon metrics with adjustments for visual balance
67
- const fontBaseline = fontSize / hoverTranslateY // Quarter of font size - adjusts for text's visual center point
68
- const iconBaseline = iconSize / hoverTranslateY // Quarter of icon size - adjusts for icon's visual center point
69
- const staticOffset = hoverTranslateY // Fixed downward adjustment to fine-tune vertical alignment
70
- const sizeCompensation = -Math.abs(iconSize - fontSize) // Compensates when icon and text sizes differ significantly
65
+ const fontBaseline = fontSize / hoverTranslateY
66
+ const iconBaseline = iconSize / hoverTranslateY
67
+ const staticOffset = hoverTranslateY
68
+ const sizeCompensation = -Math.abs(iconSize - fontSize)
71
69
 
72
70
  const baselineAlignment = fontBaseline + iconBaseline - staticOffset + sizeCompensation
73
71
 
74
- if (Platform.OS !== 'web') {
75
- // For native platforms, use baseline alignment with optional offset
76
- return { iconTranslateY: baselineAlignment + iconBaselineOffset }
77
- }
72
+ const mobileAdjustment = Platform.OS !== 'web' ? -2 : 0
78
73
 
79
74
  if (isHovered) {
80
- // Apply animation offset to the baseline-aligned position
81
- // When expanded: move icon UP (1.3 the hover distance for clear movement)
82
- // When collapsed: move icon DOWN (single hover distance)
83
75
  const hoverMovementDistance = 1.3
84
76
  const animationOffset = expanded
85
77
  ? -(hoverTranslateY * hoverMovementDistance)
86
78
  : hoverTranslateY
87
79
 
88
80
  return {
89
- iconTranslateY: baselineAlignment + iconBaselineOffset + animationOffset
81
+ iconTranslateY:
82
+ baselineAlignment + iconBaselineOffset + animationOffset + mobileAdjustment
90
83
  }
91
84
  }
92
85
 
93
- // Default state uses baseline alignment with optional offset
94
- return { iconTranslateY: baselineAlignment + iconBaselineOffset }
86
+ return { iconTranslateY: baselineAlignment + iconBaselineOffset + mobileAdjustment }
95
87
  }
96
88
 
97
89
  return (
@@ -103,6 +95,7 @@ const ExpandCollapseMiniControl = React.forwardRef(
103
95
  ...linkTokens,
104
96
  ...getTokens(linkState),
105
97
  iconSize,
98
+ blockFontSize: fontSize,
106
99
  blockLineHeight: lineHeight
107
100
  })}
108
101
  ref={ref}
@@ -171,9 +171,12 @@ const LinkBase = React.forwardRef(
171
171
  const themeTokens = resolveLinkTokens(linkState)
172
172
  const outerBorderStyles = selectOuterBorderStyles(themeTokens)
173
173
  const decorationStyles = selectDecorationStyles(themeTokens)
174
+
175
+ const mobileCompensation = null
176
+
174
177
  return [
175
178
  outerBorderStyles,
176
- staticStyles.outerBorderStyles,
179
+ mobileCompensation,
177
180
  blockLeftStyle,
178
181
  decorationStyles,
179
182
  hasIcon && staticStyles.rowContainer
@@ -191,11 +194,14 @@ const LinkBase = React.forwardRef(
191
194
  const IconComponent = icon || themeTokens.icon
192
195
  const { iconSpace } = themeTokens
193
196
 
197
+ const isTextOnlyLink = !IconComponent && !icon && accessibilityRole === 'link'
198
+ const adjustedIconSpace = Platform.OS !== 'web' && isTextOnlyLink ? 0 : iconSpace
199
+
194
200
  return (
195
201
  <IconText
196
202
  icon={IconComponent}
197
203
  iconPosition={iconPosition}
198
- space={iconSpace}
204
+ space={adjustedIconSpace}
199
205
  iconProps={{
200
206
  ...iconProps,
201
207
  tokens: iconTokens,
@@ -274,15 +280,11 @@ const staticStyles = StyleSheet.create({
274
280
  }
275
281
  })
276
282
  },
277
- outerBorderStyles: {
283
+ outerBorderCompensation: {
278
284
  ...(Platform.OS !== 'web' && {
279
- margin: 0,
280
285
  marginHorizontal: 2,
281
- padding: 0
282
- }),
283
- ...(Platform.OS === 'android' && {
284
- paddingHorizontal: 2,
285
- paddingTop: 2
286
+ paddingHorizontal: Platform.OS === 'android' ? 2 : 0,
287
+ paddingTop: Platform.OS === 'android' ? 2 : 0
286
288
  })
287
289
  }
288
290
  })
@@ -65,7 +65,6 @@ const selectContainerStyle = (windowHeight, windowWidth) => ({
65
65
  })
66
66
 
67
67
  const TOTAL_COLUMNS = 12
68
- const MAX_ITEMS_THRESHOLD = 12
69
68
 
70
69
  const MultiSelectFilter = React.forwardRef(
71
70
  (
@@ -174,13 +173,15 @@ const MultiSelectFilter = React.forwardRef(
174
173
  const getCopy = useCopy({ dictionary, copy })
175
174
  const colSizeNotMobile = items.length > rowLimit ? 2 : 1
176
175
  const colSize = viewport !== 'xs' ? colSizeNotMobile : 1
177
- const itemsLengthNotMobile = items.length > 24 ? items.length / 2 : rowLimit
178
- const rowLength = viewport !== 'xs' ? itemsLengthNotMobile : items.length
176
+
177
+ let rowLength = items.length
178
+ if (viewport !== 'xs' && colSize === 2) {
179
+ rowLength = Math.ceil(items.length / 2)
180
+ }
179
181
 
180
182
  React.useEffect(() => {
181
- if (colSize === 1) return setMaxWidth(false)
182
- return colSize === 2 && setMaxWidth(true)
183
- }, [colSize])
183
+ setMaxWidth(items.length >= rowLimit)
184
+ }, [items.length, rowLimit])
184
185
 
185
186
  React.useEffect(() => setCheckedIds(currentValues ?? []), [currentValues])
186
187
 
@@ -414,14 +415,14 @@ const MultiSelectFilter = React.forwardRef(
414
415
  dismissWhenPressedOutside={dismissWhenPressedOutside}
415
416
  onClose={onClose}
416
417
  overlaidPosition={overlaidPosition}
417
- maxHeight={items.length > MAX_ITEMS_THRESHOLD ? true : maxHeight}
418
+ maxHeight={items.length >= rowLimit ? true : maxHeight}
418
419
  maxHeightSize={maxHeightSize}
419
420
  maxWidthSize={maxWidthSize}
420
421
  minHeight={minHeight}
421
422
  minWidth={minWidth}
422
423
  tokens={{
423
424
  ...tokens,
424
- maxWidth: items.length > MAX_ITEMS_THRESHOLD ? true : maxWidth,
425
+ maxWidth: items.length >= rowLimit ? true : maxWidth,
425
426
  borderColor: containerBorderColor
426
427
  }}
427
428
  copy={copy}
@@ -1,7 +1,17 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { StyleSheet, View } from 'react-native'
4
- import { a11yProps, selectSystemProps, spacingProps, useSpacingScale, viewProps } from '../utils'
3
+ import { View, StyleSheet } from 'react-native'
4
+ import {
5
+ a11yProps,
6
+ selectSystemProps,
7
+ spacingProps,
8
+ useSpacingScale,
9
+ viewProps,
10
+ StyleSheet as StyleSheetUtils,
11
+ createMediaQueryStyles
12
+ } from '../utils'
13
+ import useMediaQuerySpacing from '../utils/useMediaQuerySpacing'
14
+ import useTheme from '../ThemeProvider/useTheme'
5
15
 
6
16
  /**
7
17
  * @typedef {import('../utils/props/spacingProps.js').SpacingValue} SpacingValue
@@ -56,11 +66,43 @@ const selectSizeStyle = (size, direction) => ({
56
66
  * Spacer has no content and is ignored by tools such as screen readers. Use `Divider` for
57
67
  * separations between elements that may be treated as semantically meaningful on web.
58
68
  */
59
- const Spacer = React.forwardRef(({ space = 1, direction = 'column', ...rest }, ref) => {
60
- const size = useSpacingScale(space)
61
- const sizeStyle = selectSizeStyle(size, direction)
69
+ const Spacer = React.forwardRef(({ space = 1, direction = 'column', dataSet, ...rest }, ref) => {
70
+ const {
71
+ themeOptions: { enableMediaQueryStyleSheet }
72
+ } = useTheme()
62
73
 
63
- return <View ref={ref} style={[staticStyles.stretch, sizeStyle]} {...selectProps(rest)} />
74
+ const { sizeByViewport } = useMediaQuerySpacing(space)
75
+
76
+ const fallbackSize = useSpacingScale(space)
77
+ const sizeStyle = selectSizeStyle(fallbackSize, direction)
78
+
79
+ let spacerStyles
80
+ let dataSetValue = dataSet
81
+
82
+ if (enableMediaQueryStyleSheet) {
83
+ const sizeKey = direction === 'row' ? 'width' : 'height'
84
+ const stylesByViewport = {
85
+ xs: { [sizeKey]: sizeByViewport.xs, ...staticStyles.stretch },
86
+ sm: { [sizeKey]: sizeByViewport.sm, ...staticStyles.stretch },
87
+ md: { [sizeKey]: sizeByViewport.md, ...staticStyles.stretch },
88
+ lg: { [sizeKey]: sizeByViewport.lg, ...staticStyles.stretch },
89
+ xl: { [sizeKey]: sizeByViewport.xl, ...staticStyles.stretch }
90
+ }
91
+ const mediaQueryStyles = createMediaQueryStyles(stylesByViewport)
92
+
93
+ const { ids, styles } = StyleSheetUtils.create({
94
+ spacer: {
95
+ ...mediaQueryStyles
96
+ }
97
+ })
98
+
99
+ spacerStyles = styles.spacer
100
+ dataSetValue = { media: ids.spacer, ...dataSet }
101
+ } else {
102
+ spacerStyles = [staticStyles.stretch, sizeStyle]
103
+ }
104
+
105
+ return <View ref={ref} style={spacerStyles} dataSet={dataSetValue} {...selectProps(rest)} />
64
106
  })
65
107
  Spacer.displayName = 'Spacer'
66
108
 
@@ -78,7 +120,12 @@ Spacer.propTypes = {
78
120
  * - `'column'` (default) applies space vertically; has a fixed height and not width.
79
121
  * - `'row'` applies space horizontally; has a fixed width and not height.
80
122
  */
81
- direction: PropTypes.oneOf(['column', 'row'])
123
+ direction: PropTypes.oneOf(['column', 'row']),
124
+ /**
125
+ * Data attributes to be applied to the element. When media query stylesheet is enabled,
126
+ * this will include media query IDs for responsive styling.
127
+ */
128
+ dataSet: PropTypes.object
82
129
  }
83
130
 
84
131
  const staticStyles = StyleSheet.create({
@@ -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'
@@ -0,0 +1,124 @@
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 = (spaceValue, tokenKey = 'spacingScale', tokens = {}, variant = {}) => {
56
+ const isResponsive =
57
+ typeof spaceValue === 'object' &&
58
+ spaceValue !== null &&
59
+ !spaceValue.space &&
60
+ !spaceValue.options
61
+
62
+ const getSpaceIndex = (viewport) => {
63
+ if (isResponsive) {
64
+ return resolveResponsiveProp(spaceValue, viewport)
65
+ }
66
+ if (typeof spaceValue === 'number') {
67
+ return spaceValue
68
+ }
69
+ return spaceValue?.space ?? 1
70
+ }
71
+
72
+ const spaceIndexByViewport = {
73
+ xs: getSpaceIndex('xs'),
74
+ sm: getSpaceIndex('sm'),
75
+ md: getSpaceIndex('md'),
76
+ lg: getSpaceIndex('lg'),
77
+ xl: getSpaceIndex('xl')
78
+ }
79
+
80
+ const tokensXs = useThemeTokens(tokenKey, tokens, variant, {
81
+ space: spaceIndexByViewport.xs,
82
+ viewport: 'xs'
83
+ })
84
+ const tokensSm = useThemeTokens(tokenKey, tokens, variant, {
85
+ space: spaceIndexByViewport.sm,
86
+ viewport: 'sm'
87
+ })
88
+ const tokensMd = useThemeTokens(tokenKey, tokens, variant, {
89
+ space: spaceIndexByViewport.md,
90
+ viewport: 'md'
91
+ })
92
+ const tokensLg = useThemeTokens(tokenKey, tokens, variant, {
93
+ space: spaceIndexByViewport.lg,
94
+ viewport: 'lg'
95
+ })
96
+ const tokensXl = useThemeTokens(tokenKey, tokens, variant, {
97
+ space: spaceIndexByViewport.xl,
98
+ viewport: 'xl'
99
+ })
100
+
101
+ const sizeByViewport = {
102
+ xs: tokensXs.size ?? 0,
103
+ sm: tokensSm.size ?? 0,
104
+ md: tokensMd.size ?? 0,
105
+ lg: tokensLg.size ?? 0,
106
+ xl: tokensXl.size ?? 0
107
+ }
108
+
109
+ const tokensByViewport = {
110
+ xs: tokensXs,
111
+ sm: tokensSm,
112
+ md: tokensMd,
113
+ lg: tokensLg,
114
+ xl: tokensXl
115
+ }
116
+
117
+ return {
118
+ spaceIndexByViewport,
119
+ sizeByViewport,
120
+ tokensByViewport
121
+ }
122
+ }
123
+
124
+ export default useMediaQuerySpacing