@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.
- package/CHANGELOG.md +10 -2
- package/lib/cjs/BaseProvider/index.js +4 -1
- package/lib/cjs/Card/Card.js +23 -4
- package/lib/cjs/Card/CardBase.js +170 -19
- package/lib/cjs/Card/PressableCardBase.js +19 -5
- package/lib/cjs/Card/backgroundImageStylesMap.js +197 -0
- package/lib/cjs/FlexGrid/FlexGrid.js +28 -12
- package/lib/cjs/Tabs/Tabs.js +34 -2
- package/lib/cjs/Tabs/TabsDropdown.js +252 -0
- package/lib/cjs/Tabs/TabsItem.js +4 -2
- package/lib/cjs/Tabs/dictionary.js +14 -0
- package/lib/cjs/ViewportProvider/ViewportProvider.js +9 -3
- package/lib/esm/BaseProvider/index.js +4 -1
- package/lib/esm/Card/Card.js +21 -4
- package/lib/esm/Card/CardBase.js +169 -19
- package/lib/esm/Card/PressableCardBase.js +19 -5
- package/lib/esm/Card/backgroundImageStylesMap.js +190 -0
- package/lib/esm/FlexGrid/FlexGrid.js +28 -12
- package/lib/esm/Tabs/Tabs.js +35 -3
- package/lib/esm/Tabs/TabsDropdown.js +245 -0
- package/lib/esm/Tabs/TabsItem.js +4 -2
- package/lib/esm/Tabs/dictionary.js +8 -0
- package/lib/esm/ViewportProvider/ViewportProvider.js +9 -3
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/BaseProvider/index.jsx +4 -2
- package/src/Card/Card.jsx +27 -3
- package/src/Card/CardBase.jsx +165 -19
- package/src/Card/PressableCardBase.jsx +31 -4
- package/src/Card/backgroundImageStylesMap.js +41 -0
- package/src/FlexGrid/FlexGrid.jsx +30 -13
- package/src/Tabs/Tabs.jsx +36 -2
- package/src/Tabs/TabsDropdown.jsx +265 -0
- package/src/Tabs/TabsItem.jsx +4 -2
- package/src/Tabs/dictionary.js +8 -0
- 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;
|
package/lib/esm/Tabs/TabsItem.js
CHANGED
|
@@ -72,8 +72,10 @@ const selectContainerStyles = _ref3 => {
|
|
|
72
72
|
borderRadius,
|
|
73
73
|
paddingHorizontal: paddingHorizontal - borderWidth,
|
|
74
74
|
paddingVertical: paddingVertical - borderWidth,
|
|
75
|
-
marginHorizontal,
|
|
76
|
-
|
|
75
|
+
marginLeft: marginHorizontal,
|
|
76
|
+
marginRight: marginHorizontal,
|
|
77
|
+
marginTop: marginVertical,
|
|
78
|
+
marginBottom: marginVertical
|
|
77
79
|
};
|
|
78
80
|
};
|
|
79
81
|
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
/**
|
package/src/Card/CardBase.jsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
}
|