@telus-uds/components-base 3.26.0 → 3.28.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 (75) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/lib/cjs/Card/Card.js +34 -13
  3. package/lib/cjs/Card/CardBase.js +90 -14
  4. package/lib/cjs/Card/PressableCardBase.js +147 -8
  5. package/lib/cjs/Carousel/Carousel.js +105 -50
  6. package/lib/cjs/Carousel/CarouselContext.js +10 -4
  7. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +11 -7
  8. package/lib/cjs/Carousel/Constants.js +11 -2
  9. package/lib/cjs/Checkbox/Checkbox.js +43 -13
  10. package/lib/cjs/ExpandCollapse/Control.js +5 -1
  11. package/lib/cjs/ExpandCollapse/ExpandCollapse.js +17 -8
  12. package/lib/cjs/ExpandCollapse/Panel.js +7 -2
  13. package/lib/cjs/IconButton/IconButton.js +10 -5
  14. package/lib/cjs/List/List.js +24 -9
  15. package/lib/cjs/List/ListItem.js +18 -1
  16. package/lib/cjs/List/ListItemBase.js +27 -8
  17. package/lib/cjs/List/ListItemMark.js +33 -62
  18. package/lib/cjs/List/PressableListItemBase.js +1 -0
  19. package/lib/cjs/Modal/Modal.js +21 -11
  20. package/lib/cjs/Progress/Progress.js +19 -5
  21. package/lib/cjs/Progress/ProgressBar.js +22 -4
  22. package/lib/cjs/Progress/ProgressContext.js +11 -0
  23. package/lib/cjs/SideNav/Item.js +3 -3
  24. package/lib/cjs/SideNav/ItemsGroup.js +46 -19
  25. package/lib/cjs/SideNav/SideNav.js +29 -13
  26. package/lib/esm/Card/Card.js +34 -13
  27. package/lib/esm/Card/CardBase.js +90 -14
  28. package/lib/esm/Card/PressableCardBase.js +148 -9
  29. package/lib/esm/Carousel/Carousel.js +106 -51
  30. package/lib/esm/Carousel/CarouselContext.js +10 -4
  31. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +11 -7
  32. package/lib/esm/Carousel/Constants.js +10 -1
  33. package/lib/esm/Checkbox/Checkbox.js +43 -13
  34. package/lib/esm/ExpandCollapse/Control.js +5 -1
  35. package/lib/esm/ExpandCollapse/ExpandCollapse.js +17 -8
  36. package/lib/esm/ExpandCollapse/Panel.js +7 -2
  37. package/lib/esm/IconButton/IconButton.js +10 -5
  38. package/lib/esm/List/List.js +24 -9
  39. package/lib/esm/List/ListItem.js +19 -2
  40. package/lib/esm/List/ListItemBase.js +27 -8
  41. package/lib/esm/List/ListItemMark.js +33 -62
  42. package/lib/esm/List/PressableListItemBase.js +1 -0
  43. package/lib/esm/Modal/Modal.js +21 -11
  44. package/lib/esm/Progress/Progress.js +19 -5
  45. package/lib/esm/Progress/ProgressBar.js +22 -4
  46. package/lib/esm/Progress/ProgressContext.js +5 -0
  47. package/lib/esm/SideNav/Item.js +3 -3
  48. package/lib/esm/SideNav/ItemsGroup.js +45 -20
  49. package/lib/esm/SideNav/SideNav.js +29 -13
  50. package/lib/package.json +2 -2
  51. package/package.json +2 -2
  52. package/src/Card/Card.jsx +29 -7
  53. package/src/Card/CardBase.jsx +97 -11
  54. package/src/Card/PressableCardBase.jsx +135 -9
  55. package/src/Carousel/Carousel.jsx +119 -64
  56. package/src/Carousel/CarouselContext.jsx +12 -4
  57. package/src/Carousel/CarouselItem/CarouselItem.jsx +10 -6
  58. package/src/Carousel/Constants.js +10 -0
  59. package/src/Checkbox/Checkbox.jsx +29 -7
  60. package/src/ExpandCollapse/Control.jsx +1 -1
  61. package/src/ExpandCollapse/ExpandCollapse.jsx +9 -8
  62. package/src/ExpandCollapse/Panel.jsx +10 -2
  63. package/src/IconButton/IconButton.jsx +40 -28
  64. package/src/List/List.jsx +33 -9
  65. package/src/List/ListItem.jsx +33 -11
  66. package/src/List/ListItemBase.jsx +33 -9
  67. package/src/List/ListItemMark.jsx +32 -53
  68. package/src/List/PressableListItemBase.jsx +1 -0
  69. package/src/Modal/Modal.jsx +23 -11
  70. package/src/Progress/Progress.jsx +18 -7
  71. package/src/Progress/ProgressBar.jsx +19 -14
  72. package/src/Progress/ProgressContext.js +5 -0
  73. package/src/SideNav/Item.jsx +3 -3
  74. package/src/SideNav/ItemsGroup.jsx +36 -16
  75. package/src/SideNav/SideNav.jsx +22 -8
@@ -4,6 +4,19 @@ import ItemContent from './ItemContent';
4
4
  import ExpandCollapse from '../ExpandCollapse';
5
5
  import { getTokensPropType, variantProp, componentPropType, selectTokens } from '../utils';
6
6
  import { useThemeTokensCallback } from '../ThemeProvider';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const selectPanelTokens = _ref => {
9
+ let {
10
+ borderWidth,
11
+ borderColor,
12
+ backgroundColor
13
+ } = _ref;
14
+ return {
15
+ contentPanelBackgroundColor: backgroundColor,
16
+ borderBottomWidth: borderWidth,
17
+ borderColor
18
+ };
19
+ };
7
20
 
8
21
  /**
9
22
  Expandable content areas for use within `SideNav`.
@@ -15,8 +28,7 @@ import { useThemeTokensCallback } from '../ThemeProvider';
15
28
  ## Usage Criteria
16
29
  - Use `SideNav.ItemsGroup` with large pages that have multiple sections
17
30
  */
18
- import { jsx as _jsx } from "react/jsx-runtime";
19
- const ItemsGroup = /*#__PURE__*/React.forwardRef((_ref, ref) => {
31
+ const ItemsGroup = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
20
32
  let {
21
33
  children,
22
34
  label,
@@ -27,7 +39,7 @@ const ItemsGroup = /*#__PURE__*/React.forwardRef((_ref, ref) => {
27
39
  tokens,
28
40
  itemTokens,
29
41
  onToggle
30
- } = _ref;
42
+ } = _ref2;
31
43
  // A SideNav control uses the same style and theme as SideNavItem, with a 'parent' variant,
32
44
  // plus control-specific tokens from the SideNavItemsGroup theme (e.g. open/close icon, etc).
33
45
  const getAppearance = appearance => ({
@@ -40,28 +52,27 @@ const ItemsGroup = /*#__PURE__*/React.forwardRef((_ref, ref) => {
40
52
  type: 'parent'
41
53
  });
42
54
  const getGroupTokens = useThemeTokensCallback('SideNavItemsGroup', tokens, variant);
43
- const getPanelTokens = appearance => {
44
- const {
45
- panelBorderColor,
46
- ...itemsGroupTokens
47
- } = getGroupTokens(getAppearance(appearance));
48
- const groupTokens = {
49
- ...itemsGroupTokens,
50
- borderWidth: 0,
51
- marginBottom: 0
52
- };
53
- return selectTokens('ExpandCollapsePanel', groupTokens);
54
- };
55
55
  const getItemTokens = useThemeTokensCallback('SideNavItem', itemTokens, variant);
56
- const getControlTokens = appearance => selectTokens('ExpandCollapseControl', {
57
- ...getItemTokens(getItemAppearance(appearance)),
58
- // main style from SideNavItem
59
- ...getGroupTokens(getAppearance(appearance)) // control-specific tokens like icon etc
56
+ const getPanelTokens = appearance => selectTokens('ExpandCollapsePanel', {
57
+ ...staticTokens.panel,
58
+ ...getGroupTokens(getAppearance(appearance)),
59
+ ...selectPanelTokens(getItemTokens(getItemAppearance(appearance)))
60
+ });
61
+ const getControlTokens = appearance => ({
62
+ ...selectTokens('ExpandCollapseControl', {
63
+ ...getItemTokens(getItemAppearance(appearance)),
64
+ // main style from SideNavItem
65
+ ...getGroupTokens(getAppearance(appearance)) // control-specific tokens like icon etc,
66
+ }),
67
+ ...staticTokens.control
60
68
  });
61
69
  const controlContent = controlState => {
62
70
  const currentItemTokens = getItemTokens(getItemAppearance(controlState));
63
71
  return /*#__PURE__*/_jsx(ItemContent, {
64
- tokens: currentItemTokens,
72
+ tokens: {
73
+ ...currentItemTokens,
74
+ ...staticTokens.content
75
+ },
65
76
  children: label
66
77
  });
67
78
  };
@@ -77,9 +88,23 @@ const ItemsGroup = /*#__PURE__*/React.forwardRef((_ref, ref) => {
77
88
  active: isActive
78
89
  } // ExpandCollapse.Panel handles expanded state
79
90
  ,
91
+ disableMobileScrollBuffer: true,
80
92
  children: children
81
93
  });
82
94
  });
95
+ const staticTokens = {
96
+ panel: {
97
+ borderWidth: 0,
98
+ marginBottom: 0
99
+ },
100
+ control: {
101
+ borderWidth: 0,
102
+ textLine: null
103
+ },
104
+ content: {
105
+ accentWidth: 0
106
+ }
107
+ };
83
108
  ItemsGroup.displayName = 'ItemsGroup';
84
109
  ItemsGroup.propTypes = {
85
110
  /**
@@ -6,13 +6,28 @@ import ItemsGroup from './ItemsGroup';
6
6
  import { useThemeTokens } from '../ThemeProvider';
7
7
  import { a11yProps, componentPropType, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils';
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
- function selectBorderStyles(tokens) {
9
+ const selectContainerTokens = _ref => {
10
+ let {
11
+ borderWidth,
12
+ borderStyle,
13
+ borderColor
14
+ } = _ref;
10
15
  return {
11
- borderBottomWidth: tokens.borderWidth,
12
- borderBottomStyle: tokens.borderStyle,
13
- borderBottomColor: tokens.borderColor
16
+ borderTopWidth: borderWidth,
17
+ borderStyle,
18
+ borderColor
14
19
  };
15
- }
20
+ };
21
+ const selectItemTokens = function () {
22
+ let tokens = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
23
+ let isLastItem = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
24
+ return {
25
+ ...tokens,
26
+ ...(isLastItem ? {
27
+ borderWidth: 0
28
+ } : {})
29
+ };
30
+ };
16
31
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
17
32
 
18
33
  /**
@@ -20,7 +35,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
20
35
  - Use in conjunction with a large amount of educational / informational content
21
36
  - Allow the user to navigate between options frequently and efficiently
22
37
  */
23
- const SideNav = /*#__PURE__*/React.forwardRef((_ref, ref) => {
38
+ const SideNav = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
24
39
  let {
25
40
  children,
26
41
  variant = {},
@@ -29,7 +44,7 @@ const SideNav = /*#__PURE__*/React.forwardRef((_ref, ref) => {
29
44
  itemTokens,
30
45
  groupTokens,
31
46
  ...rest
32
- } = _ref;
47
+ } = _ref2;
33
48
  const themeTokens = useThemeTokens('SideNav', tokens, variant);
34
49
  const [active, setActive] = React.useState({
35
50
  groupId: undefined,
@@ -47,14 +62,15 @@ const SideNav = /*#__PURE__*/React.forwardRef((_ref, ref) => {
47
62
  return /*#__PURE__*/_jsx(ExpandCollapse, {
48
63
  ref: ref,
49
64
  maxOpen: accordion ? 1 : null,
50
- style: selectBorderStyles(themeTokens),
65
+ tokens: selectContainerTokens(themeTokens),
51
66
  ...selectProps(rest),
52
- children: _ref2 => {
67
+ children: _ref3 => {
53
68
  let {
54
69
  openIds,
55
70
  onToggle
56
- } = _ref2;
57
- const renderItem = (item, index, groupId) => {
71
+ } = _ref3;
72
+ const renderItem = function (item, index, groupId) {
73
+ let isLastItem = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
58
74
  const {
59
75
  itemId = `item-${index}`,
60
76
  onPress
@@ -72,7 +88,7 @@ const SideNav = /*#__PURE__*/React.forwardRef((_ref, ref) => {
72
88
  ...variant,
73
89
  type: 'child'
74
90
  } : variant,
75
- tokens: itemTokens,
91
+ tokens: selectItemTokens(itemTokens, isLastItem),
76
92
  isActive: isItemActive(itemId, groupId),
77
93
  onPress: handlePress
78
94
  });
@@ -96,7 +112,7 @@ const SideNav = /*#__PURE__*/React.forwardRef((_ref, ref) => {
96
112
  openGroups: openIds,
97
113
  isActive: isGroupActive,
98
114
  onToggle: handleToggle
99
- }, React.Children.map(child.props.children, (item, itemIndex) => renderItem(item, itemIndex, groupId)));
115
+ }, React.Children.map(child.props.children, (item, itemIndex) => renderItem(item, itemIndex, groupId, itemIndex === child.props.children.length - 1)));
100
116
  }
101
117
  return null;
102
118
  });
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.18.0",
15
+ "@telus-uds/system-theme-tokens": "^4.20.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.26.0",
87
+ "version": "3.28.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.18.0",
15
+ "@telus-uds/system-theme-tokens": "^4.20.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.26.0",
87
+ "version": "3.28.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/src/Card/Card.jsx CHANGED
@@ -30,10 +30,10 @@ const SelectionType = {
30
30
  None: undefined
31
31
  }
32
32
 
33
- const selectInputStyle = ({ paddingTop, paddingRight }) => ({
33
+ const selectInputStyle = ({ paddingTop, paddingRight, paddingLeft }, { inputPositionProp }) => ({
34
34
  position: 'absolute',
35
35
  top: paddingTop,
36
- right: paddingRight
36
+ ...(inputPositionProp === 'left' ? { left: paddingLeft } : { right: paddingRight })
37
37
  })
38
38
 
39
39
  const getInputProps = ({
@@ -108,7 +108,18 @@ const getInputProps = ({
108
108
  */
109
109
  const Card = React.forwardRef(
110
110
  (
111
- { children, tokens, variant, dataSet, onPress, id, interactiveCard, backgroundImage, ...rest },
111
+ {
112
+ children,
113
+ tokens,
114
+ variant,
115
+ dataSet,
116
+ onPress,
117
+ id,
118
+ interactiveCard,
119
+ backgroundImage,
120
+ testID,
121
+ ...rest
122
+ },
112
123
  ref
113
124
  ) => {
114
125
  const viewport = useViewport()
@@ -216,6 +227,11 @@ const Card = React.forwardRef(
216
227
  }
217
228
 
218
229
  const renderInputPerSelectionType = (props) => {
230
+ const containerStyle = selectInputStyle(getThemeTokens(), {
231
+ inputPositionProp: interactiveCard?.inputPosition
232
+ })
233
+ const inputTestID = testID && `${testID}-selection-input`
234
+
219
235
  if (!isControl) {
220
236
  return null
221
237
  }
@@ -223,13 +239,13 @@ const Card = React.forwardRef(
223
239
  switch (selectionType) {
224
240
  case SelectionType.Checkbox:
225
241
  return (
226
- <View style={selectInputStyle(getThemeTokens())}>
242
+ <View style={containerStyle} testID={inputTestID}>
227
243
  <CheckboxButton {...props} tokens={{ ...checkboxTokens, ...props?.tokens }} />
228
244
  </View>
229
245
  )
230
246
  case SelectionType.Radio:
231
247
  return (
232
- <View style={selectInputStyle(getThemeTokens())}>
248
+ <View style={containerStyle} testID={inputTestID}>
233
249
  <RadioButton {...props} tokens={{ ...radioTokens, ...props?.tokens }} />
234
250
  </View>
235
251
  )
@@ -245,6 +261,7 @@ const Card = React.forwardRef(
245
261
  tokens={interactiveCard?.body ? restOfTokens : cardStyles}
246
262
  backgroundImage={backgroundImage}
247
263
  dataSet={mediaIds && { media: mediaIds }}
264
+ testID={testID}
248
265
  {...selectProps(rest)}
249
266
  >
250
267
  {interactiveCard?.body && (
@@ -342,7 +359,8 @@ Card.propTypes = {
342
359
  selectionType: PropTypes.oneOf(Object.values(SelectionType)),
343
360
  variant: variantProp.propType,
344
361
  href: PropTypes.string,
345
- hrefAttrs: PropTypes.shape(hrefAttrsProp.types)
362
+ hrefAttrs: PropTypes.shape(hrefAttrsProp.types),
363
+ inputPosition: PropTypes.oneOf(['left', 'right'])
346
364
  }),
347
365
  /**
348
366
  * Apply background image to the card.
@@ -365,7 +383,11 @@ Card.propTypes = {
365
383
  /**
366
384
  * Data set for the card.
367
385
  */
368
- dataSet: PropTypes.object
386
+ dataSet: PropTypes.object,
387
+ /**
388
+ * Test ID used for e2e testing.
389
+ */
390
+ testID: PropTypes.string
369
391
  }
370
392
 
371
393
  export default Card
@@ -116,6 +116,44 @@ const setBackgroundImage = ({
116
116
  )
117
117
  }
118
118
 
119
+ const selectPaddedContentStyles = ({
120
+ paddingTop,
121
+ paddingBottom,
122
+ paddingLeft,
123
+ paddingRight,
124
+ borderWidth,
125
+ borderColor,
126
+ borderRadius,
127
+ hasInteractiveBorder
128
+ }) => ({
129
+ paddingTop,
130
+ paddingBottom,
131
+ paddingLeft,
132
+ paddingRight,
133
+ ...(hasInteractiveBorder
134
+ ? {
135
+ borderWidth,
136
+ borderColor,
137
+ borderRadius
138
+ }
139
+ : {})
140
+ })
141
+
142
+ const selectInteractiveOverlayStyles = ({ backgroundColor, borderRadius, borderWidth }) => {
143
+ const adjustedBorderRadius = Math.max(0, borderRadius - borderWidth)
144
+ return {
145
+ position: 'absolute',
146
+ top: 0,
147
+ left: 0,
148
+ right: 0,
149
+ bottom: 0,
150
+ backgroundColor,
151
+ borderRadius: adjustedBorderRadius,
152
+ pointerEvents: 'none',
153
+ zIndex: 1
154
+ }
155
+ }
156
+
119
157
  // Ensure explicit selection of tokens
120
158
  export const selectStyles = ({
121
159
  flex,
@@ -195,7 +233,13 @@ export const selectStyles = ({
195
233
  */
196
234
  const CardBase = React.forwardRef(
197
235
  ({ children, tokens, dataSet, backgroundImage, fullBleedContent, cardState, ...rest }, ref) => {
198
- const cardStyle = selectStyles(typeof tokens === 'function' ? tokens(cardState) : tokens)
236
+ const resolvedTokens = typeof tokens === 'function' ? tokens(cardState) : tokens
237
+ const tokensToUse =
238
+ backgroundImage && backgroundImage.src
239
+ ? { ...resolvedTokens, gradient: undefined, backgroundGradient: undefined }
240
+ : resolvedTokens
241
+
242
+ const cardStyle = selectStyles(tokensToUse)
199
243
  const props = selectProps(rest)
200
244
 
201
245
  let content = children
@@ -214,13 +258,55 @@ const CardBase = React.forwardRef(
214
258
  const fullBleedPosition = useResponsiveProp(fullBleedContentPosition, 'bottom')
215
259
 
216
260
  if (backgroundImage && src) {
217
- const { paddingTop, paddingBottom, paddingLeft, paddingRight, ...containerStyle } = cardStyle
261
+ const {
262
+ paddingTop,
263
+ paddingBottom,
264
+ paddingLeft,
265
+ paddingRight,
266
+ borderWidth,
267
+ borderColor,
268
+ borderRadius,
269
+ backgroundColor,
270
+ ...containerStyle
271
+ } = cardStyle
218
272
 
219
273
  const hasPadding = paddingTop || paddingBottom || paddingLeft || paddingRight
220
- const paddedContent = hasPadding ? (
221
- <View style={{ paddingTop, paddingBottom, paddingLeft, paddingRight }}>{children}</View>
222
- ) : (
223
- children
274
+ const hasInteractiveBorder = borderWidth && borderWidth > 0
275
+ const hasInteractiveOverlay = isOverlayColor(backgroundColor)
276
+
277
+ const paddedContent =
278
+ hasPadding || hasInteractiveBorder ? (
279
+ <View
280
+ style={selectPaddedContentStyles({
281
+ paddingTop,
282
+ paddingBottom,
283
+ paddingLeft,
284
+ paddingRight,
285
+ borderWidth,
286
+ borderColor,
287
+ borderRadius,
288
+ hasInteractiveBorder
289
+ })}
290
+ >
291
+ {children}
292
+ </View>
293
+ ) : (
294
+ children
295
+ )
296
+
297
+ const contentWithOverlay = (
298
+ <>
299
+ {hasInteractiveOverlay && Platform.OS === 'web' && (
300
+ <View
301
+ style={selectInteractiveOverlayStyles({
302
+ backgroundColor,
303
+ borderRadius,
304
+ borderWidth
305
+ })}
306
+ />
307
+ )}
308
+ <View style={staticStyles.contentOverlay}>{paddedContent}</View>
309
+ </>
224
310
  )
225
311
 
226
312
  content = setBackgroundImage({
@@ -229,12 +315,12 @@ const CardBase = React.forwardRef(
229
315
  backgroundImageResizeMode,
230
316
  backgroundImagePosition,
231
317
  backgroundImageAlign,
232
- content: paddedContent,
233
- cardStyle: containerStyle
318
+ content: contentWithOverlay,
319
+ cardStyle: { ...containerStyle, borderRadius }
234
320
  })
235
321
 
236
322
  return (
237
- <View style={containerStyle} dataSet={dataSet} ref={ref} {...props}>
323
+ <View style={{ ...containerStyle, borderRadius }} dataSet={dataSet} ref={ref} {...props}>
238
324
  {content}
239
325
  </View>
240
326
  )
@@ -313,8 +399,8 @@ const staticStyles = StyleSheet.create({
313
399
  contentOverlay: {
314
400
  position: 'relative',
315
401
  width: '100%',
316
- height: '100%',
317
- zIndex: 1
402
+ minHeight: '100%',
403
+ zIndex: 2
318
404
  },
319
405
  containContainer: {
320
406
  width: '100%',
@@ -1,7 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
  import { Pressable, Platform, View, StyleSheet } from 'react-native'
4
-
5
4
  import { useViewport } from '../ViewportProvider'
6
5
  import { applyOuterBorder, validateThemeTokens } from '../ThemeProvider'
7
6
  import {
@@ -26,6 +25,71 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([
26
25
  viewProps
27
26
  ])
28
27
 
28
+ const selectFocusOverlayContainerStyles = (tokens) => {
29
+ const { flex, minWidth, marginTop, marginBottom, marginLeft, marginRight } = tokens
30
+
31
+ return {
32
+ flex: flex || 1,
33
+ minWidth: minWidth || 0,
34
+ marginTop,
35
+ marginBottom,
36
+ marginLeft,
37
+ marginRight
38
+ }
39
+ }
40
+
41
+ const selectFocusBorderStyles = (tokens) => {
42
+ const { borderWidth, borderColor, borderRadius } = tokens
43
+
44
+ return {
45
+ borderWidth,
46
+ borderColor,
47
+ borderRadius: borderRadius || 0
48
+ }
49
+ }
50
+
51
+ const FocusBorderOverlay = ({ tokens, pressableState, children }) => {
52
+ const { borderWidth = 0 } = tokens
53
+ const showFocusBorder = pressableState.focused && borderWidth > 0
54
+
55
+ return (
56
+ <View
57
+ style={[
58
+ staticStyles.focusOverlayContainer,
59
+ selectFocusOverlayContainerStyles(tokens),
60
+ Platform.OS === 'web' && staticStyles.webOutlineNone
61
+ ]}
62
+ >
63
+ {children}
64
+ {showFocusBorder && (
65
+ <View
66
+ style={[staticStyles.focusBorder, selectFocusBorderStyles(tokens)]}
67
+ accessible={false}
68
+ importantForAccessibility="no"
69
+ />
70
+ )}
71
+ </View>
72
+ )
73
+ }
74
+
75
+ FocusBorderOverlay.propTypes = {
76
+ tokens: PropTypes.shape({
77
+ flex: PropTypes.number,
78
+ minWidth: PropTypes.number,
79
+ marginTop: PropTypes.number,
80
+ marginBottom: PropTypes.number,
81
+ marginLeft: PropTypes.number,
82
+ marginRight: PropTypes.number,
83
+ borderColor: PropTypes.string,
84
+ borderWidth: PropTypes.number,
85
+ borderRadius: PropTypes.number
86
+ }).isRequired,
87
+ pressableState: PropTypes.shape({
88
+ focused: PropTypes.bool
89
+ }).isRequired,
90
+ children: PropTypes.node
91
+ }
92
+
29
93
  const tokenKeys = [
30
94
  'flex',
31
95
  'backgroundColor',
@@ -103,11 +167,53 @@ const PressableCardBase = React.forwardRef(
103
167
  'PressableCard'
104
168
  )
105
169
 
106
- const getCardTokens = (pressableState) => selectTokens('Card', getTokens(pressableState))
170
+ const getCardTokens = (pressableState) => {
171
+ const allTokens = getTokens(pressableState)
172
+ const cardTokens = selectTokens('Card', allTokens)
173
+
174
+ // Handle focus border transparency to avoid double borders
175
+ if (pressableState.focused && allTokens.borderWidth > 0) {
176
+ const result = {
177
+ ...cardTokens,
178
+ borderColor: 'transparent'
179
+ }
180
+
181
+ // Also handle backgroundImage for interactive states
182
+ if (backgroundImage) {
183
+ const { hovered, pressed, focused } = pressableState || {}
184
+ const isInteractiveState = hovered || pressed || focused
185
+
186
+ if (!isInteractiveState) {
187
+ const { backgroundColor, ...restTokens } = result
188
+ return restTokens
189
+ }
190
+ }
191
+
192
+ return result
193
+ }
194
+
195
+ // Handle backgroundImage when not in focus state
196
+ if (backgroundImage) {
197
+ const { hovered, pressed, focused } = pressableState || {}
198
+ const isInteractiveState = hovered || pressed || focused
199
+
200
+ if (!isInteractiveState) {
201
+ const { backgroundColor, ...restTokens } = cardTokens
202
+ return restTokens
203
+ }
204
+ }
205
+
206
+ return cardTokens
207
+ }
208
+
107
209
  const getOuterBorderStyle = (pressableState) => {
108
210
  const {
109
211
  flex,
110
212
  minWidth,
213
+ marginTop,
214
+ marginBottom,
215
+ marginLeft,
216
+ marginRight,
111
217
  outerBorderColor,
112
218
  outerBorderGap = 0,
113
219
  outerBorderWidth = 0,
@@ -116,6 +222,10 @@ const PressableCardBase = React.forwardRef(
116
222
  return {
117
223
  flex,
118
224
  minWidth: minWidth + outerBorderGap + outerBorderWidth,
225
+ marginTop,
226
+ marginBottom,
227
+ marginLeft,
228
+ marginRight,
119
229
  ...applyOuterBorder({ outerBorderColor, outerBorderGap, outerBorderWidth, borderRadius }),
120
230
  ...Platform.select({ web: { outline: 'none' } })
121
231
  }
@@ -194,13 +304,15 @@ const PressableCardBase = React.forwardRef(
194
304
  {...selectProps({ ...rest, accessibilityRole })}
195
305
  >
196
306
  {(pressableState) => (
197
- <CardBase
198
- tokens={getCardTokens(pressableState)}
199
- backgroundImage={backgroundImage}
200
- fullBleedContent={fullBleedContent}
201
- >
202
- {typeof children === 'function' ? children(getCardState(pressableState)) : children}
203
- </CardBase>
307
+ <FocusBorderOverlay tokens={getTokens(pressableState)} pressableState={pressableState}>
308
+ <CardBase
309
+ tokens={getCardTokens(pressableState)}
310
+ backgroundImage={backgroundImage}
311
+ fullBleedContent={fullBleedContent}
312
+ >
313
+ {typeof children === 'function' ? children(getCardState(pressableState)) : children}
314
+ </CardBase>
315
+ </FocusBorderOverlay>
204
316
  )}
205
317
  </Pressable>
206
318
  )
@@ -218,6 +330,20 @@ const staticStyles = StyleSheet.create({
218
330
  alignItems: 'stretch',
219
331
  justifyContent: 'flex-start',
220
332
  textDecorationLine: 'none'
333
+ },
334
+ focusOverlayContainer: {
335
+ position: 'relative'
336
+ },
337
+ webOutlineNone: {
338
+ outline: 'none'
339
+ },
340
+ focusBorder: {
341
+ position: 'absolute',
342
+ top: 0,
343
+ left: 0,
344
+ right: 0,
345
+ bottom: 0,
346
+ pointerEvents: 'none'
221
347
  }
222
348
  })
223
349