@telus-uds/components-base 3.27.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 (39) hide show
  1. package/CHANGELOG.md +21 -5
  2. package/lib/cjs/Card/CardBase.js +12 -3
  3. package/lib/cjs/ExpandCollapse/Control.js +5 -1
  4. package/lib/cjs/ExpandCollapse/ExpandCollapse.js +17 -8
  5. package/lib/cjs/ExpandCollapse/Panel.js +7 -2
  6. package/lib/cjs/IconButton/IconButton.js +10 -5
  7. package/lib/cjs/Modal/Modal.js +21 -11
  8. package/lib/cjs/Progress/Progress.js +19 -5
  9. package/lib/cjs/Progress/ProgressBar.js +22 -4
  10. package/lib/cjs/Progress/ProgressContext.js +11 -0
  11. package/lib/cjs/SideNav/Item.js +3 -3
  12. package/lib/cjs/SideNav/ItemsGroup.js +46 -19
  13. package/lib/cjs/SideNav/SideNav.js +29 -13
  14. package/lib/esm/Card/CardBase.js +12 -3
  15. package/lib/esm/ExpandCollapse/Control.js +5 -1
  16. package/lib/esm/ExpandCollapse/ExpandCollapse.js +17 -8
  17. package/lib/esm/ExpandCollapse/Panel.js +7 -2
  18. package/lib/esm/IconButton/IconButton.js +10 -5
  19. package/lib/esm/Modal/Modal.js +21 -11
  20. package/lib/esm/Progress/Progress.js +19 -5
  21. package/lib/esm/Progress/ProgressBar.js +22 -4
  22. package/lib/esm/Progress/ProgressContext.js +5 -0
  23. package/lib/esm/SideNav/Item.js +3 -3
  24. package/lib/esm/SideNav/ItemsGroup.js +45 -20
  25. package/lib/esm/SideNav/SideNav.js +29 -13
  26. package/lib/package.json +2 -2
  27. package/package.json +2 -2
  28. package/src/Card/CardBase.jsx +9 -3
  29. package/src/ExpandCollapse/Control.jsx +1 -1
  30. package/src/ExpandCollapse/ExpandCollapse.jsx +9 -8
  31. package/src/ExpandCollapse/Panel.jsx +10 -2
  32. package/src/IconButton/IconButton.jsx +40 -28
  33. package/src/Modal/Modal.jsx +23 -11
  34. package/src/Progress/Progress.jsx +18 -7
  35. package/src/Progress/ProgressBar.jsx +19 -14
  36. package/src/Progress/ProgressContext.js +5 -0
  37. package/src/SideNav/Item.jsx +3 -3
  38. package/src/SideNav/ItemsGroup.jsx +36 -16
  39. 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
@@ -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,
@@ -6,6 +6,12 @@ import ExpandCollapse from '../ExpandCollapse'
6
6
  import { getTokensPropType, variantProp, componentPropType, selectTokens } from '../utils'
7
7
  import { useThemeTokensCallback } from '../ThemeProvider'
8
8
 
9
+ const selectPanelTokens = ({ borderWidth, borderColor, backgroundColor }) => ({
10
+ contentPanelBackgroundColor: backgroundColor,
11
+ borderBottomWidth: borderWidth,
12
+ borderColor
13
+ })
14
+
9
15
  /**
10
16
  Expandable content areas for use within `SideNav`.
11
17
 
@@ -37,27 +43,30 @@ const ItemsGroup = React.forwardRef(
37
43
  const getItemAppearance = (appearance) => ({ ...getAppearance(appearance), type: 'parent' })
38
44
 
39
45
  const getGroupTokens = useThemeTokensCallback('SideNavItemsGroup', tokens, variant)
40
- const getPanelTokens = (appearance) => {
41
- const { panelBorderColor, ...itemsGroupTokens } = getGroupTokens(getAppearance(appearance))
42
- const groupTokens = {
43
- ...itemsGroupTokens,
44
- borderWidth: 0,
45
- marginBottom: 0
46
- }
47
- return selectTokens('ExpandCollapsePanel', groupTokens)
48
- }
49
-
50
46
  const getItemTokens = useThemeTokensCallback('SideNavItem', itemTokens, variant)
51
- const getControlTokens = (appearance) =>
52
- selectTokens('ExpandCollapseControl', {
53
- ...getItemTokens(getItemAppearance(appearance)), // main style from SideNavItem
54
- ...getGroupTokens(getAppearance(appearance)) // control-specific tokens like icon etc
47
+
48
+ const getPanelTokens = (appearance) =>
49
+ selectTokens('ExpandCollapsePanel', {
50
+ ...staticTokens.panel,
51
+ ...getGroupTokens(getAppearance(appearance)),
52
+ ...selectPanelTokens(getItemTokens(getItemAppearance(appearance)))
55
53
  })
56
54
 
55
+ const getControlTokens = (appearance) => ({
56
+ ...selectTokens('ExpandCollapseControl', {
57
+ ...getItemTokens(getItemAppearance(appearance)), // main style from SideNavItem
58
+ ...getGroupTokens(getAppearance(appearance)) // control-specific tokens like icon etc,
59
+ }),
60
+ ...staticTokens.control
61
+ })
62
+
57
63
  const controlContent = (controlState) => {
58
64
  const currentItemTokens = getItemTokens(getItemAppearance(controlState))
59
-
60
- return <ItemContent tokens={currentItemTokens}>{label}</ItemContent>
65
+ return (
66
+ <ItemContent tokens={{ ...currentItemTokens, ...staticTokens.content }}>
67
+ {label}
68
+ </ItemContent>
69
+ )
61
70
  }
62
71
 
63
72
  return (
@@ -70,12 +79,23 @@ const ItemsGroup = React.forwardRef(
70
79
  controlTokens={getControlTokens}
71
80
  control={controlContent}
72
81
  accessibilityState={{ active: isActive }} // ExpandCollapse.Panel handles expanded state
82
+ disableMobileScrollBuffer
73
83
  >
74
84
  {children}
75
85
  </ExpandCollapse.Panel>
76
86
  )
77
87
  }
78
88
  )
89
+
90
+ const staticTokens = {
91
+ panel: {
92
+ borderWidth: 0,
93
+ marginBottom: 0
94
+ },
95
+ control: { borderWidth: 0, textLine: null },
96
+ content: { accentWidth: 0 }
97
+ }
98
+
79
99
  ItemsGroup.displayName = 'ItemsGroup'
80
100
 
81
101
  ItemsGroup.propTypes = {
@@ -14,14 +14,23 @@ import {
14
14
  viewProps
15
15
  } from '../utils'
16
16
 
17
- function selectBorderStyles(tokens) {
17
+ const selectContainerTokens = ({ borderWidth, borderStyle, borderColor }) => {
18
18
  return {
19
- borderBottomWidth: tokens.borderWidth,
20
- borderBottomStyle: tokens.borderStyle,
21
- borderBottomColor: tokens.borderColor
19
+ borderTopWidth: borderWidth,
20
+ borderStyle,
21
+ borderColor
22
22
  }
23
23
  }
24
24
 
25
+ const selectItemTokens = (tokens = {}, isLastItem = false) => ({
26
+ ...tokens,
27
+ ...(isLastItem
28
+ ? {
29
+ borderWidth: 0
30
+ }
31
+ : {})
32
+ })
33
+
25
34
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
26
35
 
27
36
  /**
@@ -46,11 +55,11 @@ const SideNav = React.forwardRef(
46
55
  <ExpandCollapse
47
56
  ref={ref}
48
57
  maxOpen={accordion ? 1 : null}
49
- style={selectBorderStyles(themeTokens)}
58
+ tokens={selectContainerTokens(themeTokens)}
50
59
  {...selectProps(rest)}
51
60
  >
52
61
  {({ openIds, onToggle }) => {
53
- const renderItem = (item, index, groupId) => {
62
+ const renderItem = (item, index, groupId, isLastItem = false) => {
54
63
  const { itemId = `item-${index}`, onPress } = item.props
55
64
  const handlePress = (...args) => {
56
65
  onItemPress(...args)
@@ -64,7 +73,7 @@ const SideNav = React.forwardRef(
64
73
  itemId={itemId}
65
74
  groupId={groupId}
66
75
  variant={groupId ? { ...variant, type: 'child' } : variant}
67
- tokens={itemTokens}
76
+ tokens={selectItemTokens(itemTokens, isLastItem)}
68
77
  isActive={isItemActive(itemId, groupId)}
69
78
  onPress={handlePress}
70
79
  />
@@ -93,7 +102,12 @@ const SideNav = React.forwardRef(
93
102
  onToggle={handleToggle}
94
103
  >
95
104
  {React.Children.map(child.props.children, (item, itemIndex) =>
96
- renderItem(item, itemIndex, groupId)
105
+ renderItem(
106
+ item,
107
+ itemIndex,
108
+ groupId,
109
+ itemIndex === child.props.children.length - 1
110
+ )
97
111
  )}
98
112
  </ItemsGroup>
99
113
  )