@telus-uds/components-base 3.18.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/jest.config.cjs +10 -2
  3. package/lib/cjs/Box/Box.js +114 -62
  4. package/lib/cjs/Box/backgroundImageStylesMap.js +136 -28
  5. package/lib/cjs/Button/ButtonDropdown.js +1 -0
  6. package/lib/cjs/Carousel/Carousel.js +1 -1
  7. package/lib/cjs/ExpandCollapseMini/ExpandCollapseMiniControl.js +8 -21
  8. package/lib/cjs/Link/LinkBase.js +8 -9
  9. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +10 -10
  10. package/lib/cjs/Spacer/Spacer.js +65 -5
  11. package/lib/cjs/StepTracker/Step.js +12 -1
  12. package/lib/cjs/StepTracker/StepTracker.js +15 -4
  13. package/lib/cjs/TabBar/TabBar.js +4 -2
  14. package/lib/cjs/TabBar/index.js +2 -0
  15. package/lib/cjs/Tooltip/Backdrop.js +1 -1
  16. package/lib/cjs/utils/index.js +17 -1
  17. package/lib/cjs/utils/isTouchDevice.js +34 -0
  18. package/lib/cjs/utils/useMediaQuerySpacing.js +121 -0
  19. package/lib/esm/Box/Box.js +113 -63
  20. package/lib/esm/Box/backgroundImageStylesMap.js +134 -27
  21. package/lib/esm/Button/ButtonDropdown.js +1 -0
  22. package/lib/esm/Carousel/Carousel.js +2 -2
  23. package/lib/esm/ExpandCollapseMini/ExpandCollapseMiniControl.js +8 -21
  24. package/lib/esm/Link/LinkBase.js +8 -9
  25. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +10 -10
  26. package/lib/esm/Spacer/Spacer.js +66 -6
  27. package/lib/esm/StepTracker/Step.js +12 -1
  28. package/lib/esm/StepTracker/StepTracker.js +15 -4
  29. package/lib/esm/TabBar/TabBar.js +4 -2
  30. package/lib/esm/TabBar/index.js +2 -0
  31. package/lib/esm/Tooltip/Backdrop.js +1 -1
  32. package/lib/esm/utils/index.js +3 -1
  33. package/lib/esm/utils/isTouchDevice.js +27 -0
  34. package/lib/esm/utils/useMediaQuerySpacing.js +116 -0
  35. package/lib/package.json +2 -2
  36. package/package.json +2 -2
  37. package/src/Box/Box.jsx +97 -55
  38. package/src/Box/backgroundImageStylesMap.js +48 -15
  39. package/src/Button/ButtonDropdown.jsx +1 -0
  40. package/src/Carousel/Carousel.jsx +3 -2
  41. package/src/ExpandCollapseMini/ExpandCollapseMiniControl.jsx +9 -16
  42. package/src/Link/LinkBase.jsx +11 -9
  43. package/src/MultiSelectFilter/MultiSelectFilter.jsx +11 -10
  44. package/src/Spacer/Spacer.jsx +54 -7
  45. package/src/StepTracker/Step.jsx +47 -27
  46. package/src/StepTracker/StepTracker.jsx +9 -1
  47. package/src/TabBar/TabBar.jsx +3 -1
  48. package/src/TabBar/index.js +3 -0
  49. package/src/Tooltip/Backdrop.jsx +1 -1
  50. package/src/utils/index.js +2 -0
  51. package/src/utils/isTouchDevice.js +34 -0
  52. package/src/utils/useMediaQuerySpacing.js +124 -0
@@ -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: {
@@ -150,6 +150,13 @@ const getStepTestID = (isCompleted, isCurrent) => {
150
150
  }
151
151
  return testID;
152
152
  };
153
+ const selectBarContainerStyles = (themeTokens, isCompleted, isCurrent) => ({
154
+ backgroundColor: isCompleted ? themeTokens.barCompletedBackgroundColor : themeTokens.barBackgroundColor,
155
+ height: themeTokens.barHeight,
156
+ ...(isCurrent && {
157
+ backgroundColor: themeTokens.barCurrentBackgroundColor
158
+ })
159
+ });
153
160
 
154
161
  /**
155
162
  * A single step of a StepTracker.
@@ -162,6 +169,7 @@ const Step = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
162
169
  stepCount = 0,
163
170
  stepIndex = 0,
164
171
  tokens,
172
+ isBarVariant,
165
173
  ...rest
166
174
  } = _ref8;
167
175
  const {
@@ -190,7 +198,10 @@ const Step = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
190
198
  accessibilityCurrent: status === stepIndex,
191
199
  ref: ref,
192
200
  ...selectProps(rest),
193
- children: [/*#__PURE__*/_jsxs(StackView, {
201
+ children: [isBarVariant && /*#__PURE__*/_jsx(View, {
202
+ style: selectBarContainerStyles(themeTokens, isCompleted, isCurrent),
203
+ testID: getStepTestID(isCompleted, isCurrent)
204
+ }), !isBarVariant && /*#__PURE__*/_jsxs(StackView, {
194
205
  direction: "row",
195
206
  space: 0,
196
207
  tokens: {
@@ -12,6 +12,7 @@ import useCopy from '../utils/useCopy';
12
12
  import Step from './Step';
13
13
  import defaultDictionary from './dictionary';
14
14
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ const STYLE_BAR_VARIANT = 'bar';
15
16
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
16
17
  const selectContainerStyles = _ref => {
17
18
  let {
@@ -52,6 +53,14 @@ const selectStepTrackerLabelStyles = (_ref3, themeOptions) => {
52
53
  themeOptions
53
54
  });
54
55
  };
56
+ const selectStepsContainerStyles = _ref4 => {
57
+ let {
58
+ barGap
59
+ } = _ref4;
60
+ return {
61
+ gap: barGap
62
+ };
63
+ };
55
64
 
56
65
  /**
57
66
  * StepTracker component shows the current position in a sequence of steps.
@@ -85,7 +94,7 @@ const selectStepTrackerLabelStyles = (_ref3, themeOptions) => {
85
94
  * - `accessibilityValue.text` (`aria-valuetext`): `<Current Step Label>`,
86
95
  *
87
96
  */
88
- const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
97
+ const StepTracker = /*#__PURE__*/React.forwardRef((_ref5, ref) => {
89
98
  let {
90
99
  current = 0,
91
100
  copy = 'en',
@@ -94,7 +103,8 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
94
103
  tokens,
95
104
  variant,
96
105
  ...rest
97
- } = _ref4;
106
+ } = _ref5;
107
+ const isBarVariant = variant?.style === STYLE_BAR_VARIANT;
98
108
  const viewport = useViewport();
99
109
  const {
100
110
  showStepTrackerLabel,
@@ -140,7 +150,7 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
140
150
  children: /*#__PURE__*/_jsxs(StackView, {
141
151
  space: 0,
142
152
  children: [/*#__PURE__*/_jsx(View, {
143
- style: staticStyles.stepsContainer,
153
+ style: [staticStyles.stepsContainer, selectStepsContainerStyles(themeTokens)],
144
154
  accessibilityRole: stepsContainerAccessibilityRole,
145
155
  children: steps.map((label, index) => {
146
156
  return /*#__PURE__*/_jsx(Step, {
@@ -151,7 +161,8 @@ const StepTracker = /*#__PURE__*/React.forwardRef((_ref4, ref) => {
151
161
  stepCount: steps.length,
152
162
  tokens: themeTokens,
153
163
  accessibilityRole: stepAccessibilityRole,
154
- accessibilityCurrent: current === index && Platform.OS === 'web' && 'step'
164
+ accessibilityCurrent: current === index && Platform.OS === 'web' && 'step',
165
+ isBarVariant: isBarVariant
155
166
  }, label);
156
167
  })
157
168
  }), showStepTrackerLabel && /*#__PURE__*/_jsx(View, {
@@ -84,7 +84,8 @@ const TabBar = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
84
84
  iconActive: item.iconActive,
85
85
  onPress: () => handlePress(item.id),
86
86
  id: `tab-item-${index}`,
87
- accessibilityRole: "tablist"
87
+ accessibilityRole: "tablist",
88
+ tokens: item.tokens
88
89
  }, item.id))
89
90
  })
90
91
  });
@@ -98,7 +99,8 @@ TabBar.propTypes = {
98
99
  icon: PropTypes.node,
99
100
  iconActive: PropTypes.node,
100
101
  label: PropTypes.string.isRequired,
101
- href: PropTypes.string
102
+ href: PropTypes.string,
103
+ tokens: getTokensPropType('TabBarItem')
102
104
  })).isRequired,
103
105
  /** Id of the initially selected item. */
104
106
  initiallySelectedItem: PropTypes.number,
@@ -1,2 +1,4 @@
1
1
  import TabBar from './TabBar';
2
+ import TabBarItem from './TabBarItem';
3
+ TabBar.Item = TabBarItem;
2
4
  export default TabBar;
@@ -14,7 +14,7 @@ function createPortalNode(nodeId) {
14
14
  left: ${window.scrollX}px;
15
15
  right: 0;
16
16
  bottom: 0;
17
- z-index: 9999;
17
+ z-index: 100000;
18
18
  pointer-events: none;
19
19
  `;
20
20
  document.body.appendChild(node);
@@ -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';
@@ -24,4 +25,5 @@ export { transformGradient } from './transformGradient';
24
25
  export { default as convertFromMegaByteToByte } from './convertFromMegaByteToByte';
25
26
  export { default as formatImageSource } from './formatImageSource';
26
27
  export { default as getSpacingScale } from './getSpacingScale';
27
- export { default as useVariants } from './useVariants';
28
+ export { default as useVariants } from './useVariants';
29
+ export { default as isTouchDevice } from './isTouchDevice';
@@ -0,0 +1,27 @@
1
+ import Platform from "react-native-web/dist/exports/Platform";
2
+ /**
3
+ * Determines if the current device supports touch interactions
4
+ *
5
+ * @returns {boolean} True if the device supports touch, false otherwise
6
+ */
7
+ const isTouchDevice = () => {
8
+ if (Platform.OS !== 'web') {
9
+ return true;
10
+ }
11
+ if (typeof window !== 'undefined') {
12
+ if ('ontouchstart' in window) {
13
+ return true;
14
+ }
15
+ if (window.navigator && window.navigator.maxTouchPoints > 0) {
16
+ return true;
17
+ }
18
+ if (window.navigator && window.navigator.msMaxTouchPoints > 0) {
19
+ return true;
20
+ }
21
+ if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) {
22
+ return true;
23
+ }
24
+ }
25
+ return false;
26
+ };
27
+ export default isTouchDevice;
@@ -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.14.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.18.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.14.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.18.0",
87
+ "version": "3.20.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/src/Box/Box.jsx CHANGED
@@ -21,9 +21,10 @@ import {
21
21
  variantProp,
22
22
  viewProps,
23
23
  StyleSheet as RNMQStyleSheet,
24
- getSpacingScale
24
+ getSpacingScale,
25
+ formatImageSource
25
26
  } from '../utils'
26
- import backgroundImageStylesMap from './backgroundImageStylesMap'
27
+ import backgroundImageStylesMap, { backgroundPositions } from './backgroundImageStylesMap'
27
28
  import { useViewport } from '../ViewportProvider'
28
29
 
29
30
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
@@ -87,38 +88,60 @@ const setBackgroundImage = ({
87
88
  backgroundImageResizeMode,
88
89
  backgroundImagePosition,
89
90
  backgroundImageAlign,
90
- backgroundImageWidth,
91
- backgroundImageHeight,
92
- content
91
+ content,
92
+ testID
93
93
  }) => {
94
- if (backgroundImageResizeMode === 'contain') {
95
- const containedViewStyle = {
96
- ...staticStyles.containedView,
97
- width: backgroundImageWidth,
98
- height: backgroundImageHeight,
99
- ...backgroundImageStylesMap[`${backgroundImagePosition}-${backgroundImageAlign}`]
94
+ const backgroundImageTestID = testID ? `${testID}-background-image` : undefined
95
+
96
+ if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
97
+ const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`
98
+
99
+ if (Platform.OS === 'web') {
100
+ const backgroundPosition = backgroundPositions[positionKey] || 'center center'
101
+
102
+ const backgroundImageStyle = {
103
+ backgroundImage: `url(${src})`,
104
+ backgroundSize: 'contain',
105
+ backgroundRepeat: 'no-repeat',
106
+ backgroundPosition
107
+ }
108
+
109
+ return (
110
+ <View
111
+ style={[staticStyles.imageBackground, backgroundImageStyle]}
112
+ aria-label={alt}
113
+ testID={backgroundImageTestID}
114
+ >
115
+ {content}
116
+ </View>
117
+ )
100
118
  }
119
+ const positionStyles = backgroundImageStylesMap[positionKey] || {}
101
120
 
102
121
  return (
103
- <View style={staticStyles.containedContainer}>
104
- <View style={containedViewStyle}>
105
- <Image
106
- source={{ uri: src }}
107
- alt={alt}
108
- style={staticStyles.containedImage}
109
- accessibilityIgnoresInvertColors
110
- />
111
- </View>
112
- {content}
122
+ <View style={staticStyles.containContainer}>
123
+ <Image
124
+ source={src}
125
+ resizeMode={backgroundImageResizeMode}
126
+ style={[staticStyles.containImage, positionStyles]}
127
+ accessible={true}
128
+ accessibilityLabel={alt}
129
+ accessibilityIgnoresInvertColors={true}
130
+ testID={backgroundImageTestID}
131
+ />
132
+ <View style={staticStyles.contentOverlay}>{content}</View>
113
133
  </View>
114
134
  )
115
135
  }
136
+
116
137
  return (
117
138
  <ImageBackground
118
- source={{ uri: src }}
119
- alt={alt}
120
- style={staticStyles.backgroundImageContainer}
139
+ source={src}
121
140
  resizeMode={backgroundImageResizeMode}
141
+ style={staticStyles.imageBackground}
142
+ accessible={true}
143
+ accessibilityLabel={alt}
144
+ testID={backgroundImageTestID}
122
145
  >
123
146
  {content}
124
147
  </ImageBackground>
@@ -279,33 +302,47 @@ const Box = React.forwardRef(
279
302
 
280
303
  const { src = '', alt = '', resizeMode = '', position = '', align = '' } = backgroundImage || {}
281
304
  const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover')
282
- const backgroundImagePosition = useResponsiveProp(position, 'none')
283
- const backgroundImageAlign = useResponsiveProp(align, 'stretch')
284
- const [backgroundImageWidth, setBackgroundImageWidth] = React.useState(0)
285
- const [backgroundImageHeight, setBackgroundImageHeight] = React.useState(0)
286
- if (backgroundImage)
305
+ const backgroundImagePosition = useResponsiveProp(position)
306
+ const backgroundImageAlign = useResponsiveProp(align)
307
+ const imageSourceViewport = formatImageSource(useResponsiveProp(src))
308
+
309
+ if (backgroundImage && src) {
310
+ const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = boxStyles
311
+
312
+ const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
313
+ const paddedContent = hasPadding ? (
314
+ <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
315
+ ) : (
316
+ children
317
+ )
318
+
287
319
  content = setBackgroundImage({
288
- src,
320
+ src: imageSourceViewport,
289
321
  alt,
290
322
  backgroundImageResizeMode,
291
323
  backgroundImagePosition,
292
324
  backgroundImageAlign,
293
- backgroundImageWidth,
294
- backgroundImageHeight,
295
- content
325
+ content: paddedContent,
326
+ testID
296
327
  })
297
328
 
298
- React.useEffect(() => {
299
- if (backgroundImage && backgroundImageWidth === 0 && backgroundImageHeight === 0) {
300
- Image.getSize(src, (width, height) => {
301
- // Only update the state if the size has changed
302
- if (width !== backgroundImageWidth || height !== backgroundImageHeight) {
303
- setBackgroundImageWidth(width)
304
- setBackgroundImageHeight(height)
305
- }
306
- })
329
+ const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet
330
+
331
+ if (scroll) {
332
+ const scrollProps = typeof scroll === 'object' ? scroll : {}
333
+ scrollProps.contentContainerStyle = [containerStyle, scrollProps.contentContainerStyle]
334
+ return (
335
+ <ScrollView {...scrollProps} {...props} testID={testID} dataSet={dataSetValue} ref={ref}>
336
+ {content}
337
+ </ScrollView>
338
+ )
307
339
  }
308
- }, [backgroundImage, backgroundImageWidth, backgroundImageHeight, src])
340
+ return (
341
+ <View {...props} style={containerStyle} testID={testID} dataSet={dataSetValue} ref={ref}>
342
+ {content}
343
+ </View>
344
+ )
345
+ }
309
346
 
310
347
  const dataSetValue = boxMediaIds ? { media: boxMediaIds, ...dataSet } : dataSet
311
348
 
@@ -416,10 +453,12 @@ Box.propTypes = {
416
453
  */
417
454
  customGradient: PropTypes.func,
418
455
  /**
419
- * Use this prop to add a background image to the box.
456
+ * Apply background image to the box.
420
457
  */
421
458
  backgroundImage: PropTypes.shape({
422
- src: PropTypes.string.isRequired,
459
+ // The image src is either a URI string or a number (when a local image src is bundled in IOS or Android app)
460
+ // src is an object when used responsively to provide different image sources for different screen sizes
461
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]).isRequired,
423
462
  alt: PropTypes.string,
424
463
  resizeMode: responsiveProps.getTypeOptionallyByViewport(
425
464
  PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
@@ -436,18 +475,21 @@ Box.propTypes = {
436
475
  export default Box
437
476
 
438
477
  const staticStyles = StyleSheet.create({
439
- backgroundImageContainer: {
440
- flex: 1
441
- },
442
- containedContainer: {
443
- flex: 1,
444
- overflow: 'hidden'
478
+ imageBackground: { width: '100%', height: '100%' },
479
+ contentOverlay: {
480
+ position: 'relative',
481
+ width: '100%',
482
+ height: '100%',
483
+ zIndex: 1
445
484
  },
446
- containedView: {
447
- zIndex: -1,
448
- position: 'absolute'
485
+ containContainer: {
486
+ width: '100%',
487
+ height: '100%',
488
+ overflow: 'hidden',
489
+ position: 'relative'
449
490
  },
450
- containedImage: {
491
+ containImage: {
492
+ position: 'absolute',
451
493
  width: '100%',
452
494
  height: '100%'
453
495
  }