@telus-uds/components-base 3.13.0 → 3.14.1

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 (36) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/lib/cjs/BaseProvider/index.js +4 -1
  3. package/lib/cjs/Card/Card.js +23 -4
  4. package/lib/cjs/Card/CardBase.js +170 -19
  5. package/lib/cjs/Card/PressableCardBase.js +19 -5
  6. package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
  7. package/lib/cjs/FlexGrid/FlexGrid.js +28 -12
  8. package/lib/cjs/Tabs/Tabs.js +34 -2
  9. package/lib/cjs/Tabs/TabsDropdown.js +252 -0
  10. package/lib/cjs/Tabs/TabsItem.js +4 -2
  11. package/lib/cjs/Tabs/dictionary.js +14 -0
  12. package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
  13. package/lib/esm/BaseProvider/index.js +4 -1
  14. package/lib/esm/Card/Card.js +21 -4
  15. package/lib/esm/Card/CardBase.js +169 -19
  16. package/lib/esm/Card/PressableCardBase.js +19 -5
  17. package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
  18. package/lib/esm/FlexGrid/FlexGrid.js +28 -12
  19. package/lib/esm/Tabs/Tabs.js +35 -3
  20. package/lib/esm/Tabs/TabsDropdown.js +245 -0
  21. package/lib/esm/Tabs/TabsItem.js +4 -2
  22. package/lib/esm/Tabs/dictionary.js +8 -0
  23. package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
  24. package/lib/package.json +2 -2
  25. package/package.json +2 -2
  26. package/src/BaseProvider/index.jsx +4 -2
  27. package/src/Card/Card.jsx +27 -3
  28. package/src/Card/CardBase.jsx +165 -19
  29. package/src/Card/PressableCardBase.jsx +31 -4
  30. package/src/Card/backgroundImageStylesMap.js +41 -0
  31. package/src/FlexGrid/FlexGrid.jsx +30 -13
  32. package/src/Tabs/Tabs.jsx +36 -2
  33. package/src/Tabs/TabsDropdown.jsx +265 -0
  34. package/src/Tabs/TabsItem.jsx +4 -2
  35. package/src/Tabs/dictionary.js +8 -0
  36. package/src/ViewportProvider/ViewportProvider.jsx +8 -3
@@ -0,0 +1,245 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import Pressable from "react-native-web/dist/exports/Pressable";
4
+ import StyleSheet from "react-native-web/dist/exports/StyleSheet";
5
+ import Text from "react-native-web/dist/exports/Text";
6
+ import View from "react-native-web/dist/exports/View";
7
+ import { useThemeTokensCallback, applyTextStyles, useTheme } from '../ThemeProvider';
8
+ import { a11yProps, getTokensPropType, resolvePressableTokens, selectSystemProps, selectTokens, useOverlaidPosition, useCopy, variantProp, viewProps, withLinkRouter } from '../utils';
9
+ import { useViewport } from '../ViewportProvider';
10
+ import Icon from '../Icon';
11
+ import Listbox from '../Listbox';
12
+ import dictionary from './dictionary';
13
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
15
+ const selectButtonContentStyles = _ref => {
16
+ let {
17
+ backgroundColor,
18
+ borderColor,
19
+ borderWidth,
20
+ borderRadius,
21
+ paddingHorizontal,
22
+ paddingVertical,
23
+ marginHorizontal,
24
+ marginVertical
25
+ } = _ref;
26
+ return {
27
+ backgroundColor,
28
+ borderColor,
29
+ borderWidth,
30
+ borderRadius,
31
+ paddingHorizontal,
32
+ paddingVertical,
33
+ marginLeft: marginHorizontal,
34
+ marginRight: marginHorizontal,
35
+ marginTop: marginVertical,
36
+ marginBottom: marginVertical
37
+ };
38
+ };
39
+
40
+ /**
41
+ * TabsDropdown renders a dropdown version of tabs for mobile/tablet viewports.
42
+ * It shows the currently selected tab as a button that opens a dropdown menu
43
+ * containing all available tabs.
44
+ *
45
+ * This is rendered automatically by `Tabs` on mobile viewports and when variant
46
+ * is dropdown and isn't intended to be used directly.
47
+ */
48
+ const TabsDropdown = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
49
+ let {
50
+ itemTokens,
51
+ variant,
52
+ value,
53
+ onChange,
54
+ items = [],
55
+ LinkRouter,
56
+ linkRouterProps,
57
+ accessibilityRole = 'button',
58
+ copy = 'en',
59
+ dictionary: customDictionary = dictionary,
60
+ ...rest
61
+ } = _ref2;
62
+ const {
63
+ themeOptions
64
+ } = useTheme();
65
+ const viewport = useViewport();
66
+ const [isOpen, setIsOpen] = React.useState(false);
67
+ const getTokens = useThemeTokensCallback('TabsItem', itemTokens, {
68
+ viewport,
69
+ ...variant
70
+ });
71
+ const selectedItem = items.find(item => {
72
+ const itemId = item.id ?? item.label;
73
+ return value === itemId;
74
+ }) || items[0];
75
+ const {
76
+ overlaidPosition,
77
+ sourceRef,
78
+ targetRef,
79
+ onTargetLayout,
80
+ isReady
81
+ } = useOverlaidPosition({
82
+ isShown: isOpen,
83
+ offsets: {
84
+ vertical: 4
85
+ },
86
+ align: {
87
+ top: 'bottom',
88
+ left: 'left'
89
+ }
90
+ });
91
+ const handleToggle = () => setIsOpen(prev => !prev);
92
+ const handleClose = () => setIsOpen(false);
93
+ const handleItemSelect = (item, event) => {
94
+ const itemId = item.id ?? item.label;
95
+ setIsOpen(false);
96
+ if (onChange) onChange(itemId, event);
97
+ if (item.onPress) item.onPress(event);
98
+ };
99
+ const listboxItems = items.map(item => ({
100
+ ...item,
101
+ onPress: event => handleItemSelect(item, event)
102
+ }));
103
+ const isSelected = Boolean(selectedItem && value);
104
+ const getCopy = useCopy({
105
+ dictionary: customDictionary,
106
+ copy
107
+ });
108
+ const selectedProps = selectProps({
109
+ accessibilityRole,
110
+ ...rest
111
+ });
112
+ return /*#__PURE__*/_jsxs(View, {
113
+ ref: ref,
114
+ style: styles.container,
115
+ children: [/*#__PURE__*/_jsx(Pressable, {
116
+ ref: sourceRef,
117
+ onPress: handleToggle,
118
+ ...selectedProps,
119
+ style: styles.pressable,
120
+ children: pressableState => {
121
+ // Use resolvePressableTokens like TabBarItem does for proper state handling
122
+ const resolvedTokens = resolvePressableTokens(getTokens, pressableState, {
123
+ viewport,
124
+ expanded: isOpen,
125
+ selected: isSelected
126
+ });
127
+ const textStyles = applyTextStyles({
128
+ ...selectTokens('Typography', resolvedTokens),
129
+ themeOptions
130
+ });
131
+
132
+ // Get dropdown icons from resolved tokens
133
+ const dropdownIcon = isOpen ? resolvedTokens.dropdownIconExpanded : resolvedTokens.dropdownIcon;
134
+ return /*#__PURE__*/_jsxs(View, {
135
+ style: [styles.buttonContent, selectButtonContentStyles(resolvedTokens)],
136
+ children: [/*#__PURE__*/_jsx(Text, {
137
+ style: textStyles,
138
+ children: selectedItem?.label || getCopy('selectTab')
139
+ }), dropdownIcon && /*#__PURE__*/_jsx(Icon, {
140
+ icon: dropdownIcon,
141
+ variant: {
142
+ size: 'micro'
143
+ },
144
+ tokens: {
145
+ color: textStyles.color
146
+ }
147
+ })]
148
+ });
149
+ }
150
+ }), isOpen && /*#__PURE__*/_jsx(Listbox.Overlay, {
151
+ overlaidPosition: overlaidPosition,
152
+ maxWidth: 400,
153
+ minWidth: 200,
154
+ isReady: isReady,
155
+ onLayout: onTargetLayout,
156
+ children: /*#__PURE__*/_jsx(Listbox, {
157
+ items: listboxItems,
158
+ firstItemRef: targetRef,
159
+ parentRef: sourceRef,
160
+ selectedId: value,
161
+ onClose: handleClose,
162
+ LinkRouter: LinkRouter,
163
+ linkRouterProps: linkRouterProps
164
+ })
165
+ })]
166
+ });
167
+ });
168
+ TabsDropdown.displayName = 'TabsDropdown';
169
+ const dictionaryContentShape = PropTypes.shape({
170
+ selectTab: PropTypes.string.isRequired
171
+ });
172
+ TabsDropdown.propTypes = {
173
+ ...selectedSystemPropTypes,
174
+ ...withLinkRouter.propTypes,
175
+ /**
176
+ * Array of tab items
177
+ */
178
+ items: PropTypes.arrayOf(PropTypes.shape({
179
+ ...withLinkRouter.propTypes,
180
+ /** URL to navigate to when the tab is pressed */
181
+ href: PropTypes.string,
182
+ /** Display text for the tab */
183
+ label: PropTypes.string,
184
+ /** Unique identifier for the tab */
185
+ id: PropTypes.string,
186
+ /** Reference to the tab element */
187
+ ref: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
188
+ /** Custom render function for the tab content */
189
+ render: PropTypes.func
190
+ })),
191
+ /**
192
+ * Current selected tab id
193
+ */
194
+ value: PropTypes.string,
195
+ /**
196
+ * Callback for when the selected tab changes
197
+ */
198
+ onChange: PropTypes.func,
199
+ /**
200
+ * Custom tokens for the main Tabs container
201
+ */
202
+ tokens: getTokensPropType('Tabs'),
203
+ /**
204
+ * Custom tokens for `TabsItem`
205
+ */
206
+ itemTokens: getTokensPropType('TabsItem'),
207
+ /**
208
+ * Visual and behavioral variants for the tabs dropdown
209
+ */
210
+ variant: variantProp.propType,
211
+ /**
212
+ * Select English or French copy for the accessible labels.
213
+ * You may also pass in a custom dictionary object.
214
+ */
215
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
216
+ /**
217
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
218
+ */
219
+ dictionary: PropTypes.shape({
220
+ en: dictionaryContentShape,
221
+ fr: dictionaryContentShape
222
+ })
223
+ };
224
+ const styles = StyleSheet.create({
225
+ container: {
226
+ position: 'relative',
227
+ width: '100%'
228
+ },
229
+ pressable: {
230
+ outlineWidth: 0,
231
+ outlineStyle: 'none',
232
+ outlineColor: 'transparent'
233
+ },
234
+ buttonContent: {
235
+ display: 'flex',
236
+ flexDirection: 'row',
237
+ alignItems: 'center',
238
+ justifyContent: 'space-between',
239
+ width: '100%',
240
+ minHeight: 44,
241
+ outline: 'none',
242
+ boxSizing: 'border-box'
243
+ }
244
+ });
245
+ export default TabsDropdown;
@@ -72,8 +72,10 @@ const selectContainerStyles = _ref3 => {
72
72
  borderRadius,
73
73
  paddingHorizontal: paddingHorizontal - borderWidth,
74
74
  paddingVertical: paddingVertical - borderWidth,
75
- marginHorizontal,
76
- marginVertical
75
+ marginLeft: marginHorizontal,
76
+ marginRight: marginHorizontal,
77
+ marginTop: marginVertical,
78
+ marginBottom: marginVertical
77
79
  };
78
80
  };
79
81
 
@@ -0,0 +1,8 @@
1
+ export default {
2
+ en: {
3
+ selectTab: 'Select tab'
4
+ },
5
+ fr: {
6
+ selectTab: 'Sélectionner un onglet'
7
+ }
8
+ };
@@ -6,15 +6,20 @@ import useViewportListener from './useViewportListener';
6
6
 
7
7
  /**
8
8
  * Provides an up-to-date viewport value from system-constants, available via the `useViewport` hook
9
+ *
10
+ * @param {React.ReactNode} children - Child components that will have access to viewport context
11
+ * @param {string} [defaultViewport] - Default viewport to use during server-side rendering.
12
+ * Must be one of the viewport keys from system-constants. If not provided, defaults to the smallest viewport.
9
13
  */
10
14
  import { jsx as _jsx } from "react/jsx-runtime";
11
15
  const ViewportProvider = _ref => {
12
16
  let {
13
- children
17
+ children,
18
+ defaultViewport
14
19
  } = _ref;
15
20
  // Default to the smallest viewport for mobile-first SSR. On client side, this is updated
16
21
  // by useViewportListener in a layout effect before anything is shown to the user.
17
- const [viewport, setViewport] = React.useState(viewports.keys[0]);
22
+ const [viewport, setViewport] = React.useState(defaultViewport || viewports.keys[0]);
18
23
  useViewportListener(setViewport);
19
24
  return /*#__PURE__*/_jsx(ViewportContext.Provider, {
20
25
  value: viewport,
@@ -22,6 +27,7 @@ const ViewportProvider = _ref => {
22
27
  });
23
28
  };
24
29
  ViewportProvider.propTypes = {
25
- children: PropTypes.node.isRequired
30
+ children: PropTypes.node.isRequired,
31
+ defaultViewport: PropTypes.oneOf(viewports.keys)
26
32
  };
27
33
  export default ViewportProvider;
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.12.0",
15
+ "@telus-uds/system-theme-tokens": "^4.13.0",
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.13.0",
87
+ "version": "3.14.1",
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.12.0",
15
+ "@telus-uds/system-theme-tokens": "^4.13.0",
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.13.0",
87
+ "version": "3.14.1",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { PortalProvider } from '@gorhom/portal'
4
+ import { viewports } from '@telus-uds/system-constants'
4
5
  import A11yInfoProvider from '../A11yInfoProvider'
5
6
  import ViewportProvider from '../ViewportProvider'
6
7
  import ThemeProvider from '../ThemeProvider'
@@ -10,7 +11,7 @@ import { HydrationProvider } from './HydrationContext'
10
11
  const BaseProvider = React.forwardRef(({ defaultTheme, children, themeOptions }, _) => (
11
12
  <HydrationProvider>
12
13
  <A11yInfoProvider>
13
- <ViewportProvider>
14
+ <ViewportProvider defaultViewport={themeOptions?.defaultViewport}>
14
15
  <ThemeProvider defaultTheme={defaultTheme} themeOptions={themeOptions}>
15
16
  <PortalProvider>{children}</PortalProvider>
16
17
  </ThemeProvider>
@@ -26,7 +27,8 @@ BaseProvider.propTypes = {
26
27
  defaultTheme: ThemeProvider.propTypes?.defaultTheme,
27
28
  themeOptions: PropTypes.shape({
28
29
  forceAbsoluteFontSizing: PropTypes.bool,
29
- forceZIndex: PropTypes.bool
30
+ forceZIndex: PropTypes.bool,
31
+ defaultViewport: PropTypes.oneOf(viewports.keys)
30
32
  })
31
33
  }
32
34
 
package/src/Card/Card.jsx CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  responsiveProps,
18
18
  hrefAttrsProp
19
19
  } from '../utils/props'
20
- import CardBase from './CardBase'
20
+ import CardBase, { selectStyles } from './CardBase'
21
21
  import PressableCardBase from './PressableCardBase'
22
22
  import CheckboxButton from '../Checkbox/CheckboxButton'
23
23
  import RadioButton from '../Radio/RadioButton'
@@ -186,10 +186,28 @@ const Card = React.forwardRef(
186
186
  let mediaIds
187
187
 
188
188
  if (enableMediaQueryStyleSheet) {
189
- const mediaQueryStyleSheet = createMediaQueryStyles(themeTokens)
189
+ const transformedThemeTokens = Object.entries(themeTokens).reduce(
190
+ (acc, [vp, viewportTokens]) => {
191
+ const tokensToTransform = selectionType
192
+ ? selectStyles({
193
+ ...viewportTokens,
194
+ paddingTop: 0,
195
+ paddingBottom: 0,
196
+ paddingLeft: 0,
197
+ paddingRight: 0
198
+ })
199
+ : selectStyles(viewportTokens)
200
+
201
+ acc[vp] = tokensToTransform
202
+ return acc
203
+ },
204
+ {}
205
+ )
206
+
207
+ const mediaQueryStyleSheet = createMediaQueryStyles(transformedThemeTokens)
190
208
 
191
209
  const { ids, styles } = StyleSheet.create({
192
- card: mediaQueryStyleSheet
210
+ card: { ...themeTokens[viewport], ...mediaQueryStyleSheet }
193
211
  })
194
212
  cardStyles = styles.card
195
213
  mediaIds = ids.card
@@ -336,6 +354,12 @@ Card.propTypes = {
336
354
  alt: PropTypes.string,
337
355
  resizeMode: responsiveProps.getTypeOptionallyByViewport(
338
356
  PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
357
+ ),
358
+ position: responsiveProps.getTypeOptionallyByViewport(
359
+ PropTypes.oneOf(['bottom', 'left', 'right', 'top'])
360
+ ),
361
+ align: responsiveProps.getTypeOptionallyByViewport(
362
+ PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
339
363
  )
340
364
  }),
341
365
  /**
@@ -1,15 +1,117 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, Platform, ImageBackground, StyleSheet } from 'react-native'
3
+ import { View, Platform, ImageBackground, Image, StyleSheet } from 'react-native'
4
4
 
5
5
  import { applyShadowToken } from '../ThemeProvider'
6
6
  import { getTokensPropType, responsiveProps, useResponsiveProp, formatImageSource } from '../utils'
7
7
  import { a11yProps, viewProps, selectSystemProps } from '../utils/props'
8
+ import backgroundImageStylesMap from './backgroundImageStylesMap'
8
9
 
9
10
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
10
11
 
12
+ const setBackgroundImage = ({
13
+ src,
14
+ alt,
15
+ backgroundImageResizeMode,
16
+ backgroundImagePosition,
17
+ backgroundImageAlign,
18
+ content,
19
+ cardStyle
20
+ }) => {
21
+ const borderRadius = cardStyle?.borderRadius || 0
22
+ const borderWidth = cardStyle?.borderWidth || 0
23
+ const adjustedBorderRadius = Math.max(0, borderRadius - borderWidth)
24
+
25
+ // For contain mode with position and align, use CSS background properties for web
26
+ if (backgroundImageResizeMode === 'contain' && backgroundImagePosition && backgroundImageAlign) {
27
+ const positionKey = `${backgroundImagePosition}-${backgroundImageAlign}`
28
+
29
+ if (Platform.OS === 'web') {
30
+ // Create background position based on position and align
31
+ let backgroundPosition
32
+
33
+ switch (positionKey) {
34
+ case 'top-start':
35
+ backgroundPosition = 'left top'
36
+ break
37
+ case 'top-center':
38
+ backgroundPosition = 'center top'
39
+ break
40
+ case 'top-end':
41
+ backgroundPosition = 'right top'
42
+ break
43
+ case 'bottom-start':
44
+ backgroundPosition = 'left bottom'
45
+ break
46
+ case 'bottom-center':
47
+ backgroundPosition = 'center bottom'
48
+ break
49
+ case 'bottom-end':
50
+ backgroundPosition = 'right bottom'
51
+ break
52
+ case 'left-center':
53
+ backgroundPosition = 'left center'
54
+ break
55
+ case 'right-center':
56
+ backgroundPosition = 'right center'
57
+ break
58
+ default:
59
+ backgroundPosition = 'center center'
60
+ }
61
+
62
+ const backgroundImageStyle = {
63
+ backgroundImage: `url(${src})`,
64
+ backgroundSize: 'contain',
65
+ backgroundRepeat: 'no-repeat',
66
+ backgroundPosition,
67
+ borderRadius: adjustedBorderRadius
68
+ }
69
+
70
+ return (
71
+ <View
72
+ style={[staticStyles.imageBackground, backgroundImageStyle]}
73
+ role="img"
74
+ aria-label={alt}
75
+ >
76
+ {content}
77
+ </View>
78
+ )
79
+ }
80
+ // For React Native, apply positioning styles with full dimensions
81
+ const positionStyles = backgroundImageStylesMap[positionKey] || {}
82
+
83
+ return (
84
+ <View style={[staticStyles.containContainer, { borderRadius: adjustedBorderRadius }]}>
85
+ <Image
86
+ source={src}
87
+ resizeMode={backgroundImageResizeMode}
88
+ style={[staticStyles.containImage, positionStyles]}
89
+ accessible={true}
90
+ accessibilityLabel={alt}
91
+ accessibilityIgnoresInvertColors={true}
92
+ />
93
+ <View style={staticStyles.contentOverlay}>{content}</View>
94
+ </View>
95
+ )
96
+ }
97
+
98
+ // Use ImageBackground for all other resize modes and React Native
99
+ return (
100
+ <ImageBackground
101
+ source={src}
102
+ imageStyle={{ borderRadius: adjustedBorderRadius }}
103
+ resizeMode={backgroundImageResizeMode}
104
+ style={staticStyles.imageBackground}
105
+ accessible={true}
106
+ accessibilityLabel={alt}
107
+ >
108
+ {content}
109
+ </ImageBackground>
110
+ )
111
+ }
112
+
11
113
  // Ensure explicit selection of tokens
12
- const selectStyles = ({
114
+ export const selectStyles = ({
13
115
  flex,
14
116
  backgroundColor,
15
117
  borderColor,
@@ -64,34 +166,72 @@ const CardBase = React.forwardRef(
64
166
  const cardStyle = selectStyles(typeof tokens === 'function' ? tokens() : tokens)
65
167
  const props = selectProps(rest)
66
168
 
67
- const { src = '', alt = '', resizeMode = '' } = backgroundImage || {}
169
+ let content = children
170
+
171
+ const { src = '', alt = '', resizeMode = '', position = '', align = '' } = backgroundImage || {}
68
172
  const backgroundImageResizeMode = useResponsiveProp(resizeMode, 'cover')
173
+ const backgroundImagePosition = useResponsiveProp(position)
174
+ const backgroundImageAlign = useResponsiveProp(align)
69
175
  const imageSourceViewport = formatImageSource(useResponsiveProp(src))
70
176
 
177
+ if (backgroundImage && src) {
178
+ // When there's a background image, separate the padding from the container style
179
+ // so the image can fill the entire container without padding interference
180
+ const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = cardStyle
181
+
182
+ // Only create padding wrapper if there's actually padding defined
183
+ const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
184
+ const paddedContent = hasPadding ? (
185
+ <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
186
+ ) : (
187
+ children
188
+ )
189
+
190
+ content = setBackgroundImage({
191
+ src: imageSourceViewport,
192
+ alt,
193
+ backgroundImageResizeMode,
194
+ backgroundImagePosition,
195
+ backgroundImageAlign,
196
+ content: paddedContent,
197
+ cardStyle: containerStyle
198
+ })
199
+
200
+ return (
201
+ <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
202
+ {content}
203
+ </View>
204
+ )
205
+ }
206
+
71
207
  return (
72
208
  <View style={cardStyle} dataSet={dataSet} ref={ref} {...props}>
73
- {src ? (
74
- <ImageBackground
75
- source={imageSourceViewport}
76
- imageStyle={{ borderRadius: cardStyle?.borderRadius - cardStyle?.borderWidth }}
77
- resizeMode={backgroundImageResizeMode}
78
- style={styles.imageBackground}
79
- accessible={true}
80
- accessibilityLabel={alt}
81
- >
82
- {children}
83
- </ImageBackground>
84
- ) : (
85
- children
86
- )}
209
+ {content}
87
210
  </View>
88
211
  )
89
212
  }
90
213
  )
91
214
  CardBase.displayName = 'CardBase'
92
215
 
93
- const styles = StyleSheet.create({
94
- imageBackground: { width: '100%', height: '100%' }
216
+ const staticStyles = StyleSheet.create({
217
+ imageBackground: { width: '100%', height: '100%' },
218
+ contentOverlay: {
219
+ position: 'relative',
220
+ width: '100%',
221
+ height: '100%',
222
+ zIndex: 1
223
+ },
224
+ containContainer: {
225
+ width: '100%',
226
+ height: '100%',
227
+ overflow: 'hidden',
228
+ position: 'relative'
229
+ },
230
+ containImage: {
231
+ position: 'absolute',
232
+ width: '100%',
233
+ height: '100%'
234
+ }
95
235
  })
96
236
 
97
237
  CardBase.propTypes = {
@@ -108,6 +248,12 @@ CardBase.propTypes = {
108
248
  alt: PropTypes.string,
109
249
  resizeMode: responsiveProps.getTypeOptionallyByViewport(
110
250
  PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center'])
251
+ ),
252
+ position: responsiveProps.getTypeOptionallyByViewport(
253
+ PropTypes.oneOf(['bottom', 'left', 'right', 'top'])
254
+ ),
255
+ align: responsiveProps.getTypeOptionallyByViewport(
256
+ PropTypes.oneOf(['start', 'end', 'center', 'stretch'])
111
257
  )
112
258
  })
113
259
  }