@telus-uds/components-base 4.0.0-alpha.0 → 4.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -6
  2. package/jest.config.cjs +1 -0
  3. package/lib/cjs/ActionCard/ActionCard.js +10 -3
  4. package/lib/cjs/Card/Card.js +3 -3
  5. package/lib/cjs/Carousel/Carousel.js +6 -1
  6. package/lib/cjs/Carousel/CarouselThumbnail.js +125 -33
  7. package/lib/cjs/Carousel/CarouselThumbnailNavigation.js +8 -1
  8. package/lib/cjs/InputSupports/InputSupports.js +1 -2
  9. package/lib/cjs/InputSupports/useInputSupports.js +1 -3
  10. package/lib/cjs/Link/ChevronLink.js +1 -2
  11. package/lib/cjs/Notification/Notification.js +4 -12
  12. package/lib/cjs/Progress/ProgressBar.js +3 -3
  13. package/lib/cjs/Progress/ProgressBarBackground.js +2 -3
  14. package/lib/cjs/QuickLinks/QuickLinks.js +7 -0
  15. package/lib/cjs/StackView/StackWrapBox.js +9 -1
  16. package/lib/cjs/StackView/StackWrapGap.js +3 -1
  17. package/lib/cjs/StackView/getStackedContent.js +20 -10
  18. package/lib/esm/ActionCard/ActionCard.js +10 -3
  19. package/lib/esm/Card/Card.js +3 -3
  20. package/lib/esm/Carousel/Carousel.js +6 -1
  21. package/lib/esm/Carousel/CarouselThumbnail.js +126 -34
  22. package/lib/esm/Carousel/CarouselThumbnailNavigation.js +8 -1
  23. package/lib/esm/InputSupports/InputSupports.js +1 -2
  24. package/lib/esm/InputSupports/useInputSupports.js +1 -3
  25. package/lib/esm/Link/ChevronLink.js +1 -2
  26. package/lib/esm/Notification/Notification.js +4 -12
  27. package/lib/esm/Progress/ProgressBar.js +3 -3
  28. package/lib/esm/Progress/ProgressBarBackground.js +2 -3
  29. package/lib/esm/QuickLinks/QuickLinks.js +7 -0
  30. package/lib/esm/StackView/StackWrapBox.js +9 -1
  31. package/lib/esm/StackView/StackWrapGap.js +3 -1
  32. package/lib/esm/StackView/getStackedContent.js +20 -10
  33. package/lib/package.json +2 -2
  34. package/package.json +2 -2
  35. package/src/ActionCard/ActionCard.jsx +13 -4
  36. package/src/Card/Card.jsx +3 -3
  37. package/src/Carousel/Carousel.jsx +6 -1
  38. package/src/Carousel/CarouselThumbnail.jsx +117 -30
  39. package/src/Carousel/CarouselThumbnailNavigation.jsx +8 -2
  40. package/src/InputSupports/InputSupports.jsx +1 -6
  41. package/src/InputSupports/useInputSupports.js +1 -1
  42. package/src/Link/ChevronLink.jsx +1 -2
  43. package/src/Notification/Notification.jsx +5 -21
  44. package/src/Progress/ProgressBar.jsx +3 -3
  45. package/src/Progress/ProgressBarBackground.jsx +2 -3
  46. package/src/QuickLinks/QuickLinks.jsx +8 -0
  47. package/src/StackView/StackWrapBox.jsx +13 -1
  48. package/src/StackView/StackWrapGap.jsx +2 -1
  49. package/src/StackView/getStackedContent.jsx +22 -8
  50. package/types/Link.d.ts +1 -2
@@ -1,9 +1,10 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { Pressable, Image } from 'react-native'
3
+ import { StyleSheet, Pressable, Image, View } from 'react-native'
4
4
  import { useCarousel } from './CarouselContext'
5
5
  import { useThemeTokensCallback } from '../ThemeProvider/useThemeTokens'
6
6
  import { useViewport } from '../ViewportProvider/useViewport'
7
+ import { Icon } from '../Icon/Icon'
7
8
 
8
9
  const selectPressableTokens = ({ borderColor, borderRadius, borderWidth, margin, padding }) => ({
9
10
  borderColor,
@@ -13,12 +14,47 @@ const selectPressableTokens = ({ borderColor, borderRadius, borderWidth, margin,
13
14
  padding
14
15
  })
15
16
 
17
+ // Play icon overlay appearance is consistent across all brands and is not theme-configurable.
18
+ // The circle occupies 55% of the thumbnail size, and the icon occupies 55% of the circle diameter.
19
+ const PLAY_ICON_RATIO = 0.55
20
+ const PLAY_ICON_BORDER_RADIUS_DIVISOR = 2
21
+
22
+ const selectImageStyles = (size) => ({
23
+ width: size,
24
+ height: size
25
+ })
26
+
27
+ const selectSelectedStyles = ({ selectedBorderColor, selectedBorderWidth, padding, margin }) => ({
28
+ borderColor: selectedBorderColor,
29
+ borderWidth: selectedBorderWidth,
30
+ padding: padding - selectedBorderWidth,
31
+ marginBottom: margin + selectedBorderWidth
32
+ })
33
+
34
+ const selectNonSelectedStyles = ({ borderWidth, padding, margin, selectedBorderWidth }) => ({
35
+ padding: padding - borderWidth,
36
+ marginBottom: margin + selectedBorderWidth
37
+ })
38
+
39
+ const selectPlayIconCircleStyles = (thumbnailSize) => {
40
+ const diameter = thumbnailSize * PLAY_ICON_RATIO
41
+ return {
42
+ width: diameter,
43
+ height: diameter,
44
+ borderRadius: diameter / PLAY_ICON_BORDER_RADIUS_DIVISOR
45
+ }
46
+ }
47
+
48
+ const selectPlayIconTokens = (thumbnailSize) => ({
49
+ size: thumbnailSize * PLAY_ICON_RATIO * PLAY_ICON_RATIO
50
+ })
51
+
16
52
  /**
17
- * `Carousel.Thumbnail` is used to wrap the content of an individual slide and is suppsoed to be the
53
+ * `CarouselThumbnail` is used to wrap the content of an individual slide and is suppsoed to be the
18
54
  * only top-level component passed to the `Carousel`
19
55
  */
20
56
  export const CarouselThumbnail = React.forwardRef(
21
- ({ accessibilityLabel, alt, index, src }, ref) => {
57
+ ({ accessibilityLabel, alt, index, isVideo, src }, ref) => {
22
58
  const { activeIndex, itemLabel, totalItems, getCopyWithPlaceholders, goTo } = useCarousel()
23
59
  const getThumbnailTokens = useThemeTokensCallback('CarouselThumbnail')
24
60
  const viewport = useViewport()
@@ -33,24 +69,15 @@ export const CarouselThumbnail = React.forwardRef(
33
69
  // Allow using the spacebar for navigation
34
70
  if (event?.key === ' ') goTo(index)
35
71
  }
36
- const { borderWidth, padding, selectedBorderColor, selectedBorderWidth, size, margin } =
37
- getThumbnailTokens({ viewport })
38
- const styles = {
39
- image: {
40
- height: size,
41
- width: size
42
- },
43
- selected: {
44
- borderColor: selectedBorderColor,
45
- borderWidth: selectedBorderWidth,
46
- padding: padding - selectedBorderWidth,
47
- marginBottom: margin + selectedBorderWidth
48
- },
49
- nonSelected: {
50
- padding: padding - borderWidth,
51
- marginBottom: margin + selectedBorderWidth
52
- }
53
- }
72
+ const {
73
+ borderWidth,
74
+ padding,
75
+ selectedBorderColor,
76
+ selectedBorderWidth,
77
+ size,
78
+ margin,
79
+ playIcon
80
+ } = getThumbnailTokens({ viewport })
54
81
 
55
82
  return (
56
83
  <Pressable
@@ -68,18 +95,48 @@ export const CarouselThumbnail = React.forwardRef(
68
95
 
69
96
  return [
70
97
  pressableStyles,
71
- index === activeIndex ? [styles.selected, { outline: 'none' }] : styles.nonSelected
98
+ index === activeIndex
99
+ ? [
100
+ selectSelectedStyles({
101
+ selectedBorderColor,
102
+ selectedBorderWidth,
103
+ padding,
104
+ margin
105
+ }),
106
+ staticStyles.selectedPressableOutline
107
+ ]
108
+ : selectNonSelectedStyles({ borderWidth, padding, margin, selectedBorderWidth })
72
109
  ]
73
110
  }}
74
111
  ref={ref}
75
112
  >
76
- <Image
77
- accessibilityIgnoresInvertColors
78
- accessibilityLabel={accessibilityLabel ?? alt}
79
- source={src}
80
- style={styles.image}
81
- title={thumbnailTitle}
82
- />
113
+ <View style={[staticStyles.imageContainer, selectImageStyles(size)]}>
114
+ <Image
115
+ accessibilityIgnoresInvertColors
116
+ accessibilityLabel={accessibilityLabel ?? alt}
117
+ source={src}
118
+ style={selectImageStyles(size)}
119
+ title={thumbnailTitle}
120
+ />
121
+ {isVideo && playIcon && (
122
+ <View
123
+ style={staticStyles.playIconOverlayContainer}
124
+ testID={`play-icon-overlay-${index}`}
125
+ >
126
+ <View
127
+ style={[selectPlayIconCircleStyles(size), staticStyles.playIconCircleBackground]}
128
+ >
129
+ <Icon
130
+ icon={playIcon}
131
+ tokens={{
132
+ ...selectPlayIconTokens(size),
133
+ color: staticStyles.playIconSymbol.color
134
+ }}
135
+ />
136
+ </View>
137
+ </View>
138
+ )}
139
+ </View>
83
140
  </Pressable>
84
141
  )
85
142
  }
@@ -88,8 +145,38 @@ export const CarouselThumbnail = React.forwardRef(
88
145
  CarouselThumbnail.displayName = 'CarouselThumbnail'
89
146
 
90
147
  CarouselThumbnail.propTypes = {
148
+ /** Accessibility label for screen readers, overrides the alt text */
91
149
  accessibilityLabel: PropTypes.string,
150
+ /** Alt text for the thumbnail image */
92
151
  alt: PropTypes.string,
152
+ /** Zero-based index of this thumbnail within the carousel */
93
153
  index: PropTypes.number,
94
- src: PropTypes.string
154
+ /**
155
+ * When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video.
156
+ */
157
+ isVideo: PropTypes.bool,
158
+ /** Image source URI (web) or local asset require() (native) */
159
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
95
160
  }
161
+
162
+ const staticStyles = StyleSheet.create({
163
+ imageContainer: {
164
+ position: 'relative'
165
+ },
166
+ playIconOverlayContainer: {
167
+ position: 'absolute',
168
+ top: 0,
169
+ left: 0,
170
+ right: 0,
171
+ bottom: 0,
172
+ justifyContent: 'center',
173
+ alignItems: 'center'
174
+ },
175
+ selectedPressableOutline: { outlineStyle: 'none' },
176
+ playIconCircleBackground: {
177
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
178
+ justifyContent: 'center',
179
+ alignItems: 'center'
180
+ },
181
+ playIconSymbol: { color: 'rgb(255, 255, 255)' }
182
+ })
@@ -27,11 +27,12 @@ export const CarouselThumbnailNavigation = React.forwardRef(({ thumbnails = [] }
27
27
  return (
28
28
  <View style={containerStyles}>
29
29
  <StackWrap direction="row" tokens={stackWrapTokens} space={2} ref={ref}>
30
- {thumbnails.map(({ accessibilityLabel, alt, src }, index) => (
30
+ {thumbnails.map(({ accessibilityLabel, alt, isVideo, src }, index) => (
31
31
  <CarouselThumbnail
32
32
  accessibilityLabel={accessibilityLabel}
33
33
  alt={alt}
34
34
  index={index}
35
+ isVideo={isVideo}
35
36
  key={src}
36
37
  src={src}
37
38
  />
@@ -47,9 +48,14 @@ CarouselThumbnailNavigation.propTypes = {
47
48
  */
48
49
  thumbnails: PropTypes.arrayOf(
49
50
  PropTypes.shape({
51
+ /** Accessibility label for the thumbnail image, used by assistive technologies. */
50
52
  accessibilityLabel: PropTypes.string,
53
+ /** Alternative text for the thumbnail image, displayed when the image cannot be rendered. */
51
54
  alt: PropTypes.string,
52
- src: PropTypes.string
55
+ /** When true, renders a play icon overlay on the thumbnail to indicate that the slide contains a video. */
56
+ isVideo: PropTypes.bool,
57
+ /** URL or path of the thumbnail image. When used with `isVideo`, this should be the video's poster/preview image. */
58
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
53
59
  })
54
60
  ).isRequired
55
61
  }
@@ -67,12 +67,7 @@ export const InputSupports = React.forwardRef(
67
67
  />
68
68
  )}
69
69
  {typeof children === 'function'
70
- ? children({
71
- inputId,
72
- ...a11yProps,
73
- validation: feedbackValidation,
74
- accessibilityDescribedBy: feedbackId
75
- })
70
+ ? children({ inputId, ...a11yProps, validation: feedbackValidation })
76
71
  : children}
77
72
  {feedback || maxCharsReachedErrorMessage ? (
78
73
  <Feedback
@@ -31,8 +31,8 @@ export const useInputSupports = ({
31
31
  : undefined
32
32
  ]), // native only -> replaced with describedBy on web
33
33
  accessibilityDescribedBy: joinDefined([
34
- !hasValidationError && feedback && feedbackId, // feedback receives a11yRole=alert on error, so there's no need to include it here
35
34
  hint && hintId,
35
+ (feedback && feedbackId) || undefined,
36
36
  charactersCount
37
37
  ? getCopy('charactersRemaining').replace(/%\{charCount\}/g, charactersCount)
38
38
  : undefined
@@ -53,8 +53,7 @@ ChevronLink.propTypes = {
53
53
  children: PropTypes.node,
54
54
  variant: PropTypes.exact({
55
55
  size: PropTypes.oneOf(['large', 'small', 'micro']),
56
- alternative: PropTypes.bool,
57
- inverse: PropTypes.bool
56
+ style: PropTypes.oneOf(['inline', 'subtle', 'feature', 'danger', 'inverse'])
58
57
  }),
59
58
  ...linkProps.types,
60
59
  tokens: getTokensPropType('ChevronLink', 'Link'),
@@ -227,11 +227,10 @@ const getDefaultStyles = (
227
227
  * ### Variants
228
228
  * - Use `variant.style` to set the visual style of the notification
229
229
  * - Use `dismissible` prop to enable dismissible functionality
230
- * - Use `system` prop to set the visual style of the notification and denote an announcement from the system or application
231
230
  *
232
- * ### When to use the system prop?
233
- * - Use `system` to show system-based messages coming from the application
234
- * - Don’t use `system` when the message is in response to user action
231
+ * ### When to use the system style variant?
232
+ * - Use `variant.style ‘system’` to show system-based messages coming from the application
233
+ * - Don’t use the `system` style variant when the message is in response to user action
235
234
  *
236
235
  * ## Variants
237
236
  *
@@ -263,25 +262,14 @@ const getDefaultStyles = (
263
262
  */
264
263
  export const Notification = React.forwardRef(
265
264
  (
266
- {
267
- children,
268
- system,
269
- dismissible,
270
- copy = 'en',
271
- tokens,
272
- variant,
273
- onDismiss,
274
- contentMinWidth,
275
- ...rest
276
- },
265
+ { children, dismissible, copy = 'en', tokens, variant, onDismiss, contentMinWidth, ...rest },
277
266
  ref
278
267
  ) => {
279
268
  const [isDismissed, setIsDismissed] = React.useState(false)
280
269
  const viewport = useViewport()
281
270
  const getCopy = useCopy({ dictionary, copy })
282
271
 
283
- // TODO: Remove this once the system style variant is deprecated
284
- const isSystemEnabled = system || variant?.style?.includes('system')
272
+ const isSystemEnabled = variant?.style?.includes('system')
285
273
 
286
274
  const { themeOptions } = useTheme()
287
275
  const { enableMediaQueryStyleSheet } = themeOptions
@@ -442,10 +430,6 @@ Notification.propTypes = {
442
430
  * Content of the `Notification`.
443
431
  */
444
432
  children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
445
- /**
446
- * Use `system` prop to set the visual style of the notification and denote an announcement from the system or application
447
- */
448
- system: PropTypes.bool,
449
433
  /**
450
434
  * Use the `dismissible` prop to allow users to dismiss the Notification at any time.
451
435
  */
@@ -48,9 +48,9 @@ const selectBarStyles = ({ themeTokens, calculatedPercentage, barPosition, layer
48
48
  *
49
49
  * ## Variants
50
50
  *
51
- * - Use the following variants to render specific types of progress bars:
52
- * - `negative`: set to `true` if you wish to use the negative theming for progress bar filling,
53
- * - `inactive`: set to `true` if you wish to style the progress bar as inactive.
51
+ * - Use `variant={{ style: 'value' }}` to render specific types of progress bars:
52
+ * - `style: 'negative'` for negative theming for progress bar filling,
53
+ * - `style: 'inactive'` to style the progress bar as inactive.
54
54
  *
55
55
  * ## Usability and A11y guidelines
56
56
  *
@@ -9,10 +9,9 @@ const negativeBackground = `%3Csvg xmlns='http://www.w3.org/2000/svg' width='100
9
9
 
10
10
  export const ProgressBarBackground = React.forwardRef(({ variant }, ref) => {
11
11
  let source = null
12
- // TODO: Remove the `variant?.inactive` & `variant?.negative` to complete the deprecation
13
- if (variant?.inactive || variant?.style === INACTIVE_VARIANT) {
12
+ if (variant?.style === INACTIVE_VARIANT) {
14
13
  source = inactiveBackground
15
- } else if (variant?.negative || variant?.style === NEGATIVE_VARIANT) {
14
+ } else if (variant?.style === NEGATIVE_VARIANT) {
16
15
  source = negativeBackground
17
16
  } else {
18
17
  return null
@@ -1,10 +1,12 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
3
 
4
+ import { Platform } from 'react-native'
4
5
  import { useThemeTokens } from '../ThemeProvider/useThemeTokens'
5
6
  import { useViewport } from '../ViewportProvider/useViewport'
6
7
  import { getTokensPropType } from '../utils/props/tokens'
7
8
  import { variantProp } from '../utils/props/variantProp'
9
+ import { tagsToRoles } from '../utils/a11y/semantics'
8
10
 
9
11
  import { List } from '../List/List'
10
12
  import { StackWrap } from '../StackView/StackWrap'
@@ -28,6 +30,11 @@ export const QuickLinks = React.forwardRef(
28
30
  }
29
31
  )
30
32
 
33
+ const itemAccessibilityRole = Platform.select({
34
+ web: ['ul', 'ol'].includes(tag) ? tagsToRoles.li : undefined,
35
+ default: undefined
36
+ })
37
+
31
38
  const content = (list && (
32
39
  <List ref={ref} tokens={listTokens} showDivider={dividers} tag={tag} {...rest}>
33
40
  {children}
@@ -38,6 +45,7 @@ export const QuickLinks = React.forwardRef(
38
45
  gap={stackGap}
39
46
  tokens={{ justifyContent: stackJustify }}
40
47
  tag={tag}
48
+ itemAccessibilityRole={itemAccessibilityRole}
41
49
  {...rest}
42
50
  >
43
51
  {children}
@@ -55,6 +55,7 @@ export const StackWrapBox = React.forwardRef(
55
55
  variant,
56
56
  tag,
57
57
  accessibilityRole,
58
+ itemAccessibilityRole,
58
59
  ...rest
59
60
  },
60
61
  ref
@@ -73,7 +74,12 @@ export const StackWrapBox = React.forwardRef(
73
74
  const gapSize = useSpacingScale(gap)
74
75
  const offsetStyle = { [offsetSides[direction]]: -1 * gapSize }
75
76
  const boxProps = { [gapSides[direction]]: gap, [spaceSides[direction]]: space }
76
- const content = getStackedContent(children, { direction, space: 0, box: boxProps })
77
+ const content = getStackedContent(children, {
78
+ direction,
79
+ space: 0,
80
+ box: boxProps,
81
+ itemAccessibilityRole
82
+ })
77
83
 
78
84
  return (
79
85
  <View
@@ -116,6 +122,12 @@ StackWrapBox.propTypes = {
116
122
  * is not defined, the accessibilityRole will default to "heading".
117
123
  */
118
124
  tag: PropTypes.oneOf(layoutTags),
125
+ /**
126
+ * Optional accessibility role to apply to each item in the stack.
127
+ * On web, items are wrapped (or cloned) with this role, enabling correct list semantics
128
+ * when the container tag is "ul" or "ol".
129
+ */
130
+ itemAccessibilityRole: PropTypes.string,
119
131
  /**
120
132
  * A StackWrap may take any children, but will have no effect if it is only passed one child or is passed children
121
133
  * wrapped in a component. If necessary, children may be wrapped in one React Fragment.
@@ -35,6 +35,7 @@ export const StackWrapGap = React.forwardRef(
35
35
  children,
36
36
  tag,
37
37
  accessibilityRole,
38
+ itemAccessibilityRole,
38
39
  ...rest
39
40
  },
40
41
  ref
@@ -52,7 +53,7 @@ export const StackWrapGap = React.forwardRef(
52
53
  const size = useSpacingScale(space)
53
54
  const gapStyle = { gap: size }
54
55
 
55
- const content = getStackedContent(children, { direction, space: 0 })
56
+ const content = getStackedContent(children, { direction, space: 0, itemAccessibilityRole })
56
57
 
57
58
  return (
58
59
  <View
@@ -32,7 +32,7 @@ import { Spacer } from '../Spacer/Spacer'
32
32
  */
33
33
  export const getStackedContent = (
34
34
  children,
35
- { divider, space, direction = 'column', box, preserveFragments = false }
35
+ { divider, space, direction = 'column', box, preserveFragments = false, itemAccessibilityRole }
36
36
  ) => {
37
37
  const boxProps = box && typeof box === 'object' ? box : { space }
38
38
  const dividerProps = divider && typeof divider === 'object' ? divider : {}
@@ -42,15 +42,29 @@ export const getStackedContent = (
42
42
  const validChildren = React.Children.toArray(topLevelChildren).filter(Boolean)
43
43
  const content = validChildren.reduce((newChildren, child, index) => {
44
44
  const boxID = `Stack-Box-${index}`
45
- const item = box ? (
45
+
46
+ let item
47
+ if (box) {
46
48
  // If wrapped in Box, that Box needs a key.
47
49
  // If possible, use an existing content key; use an index-based key only if necessary.
48
- <Box {...boxProps} key={child.key || boxID} testID={boxID}>
49
- {child}
50
- </Box>
51
- ) : (
52
- child
53
- )
50
+ item = (
51
+ <Box
52
+ {...boxProps}
53
+ accessibilityRole={itemAccessibilityRole}
54
+ key={child.key || boxID}
55
+ testID={boxID}
56
+ >
57
+ {child}
58
+ </Box>
59
+ )
60
+ } else if (itemAccessibilityRole) {
61
+ item = React.cloneElement(child, {
62
+ accessibilityRole: itemAccessibilityRole,
63
+ key: child.key || boxID
64
+ })
65
+ } else {
66
+ item = child
67
+ }
54
68
  if (!index || (!space && !divider)) return [...newChildren, item]
55
69
 
56
70
  const testID = `Stack-${divider ? 'Divider' : 'Spacer'}-${index}`
package/types/Link.d.ts CHANGED
@@ -22,8 +22,7 @@ export enum LinkSize {
22
22
 
23
23
  export type LinkAndTextButtonVariants = {
24
24
  size?: 'large' | 'small' | 'micro'
25
- alternative?: boolean
26
- inverse?: boolean
25
+ style?: 'inline' | 'subtle' | 'feature' | 'danger' | 'inverse'
27
26
  }
28
27
 
29
28
  export type LinkTokens = {