@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
@@ -92,34 +92,46 @@ const selectInnerStyle = (
92
92
  height
93
93
  },
94
94
  password
95
- ) => ({
96
- // Inner borders animate with the icon and should be treated like a themable feature of the icon
97
- borderColor,
98
- borderRadius,
99
- borderWidth,
100
- borderTopLeftRadius,
101
- borderTopRightRadius,
102
- borderBottomLeftRadius,
103
- borderBottomRightRadius,
104
- borderTopWidth,
105
- borderRightWidth,
106
- borderBottomWidth,
107
- borderLeftWidth,
108
- padding: calculatePadding(padding, borderWidth),
109
- paddingLeft: calculatePadding(paddingLeft, borderLeftWidth),
110
- paddingRight: calculatePadding(paddingRight, borderRightWidth),
111
- paddingTop: calculatePadding(paddingTop, borderTopWidth),
112
- paddingBottom: calculatePadding(paddingBottom, borderBottomWidth),
113
- ...Platform.select({
114
- web: {
115
- pointerEvents: 'none',
116
- display: 'inline-flex',
117
- alignItems: 'center',
118
- justifyContent: 'center'
119
- }
120
- }),
121
- ...getPasswordDimensions(password, width, height)
122
- })
95
+ ) => {
96
+ const basePadding = calculatePadding(padding, borderWidth)
97
+
98
+ const calculateSpecificPadding = (specificPadding, specificBorderWidth) => {
99
+ const calculated = calculatePadding(
100
+ specificPadding ?? padding,
101
+ specificBorderWidth ?? borderWidth
102
+ )
103
+ return calculated !== basePadding && calculated !== undefined ? calculated : undefined
104
+ }
105
+
106
+ return {
107
+ // Inner borders animate with the icon and should be treated like a themable feature of the icon
108
+ borderColor,
109
+ borderRadius,
110
+ borderWidth,
111
+ borderTopLeftRadius,
112
+ borderTopRightRadius,
113
+ borderBottomLeftRadius,
114
+ borderBottomRightRadius,
115
+ borderTopWidth,
116
+ borderRightWidth,
117
+ borderBottomWidth,
118
+ borderLeftWidth,
119
+ padding: basePadding,
120
+ paddingLeft: calculateSpecificPadding(paddingLeft, borderLeftWidth),
121
+ paddingRight: calculateSpecificPadding(paddingRight, borderRightWidth),
122
+ paddingTop: calculateSpecificPadding(paddingTop, borderTopWidth),
123
+ paddingBottom: calculateSpecificPadding(paddingBottom, borderBottomWidth),
124
+ ...Platform.select({
125
+ web: {
126
+ pointerEvents: 'none',
127
+ display: 'inline-flex',
128
+ alignItems: 'center',
129
+ justifyContent: 'center'
130
+ }
131
+ }),
132
+ ...getPasswordDimensions(password, width, height)
133
+ }
134
+ }
123
135
 
124
136
  /**
125
137
  * A pressable themeless base component that handles pressable states and passes tokens
package/src/List/List.jsx CHANGED
@@ -5,10 +5,22 @@ import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps
5
5
 
6
6
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
7
7
 
8
+ const LIST_ITEM_TYPE = 'ListItem'
9
+ const LINKS_ITEM_TYPE = 'LinksItem'
10
+
8
11
  const isListItem = (element) => {
9
- const elementName = element?.type?.displayName || element?.type?.name
10
- // Match our own ListItem, and also, custom list items
11
- return Boolean(elementName.match(/Item/))
12
+ if (!element?.type) return false
13
+
14
+ if (element.type.__UDS_COMPONENT_TYPE__ === LIST_ITEM_TYPE) {
15
+ return true
16
+ }
17
+
18
+ const elementName = element.type.displayName || element.type.name || ''
19
+ return (
20
+ elementName === LIST_ITEM_TYPE ||
21
+ elementName.includes(LIST_ITEM_TYPE) ||
22
+ elementName.includes(LINKS_ITEM_TYPE)
23
+ )
12
24
  }
13
25
 
14
26
  /**
@@ -22,6 +34,7 @@ const List = React.forwardRef(
22
34
  showDivider,
23
35
  tokens,
24
36
  variant,
37
+ iconVerticalAlign,
25
38
  accessibilityRole = Platform.select({ web: 'list', default: undefined }),
26
39
  ...rest
27
40
  },
@@ -30,13 +43,19 @@ const List = React.forwardRef(
30
43
  const items = React.Children.map(children, (child, index) => {
31
44
  // Pass ListItem-specific props to children (by name so teams can add their own ListItems)
32
45
  if (isListItem(child)) {
33
- return React.cloneElement(child, {
34
- showDivider,
35
- isLastItem: index + 1 === React.Children.count(children),
46
+ const childProps = {
36
47
  tokens,
37
48
  variant,
38
- ...child.props
39
- })
49
+ ...child.props,
50
+ showDivider,
51
+ isLastItem: index + 1 === React.Children.count(children)
52
+ }
53
+
54
+ if (!child.props.iconVerticalAlign && iconVerticalAlign) {
55
+ childProps.iconVerticalAlign = iconVerticalAlign
56
+ }
57
+
58
+ return React.cloneElement(child, childProps)
40
59
  }
41
60
  return child
42
61
  })
@@ -76,7 +95,12 @@ List.propTypes = {
76
95
  /**
77
96
  * In case it is not the last item allow display divider
78
97
  */
79
- showDivider: PropTypes.bool
98
+ showDivider: PropTypes.bool,
99
+ /**
100
+ * The vertical alignment of the icon in ListItems.
101
+ * This prop is passed down to ListItem components and can be overridden in individual List.Item components.
102
+ */
103
+ iconVerticalAlign: PropTypes.oneOf(['top', 'center', 'bottom'])
80
104
  }
81
105
 
82
106
  export default List
@@ -1,25 +1,47 @@
1
1
  import React from 'react'
2
-
2
+ import PropTypes from 'prop-types'
3
3
  import ListItemBase from './ListItemBase'
4
4
  import { useThemeTokens } from '../ThemeProvider'
5
- import { variantProp } from '../utils'
5
+ import { getTokensPropType, variantProp } from '../utils'
6
6
 
7
7
  /**
8
8
  * ListItem is responsible for rendering icon or a bullet as side item
9
9
  */
10
- const ListItem = React.forwardRef(({ tokens, variant, children, title, ...listItemProps }, ref) => {
11
- const themeTokens = useThemeTokens('List', tokens, variant)
12
- return (
13
- <ListItemBase tokens={themeTokens} ref={ref} {...listItemProps} title={title}>
14
- {children}
15
- </ListItemBase>
16
- )
17
- })
10
+ const ListItem = React.forwardRef(
11
+ ({ tokens, variant, children, title, iconVerticalAlign = 'top', ...listItemProps }, ref) => {
12
+ const themeTokens = useThemeTokens('List', tokens, variant)
13
+ return (
14
+ <ListItemBase
15
+ tokens={themeTokens}
16
+ ref={ref}
17
+ {...listItemProps}
18
+ title={title}
19
+ iconVerticalAlign={iconVerticalAlign}
20
+ >
21
+ {children}
22
+ </ListItemBase>
23
+ )
24
+ }
25
+ )
18
26
  ListItem.displayName = 'ListItem'
19
27
 
20
28
  ListItem.propTypes = {
29
+ /** Theme tokens for styling */
30
+ tokens: getTokensPropType('List'),
31
+ /** Variant configuration for the component */
21
32
  variant: variantProp.propType,
22
- ...ListItemBase.propTypes
33
+ /** Content to be rendered within the list item */
34
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
35
+ /** Title of the list item */
36
+ title: PropTypes.node,
37
+ /** Controls the vertical alignment of the icon */
38
+ iconVerticalAlign: PropTypes.oneOf(['top', 'center', 'bottom']),
39
+ /** Icon to be displayed */
40
+ icon: PropTypes.elementType,
41
+ /** Color of the icon */
42
+ iconColor: PropTypes.string,
43
+ /** Size of the icon */
44
+ iconSize: PropTypes.number
23
45
  }
24
46
 
25
47
  export default ListItem
@@ -2,7 +2,6 @@ import React from 'react'
2
2
  import { View, Platform, StyleSheet } from 'react-native'
3
3
  import PropTypes from 'prop-types'
4
4
  import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
5
-
6
5
  import ListItemContent from './ListItemContent'
7
6
  import ListItemMark from './ListItemMark'
8
7
  import Typography from '../Typography'
@@ -10,6 +9,14 @@ import { useThemeTokens } from '../ThemeProvider'
10
9
 
11
10
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
12
11
 
12
+ const VERTICAL_CENTERING_DIVISOR = 2
13
+
14
+ const alignmentMap = {
15
+ top: 'flex-start',
16
+ center: 'center',
17
+ bottom: 'flex-end'
18
+ }
19
+
13
20
  const selectItemBlockStyles = ({ interItemMargin }) => ({
14
21
  marginBottom: interItemMargin
15
22
  })
@@ -21,9 +28,17 @@ const selectDividerStyles = ({ dividerColor, dividerSize, interItemMarginWithDiv
21
28
  paddingBottom: interItemMarginWithDivider
22
29
  })
23
30
 
31
+ const selectAlignmentStyles = (iconVerticalAlign) => ({
32
+ alignItems: alignmentMap[iconVerticalAlign]
33
+ })
34
+
24
35
  /**
25
36
  * ListItem is responsible for rendering icon or a bullet as side item
26
37
  */
38
+ const calculateIconMarginTop = (itemIconSize, fontSize, lineHeightRatio) => {
39
+ return (fontSize * lineHeightRatio - itemIconSize) / VERTICAL_CENTERING_DIVISOR
40
+ }
41
+
27
42
  const ListItemBase = React.forwardRef(
28
43
  (
29
44
  {
@@ -35,6 +50,7 @@ const ListItemBase = React.forwardRef(
35
50
  children,
36
51
  title,
37
52
  isLastItem,
53
+ iconVerticalAlign = 'top',
38
54
  accessibilityRole = Platform.select({ web: 'listitem', default: undefined }),
39
55
  ...rest
40
56
  },
@@ -45,15 +61,15 @@ const ListItemBase = React.forwardRef(
45
61
  const itemBlockStyles = selectItemBlockStyles(themeTokens)
46
62
  const dividerStyles = selectDividerStyles(themeTokens)
47
63
  const { iconMarginTop, itemIconSize } = themeTokens
48
- let adjustedIconMarginTop = iconMarginTop
49
64
  const { fontSize, lineHeight: lineHeightRatio } = useThemeTokens(
50
65
  'Typography',
51
66
  {},
52
67
  { size: 'h4', bold: true }
53
68
  )
54
- if (title) {
55
- adjustedIconMarginTop = (fontSize * lineHeightRatio - itemIconSize) / 2
56
- }
69
+ const adjustedIconMarginTop = title
70
+ ? calculateIconMarginTop(itemIconSize, fontSize, lineHeightRatio)
71
+ : iconMarginTop
72
+
57
73
  /**
58
74
  * Function responsible returning styling, in case the item is the last shouldn't
59
75
  * add extra margin on the bottom, if "showDivider" is true it should add a divider
@@ -81,14 +97,14 @@ const ListItemBase = React.forwardRef(
81
97
  {typeof children === 'function' ? (
82
98
  children({ tokens, icon, iconColor, iconSize, isLastItem })
83
99
  ) : (
84
- <View style={staticStyles.container}>
100
+ <View style={[staticStyles.innerContainer, selectAlignmentStyles(iconVerticalAlign)]}>
85
101
  <ListItemMark
86
102
  tokens={{ ...tokens, iconMarginTop: adjustedIconMarginTop }}
87
103
  icon={icon}
88
104
  iconColor={iconColor}
89
105
  iconSize={iconSize}
90
106
  />
91
- <View style={staticStyles.titleAndContentContainer}>
107
+ <View style={[staticStyles.titleAndContentContainer]}>
92
108
  {Boolean(title) && (
93
109
  <Typography variant={{ size: 'h4', bold: true }}>{title}</Typography>
94
110
  )}
@@ -101,16 +117,22 @@ const ListItemBase = React.forwardRef(
101
117
  }
102
118
  )
103
119
  ListItemBase.displayName = 'ListItem'
120
+ ListItemBase.__UDS_COMPONENT_TYPE__ = 'ListItem'
104
121
 
105
122
  const staticStyles = StyleSheet.create({
106
123
  container: {
124
+ flexDirection: 'row',
125
+ width: '100%'
126
+ },
127
+ innerContainer: {
107
128
  flex: 1,
108
129
  flexDirection: 'row'
109
130
  },
110
131
  titleAndContentContainer: {
111
132
  flexDirection: 'column',
112
- flexShrink: 1,
113
- flexGrow: 1
133
+ flex: 1,
134
+ flexGrow: 1,
135
+ flexShrink: 1
114
136
  }
115
137
  })
116
138
 
@@ -119,6 +141,8 @@ ListItemBase.propTypes = {
119
141
  tokens: getTokensPropType('List'),
120
142
  variant: variantProp.propType,
121
143
  children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
144
+ /** Controls the vertical alignment of the icon */
145
+ iconVerticalAlign: PropTypes.oneOf(['top', 'center', 'bottom']),
122
146
  /**
123
147
  * Renders side item icon
124
148
  */
@@ -1,7 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
-
4
- import { View, StyleSheet } from 'react-native'
3
+ import { View } from 'react-native'
5
4
  import Icon from '../Icon'
6
5
  import { useVariants } from '../utils'
7
6
 
@@ -9,33 +8,21 @@ export const tokenTypes = {
9
8
  itemIconSize: PropTypes.number.isRequired,
10
9
  itemIconColor: PropTypes.string.isRequired,
11
10
  listGutter: PropTypes.number.isRequired,
12
- iconMarginTop: PropTypes.number.isRequired
11
+ iconMarginTop: PropTypes.number.isRequired,
12
+ bulletIcon: PropTypes.elementType.isRequired
13
13
  }
14
14
 
15
- const selectItemIconTokens = ({ itemIconSize, itemIconColor }) => ({
16
- size: itemIconSize,
17
- color: itemIconColor
18
- })
19
-
20
- const selectSideItemContainerStyles = ({ listGutter, iconMarginTop }) => ({
21
- marginTop: iconMarginTop,
22
- marginRight: listGutter
15
+ const selectContainerStyles = ({ listGutter }) => ({
16
+ marginInlineEnd: listGutter,
17
+ display: 'flex',
18
+ alignItems: 'flex-start'
23
19
  })
24
-
25
- // Align bullets with the top line of text the same way icons are aligned
26
- const selectBulletPositioningStyles = ({ itemIconSize }) => ({
20
+ const selectBulletStyles = ({ itemIconSize }) => ({
27
21
  width: itemIconSize,
28
- height: itemIconSize
29
- })
30
-
31
- const selectBulletContainerStyles = ({
32
- itemBulletContainerWidth,
33
- itemBulletContainerHeight,
34
- itemBulletContainerAlign
35
- }) => ({
36
- width: itemBulletContainerWidth,
37
- height: itemBulletContainerHeight,
38
- alignItems: itemBulletContainerAlign
22
+ height: itemIconSize,
23
+ alignItems: 'center',
24
+ justifyContent: 'center',
25
+ flexShrink: 0
39
26
  })
40
27
 
41
28
  const getIconColorVariants = (iconVariants) =>
@@ -50,26 +37,24 @@ const getIconColorVariants = (iconVariants) =>
50
37
  */
51
38
  const ListItemMark = React.forwardRef(({ icon, iconColor, iconSize, tokens = {} }, ref) => {
52
39
  const themeTokens = typeof tokens === 'function' ? tokens() : tokens
40
+ const containerStyles = selectContainerStyles(themeTokens)
53
41
 
54
- const sideItemContainerStyles = selectSideItemContainerStyles(themeTokens)
55
- const bulletContainerStyles = selectBulletContainerStyles(themeTokens)
56
-
57
- // TODO: Remove it when iconColor custom colors are deprecated.
58
42
  const iconVariants = useVariants('Icon')
59
43
  const iconColorVariants = getIconColorVariants(iconVariants)
60
44
 
61
45
  if (icon) {
62
- const iconTokens = selectItemIconTokens(themeTokens)
46
+ const { itemIconSize, itemIconColor } = themeTokens
47
+ const finalIconSize = iconSize ?? itemIconSize
48
+ const finalIconColor =
49
+ iconColor && !iconColorVariants?.includes(iconColor) ? iconColor : itemIconColor
50
+
63
51
  return (
64
- <View style={[sideItemContainerStyles, bulletContainerStyles]}>
52
+ <View style={containerStyles}>
65
53
  <Icon
66
54
  icon={icon}
67
55
  tokens={{
68
- size: iconSize ?? iconTokens.size,
69
- ...(((iconColor && !iconColorVariants?.includes(iconColor)) || !iconColor) && {
70
- color:
71
- iconColor && !iconColorVariants?.includes(iconColor) ? iconColor : iconTokens.color
72
- })
56
+ size: finalIconSize,
57
+ color: finalIconColor
73
58
  }}
74
59
  variant={{
75
60
  ...(iconColorVariants?.includes(iconColor) && { color: iconColor })
@@ -79,18 +64,19 @@ const ListItemMark = React.forwardRef(({ icon, iconColor, iconSize, tokens = {}
79
64
  )
80
65
  }
81
66
 
82
- const bulletColor = themeTokens.itemBulletColor
83
- const { bulletIcon } = themeTokens
84
- const itemBulletContainerStyles = selectBulletContainerStyles(themeTokens)
85
- const itemBulletPositioningStyles = selectBulletPositioningStyles(themeTokens)
67
+ const { itemIconSize, itemIconColor } = themeTokens
68
+ const bulletStyles = selectBulletStyles(themeTokens)
86
69
 
87
70
  return (
88
- <View style={[sideItemContainerStyles, itemBulletContainerStyles]} ref={ref}>
89
- <View
90
- style={[staticStyles.bulletPositioning, itemBulletPositioningStyles]}
91
- testID="unordered-item-bullet"
92
- >
93
- <Icon icon={bulletIcon} tokens={{ color: bulletColor, size: themeTokens.itemIconSize }} />
71
+ <View style={containerStyles} ref={ref}>
72
+ <View style={bulletStyles}>
73
+ <Icon
74
+ icon={themeTokens.bulletIcon}
75
+ tokens={{
76
+ color: itemIconColor,
77
+ size: itemIconSize
78
+ }}
79
+ />
94
80
  </View>
95
81
  </View>
96
82
  )
@@ -114,11 +100,4 @@ ListItemMark.propTypes = {
114
100
  iconSize: PropTypes.number
115
101
  }
116
102
 
117
- const staticStyles = StyleSheet.create({
118
- bulletPositioning: {
119
- alignItems: 'center',
120
- justifyContent: 'center'
121
- }
122
- })
123
-
124
103
  export default ListItemMark
@@ -81,6 +81,7 @@ const PressableListItemBase = React.forwardRef(
81
81
  )
82
82
 
83
83
  PressableListItemBase.displayName = 'PressableListItemBase'
84
+ PressableListItemBase.__UDS_COMPONENT_TYPE__ = 'ListItem'
84
85
 
85
86
  const staticStyles = StyleSheet.create({
86
87
  itemContainer: {
@@ -68,9 +68,10 @@ const selectModalStyles = ({
68
68
  ...applyShadowToken(shadow)
69
69
  })
70
70
 
71
- const selectBackdropStyles = ({ backdropColor, backdropOpacity }) => ({
71
+ const selectBackdropStyles = ({ backdropColor, backdropOpacity, backdropCursor }) => ({
72
72
  backgroundColor: backdropColor,
73
- opacity: backdropOpacity
73
+ opacity: backdropOpacity,
74
+ ...(Platform.OS === 'web' && backdropCursor ? { cursor: backdropCursor } : {})
74
75
  })
75
76
 
76
77
  const selectCloseButtonContainerStyles = ({ paddingRight, paddingTop }) => ({
@@ -119,12 +120,20 @@ const Modal = React.forwardRef(
119
120
  cancelButtonText,
120
121
  cancelButtonType,
121
122
  footer,
123
+ backgroundDismissible = true,
122
124
  ...rest
123
125
  },
124
126
  ref
125
127
  ) => {
126
128
  const viewport = useViewport()
127
- const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth })
129
+
130
+ const isBackdropClickable = onClose && backgroundDismissible
131
+
132
+ const themeTokens = useThemeTokens('Modal', tokens, variant, {
133
+ viewport,
134
+ maxWidth,
135
+ backdropCursor: isBackdropClickable ? 'pointer' : 'default'
136
+ })
128
137
  const modalRef = useScrollBlocking(isOpen)
129
138
  const modalBodyRef = React.useRef(ref)
130
139
  const modalContentRef = React.useRef(null)
@@ -253,7 +262,7 @@ const Modal = React.forwardRef(
253
262
  </View>
254
263
  {/* when a modal becomes open its first focusable element is being automatically focused */}
255
264
  {/* and we prefer the close button over backdrop */}
256
- <TouchableWithoutFeedback onPress={handleClose}>
265
+ <TouchableWithoutFeedback onPress={isBackdropClickable && handleClose}>
257
266
  <View style={[staticStyles.backdrop, selectBackdropStyles(themeTokens)]} />
258
267
  </TouchableWithoutFeedback>
259
268
  </ScrollView>
@@ -349,7 +358,15 @@ Modal.propTypes = {
349
358
  /**
350
359
  * Receive a react node or an array of nodes to render at the bottom of the modal, above the action buttons.
351
360
  */
352
- footer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
361
+ footer: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
362
+ /**
363
+ * Controls whether the modal can be dismissed by clicking on the backdrop.
364
+ * When set to `false`, clicking the backdrop will not close the modal.
365
+ * The backdrop cursor automatically changes to 'default' to indicate it's not clickable.
366
+ * Note: Backdrop dismissal requires `onClose` to be defined.
367
+ * @default true
368
+ */
369
+ backgroundDismissible: PropTypes.bool
353
370
  }
354
371
 
355
372
  export default Modal
@@ -361,12 +378,7 @@ const staticStyles = StyleSheet.create({
361
378
  left: 0,
362
379
  right: 0,
363
380
  bottom: 0,
364
- zIndex: -1,
365
- ...Platform.select({
366
- web: {
367
- cursor: 'pointer'
368
- }
369
- })
381
+ zIndex: -1
370
382
  },
371
383
  positioningContainer: {
372
384
  flexBasis: '100%',
@@ -4,6 +4,7 @@ import { View, StyleSheet } from 'react-native'
4
4
 
5
5
  import { applyShadowToken, useThemeTokens } from '../ThemeProvider'
6
6
  import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
7
+ import ProgressContext from './ProgressContext'
7
8
 
8
9
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
9
10
 
@@ -45,6 +46,12 @@ const selectProgressStyles = ({
45
46
  *
46
47
  * - Use the `size` variant to control the height of your progress bars: passing `'mini'` will make your
47
48
  * progress bar container narrower.
49
+ * - Use the `layers` variant to control how multiple progress bars are positioned:
50
+ * - `false` (default): bars are positioned vertically one below the other.
51
+ * - `true`: bars overlay on top of each other (layered/stacked on z-axis).
52
+ * Note: The `layers` prop is deprecated. After August 2026, `layers: true` will become the permanent
53
+ * default behavior and the `layers` prop will be removed. To maintain vertical layout after removal,
54
+ * use separate individual Progress components.
48
55
  *
49
56
  * ## Usability and A11y guidelines
50
57
  *
@@ -55,15 +62,19 @@ const selectProgressStyles = ({
55
62
  */
56
63
  const Progress = React.forwardRef(({ children, tokens, variant, ...rest }, ref) => {
57
64
  const themeTokens = useThemeTokens('Progress', tokens, variant)
65
+ // Default to false (vertical layout) to preserve existing behavior and avoid breaking changes
66
+ const layers = variant?.layers ?? false
58
67
 
59
68
  return (
60
- <View
61
- ref={ref}
62
- style={[staticStyles.progressContainer, selectProgressStyles(themeTokens)]}
63
- {...selectProps(rest)}
64
- >
65
- {children}
66
- </View>
69
+ <ProgressContext.Provider value={{ layers }}>
70
+ <View
71
+ ref={ref}
72
+ style={[staticStyles.progressContainer, selectProgressStyles(themeTokens)]}
73
+ {...selectProps(rest)}
74
+ >
75
+ {children}
76
+ </View>
77
+ </ProgressContext.Provider>
67
78
  )
68
79
  })
69
80
  Progress.displayName = 'Progress'
@@ -6,22 +6,23 @@ import ProgressBarBackground from './ProgressBarBackground'
6
6
  import { applyShadowToken, useThemeTokens } from '../ThemeProvider'
7
7
  import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
8
8
  import { MAX_PERCENT_VALUE, MIN_PERCENT_VALUE } from './constants'
9
+ import ProgressContext from './ProgressContext'
9
10
 
10
11
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
11
12
 
12
- const selectBarStyles = (
13
- { backgroundColor, borderRadius, outlineWidth, outlineColor, shadow },
14
- calculatedPercentage,
15
- barPosition
16
- ) => ({
17
- backgroundColor,
18
- borderRadius,
19
- outlineWidth,
20
- outlineColor,
21
- ...applyShadowToken(shadow),
22
- width: `${calculatedPercentage}%`,
23
- left: `${barPosition}%`
24
- })
13
+ const selectBarStyles = ({ themeTokens, calculatedPercentage, barPosition, layers }) => {
14
+ const { backgroundColor, borderRadius, outlineWidth, outlineColor, shadow } = themeTokens
15
+ return {
16
+ backgroundColor,
17
+ borderRadius,
18
+ outlineWidth,
19
+ outlineColor,
20
+ ...applyShadowToken(shadow),
21
+ width: `${calculatedPercentage}%`,
22
+ left: `${barPosition}%`,
23
+ ...(layers ? { position: 'absolute' } : {})
24
+ }
25
+ }
25
26
 
26
27
  /**
27
28
  * The `ProgressBar` is a visual representation of linear progression.
@@ -70,6 +71,7 @@ const ProgressBar = React.forwardRef(
70
71
  },
71
72
  ref
72
73
  ) => {
74
+ const { layers } = React.useContext(ProgressContext)
73
75
  const { items, current } = offset
74
76
  let calculatedPercentage = percentage
75
77
  let barPosition = MIN_PERCENT_VALUE
@@ -104,7 +106,10 @@ const ProgressBar = React.forwardRef(
104
106
  return percentage > MIN_PERCENT_VALUE || items ? (
105
107
  <View
106
108
  ref={ref}
107
- style={[staticStyles.bar, selectBarStyles(themeTokens, calculatedPercentage, barPosition)]}
109
+ style={[
110
+ staticStyles.bar,
111
+ selectBarStyles({ themeTokens, calculatedPercentage, barPosition, layers })
112
+ ]}
108
113
  {...selectedProps}
109
114
  >
110
115
  {children ?? <ProgressBarBackground variant={variant} />}
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react'
2
+
3
+ const ProgressContext = createContext({ layers: false })
4
+
5
+ export default ProgressContext
@@ -29,9 +29,9 @@ function selectItemStyles({
29
29
  }) {
30
30
  return {
31
31
  backgroundColor,
32
- borderTopColor: borderColor,
33
- borderTopWidth: borderWidth,
34
- borderTopStyle: borderStyle,
32
+ borderBottomColor: borderColor,
33
+ borderBottomWidth: borderWidth,
34
+ borderBottomStyle: borderStyle,
35
35
  paddingLeft,
36
36
  paddingRight,
37
37
  paddingTop,