@telus-uds/components-base 1.12.0 → 1.14.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 (132) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/component-docs.json +933 -55
  3. package/lib/BaseProvider/index.js +7 -2
  4. package/lib/Button/ButtonBase.js +52 -19
  5. package/lib/Button/ButtonGroup.js +7 -0
  6. package/lib/Button/propTypes.js +18 -0
  7. package/lib/Carousel/Carousel.js +83 -58
  8. package/lib/Carousel/CarouselContext.js +22 -8
  9. package/lib/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +73 -0
  10. package/lib/Carousel/CarouselStepTracker/CarouselStepTracker.js +56 -0
  11. package/lib/Carousel/CarouselStepTracker/index.js +13 -0
  12. package/lib/Carousel/CarouselTabs/CarouselTabs.js +70 -0
  13. package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +95 -0
  14. package/lib/Carousel/CarouselTabs/CarouselTabsPanelItem.js +148 -0
  15. package/lib/Carousel/CarouselTabs/index.js +13 -0
  16. package/lib/Carousel/CarouselThumbnail.js +99 -0
  17. package/lib/Carousel/CarouselThumbnailNavigation.js +87 -0
  18. package/lib/Carousel/dictionary.js +4 -2
  19. package/lib/Carousel/index.js +10 -1
  20. package/lib/Checkbox/Checkbox.js +7 -3
  21. package/lib/Checkbox/CheckboxGroup.js +8 -1
  22. package/lib/Feedback/Feedback.js +18 -10
  23. package/lib/Icon/IconText.js +6 -1
  24. package/lib/InputLabel/InputLabel.js +11 -5
  25. package/lib/Link/InlinePressable.js +1 -8
  26. package/lib/Link/LinkBase.js +13 -10
  27. package/lib/List/ListItem.js +8 -4
  28. package/lib/Notification/Notification.js +44 -24
  29. package/lib/Pagination/Pagination.js +7 -3
  30. package/lib/Radio/RadioGroup.js +8 -0
  31. package/lib/RadioCard/RadioCard.js +6 -1
  32. package/lib/RadioCard/RadioCardGroup.js +7 -0
  33. package/lib/Select/Select.js +7 -3
  34. package/lib/SkipLink/SkipLink.js +216 -0
  35. package/lib/SkipLink/index.js +13 -0
  36. package/lib/StepTracker/Step.js +8 -4
  37. package/lib/StepTracker/StepTracker.js +7 -3
  38. package/lib/Tabs/TabsItem.js +4 -0
  39. package/lib/TextInput/TextInputBase.js +7 -3
  40. package/lib/ThemeProvider/ThemeProvider.js +25 -3
  41. package/lib/ThemeProvider/utils/styles.js +8 -1
  42. package/lib/ThemeProvider/utils/theme-tokens.js +1 -1
  43. package/lib/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  44. package/lib/Typography/Typography.js +6 -2
  45. package/lib/index.js +9 -0
  46. package/lib-module/BaseProvider/index.js +7 -2
  47. package/lib-module/Button/ButtonBase.js +41 -9
  48. package/lib-module/Button/ButtonGroup.js +7 -0
  49. package/lib-module/Button/propTypes.js +17 -0
  50. package/lib-module/Carousel/Carousel.js +80 -57
  51. package/lib-module/Carousel/CarouselContext.js +21 -8
  52. package/lib-module/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +51 -0
  53. package/lib-module/Carousel/CarouselStepTracker/CarouselStepTracker.js +42 -0
  54. package/lib-module/Carousel/CarouselStepTracker/index.js +2 -0
  55. package/lib-module/Carousel/CarouselTabs/CarouselTabs.js +50 -0
  56. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +76 -0
  57. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanelItem.js +126 -0
  58. package/lib-module/Carousel/CarouselTabs/index.js +2 -0
  59. package/lib-module/Carousel/CarouselThumbnail.js +85 -0
  60. package/lib-module/Carousel/CarouselThumbnailNavigation.js +66 -0
  61. package/lib-module/Carousel/dictionary.js +4 -2
  62. package/lib-module/Carousel/index.js +2 -1
  63. package/lib-module/Checkbox/Checkbox.js +8 -4
  64. package/lib-module/Checkbox/CheckboxGroup.js +8 -1
  65. package/lib-module/Feedback/Feedback.js +19 -11
  66. package/lib-module/Icon/IconText.js +6 -1
  67. package/lib-module/InputLabel/InputLabel.js +12 -6
  68. package/lib-module/Link/InlinePressable.js +1 -8
  69. package/lib-module/Link/LinkBase.js +14 -11
  70. package/lib-module/List/ListItem.js +9 -5
  71. package/lib-module/Notification/Notification.js +46 -26
  72. package/lib-module/Pagination/Pagination.js +8 -4
  73. package/lib-module/Radio/RadioGroup.js +8 -0
  74. package/lib-module/RadioCard/RadioCard.js +7 -2
  75. package/lib-module/RadioCard/RadioCardGroup.js +7 -0
  76. package/lib-module/Select/Select.js +8 -4
  77. package/lib-module/SkipLink/SkipLink.js +188 -0
  78. package/lib-module/SkipLink/index.js +2 -0
  79. package/lib-module/StepTracker/Step.js +9 -5
  80. package/lib-module/StepTracker/StepTracker.js +8 -4
  81. package/lib-module/Tabs/TabsItem.js +5 -1
  82. package/lib-module/TextInput/TextInputBase.js +8 -4
  83. package/lib-module/ThemeProvider/ThemeProvider.js +24 -3
  84. package/lib-module/ThemeProvider/utils/styles.js +8 -1
  85. package/lib-module/ThemeProvider/utils/theme-tokens.js +1 -1
  86. package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  87. package/lib-module/Typography/Typography.js +7 -3
  88. package/lib-module/index.js +1 -0
  89. package/package.json +46 -47
  90. package/src/BaseProvider/index.jsx +6 -3
  91. package/src/Button/ButtonBase.jsx +36 -12
  92. package/src/Button/ButtonGroup.jsx +6 -0
  93. package/src/Button/propTypes.js +14 -0
  94. package/src/Carousel/Carousel.jsx +91 -64
  95. package/src/Carousel/CarouselContext.jsx +29 -5
  96. package/src/Carousel/CarouselFirstFocus/CarouselFirstFocus.jsx +49 -0
  97. package/src/Carousel/CarouselStepTracker/CarouselStepTracker.jsx +36 -0
  98. package/src/Carousel/CarouselStepTracker/index.js +3 -0
  99. package/src/Carousel/CarouselTabs/CarouselTabs.jsx +37 -0
  100. package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +69 -0
  101. package/src/Carousel/CarouselTabs/CarouselTabsPanelItem.jsx +119 -0
  102. package/src/Carousel/CarouselTabs/index.js +3 -0
  103. package/src/Carousel/CarouselThumbnail.jsx +77 -0
  104. package/src/Carousel/CarouselThumbnailNavigation.jsx +53 -0
  105. package/src/Carousel/dictionary.js +4 -2
  106. package/src/Carousel/index.js +1 -0
  107. package/src/Checkbox/Checkbox.jsx +14 -11
  108. package/src/Checkbox/CheckboxGroup.jsx +8 -1
  109. package/src/Feedback/Feedback.jsx +14 -7
  110. package/src/Icon/IconText.jsx +3 -1
  111. package/src/InputLabel/InputLabel.jsx +13 -12
  112. package/src/Link/InlinePressable.jsx +2 -8
  113. package/src/Link/LinkBase.jsx +18 -21
  114. package/src/List/ListItem.jsx +10 -5
  115. package/src/Notification/Notification.jsx +40 -23
  116. package/src/Pagination/Pagination.jsx +6 -4
  117. package/src/Radio/RadioGroup.jsx +7 -0
  118. package/src/RadioCard/RadioCard.jsx +3 -2
  119. package/src/RadioCard/RadioCardGroup.jsx +6 -0
  120. package/src/Select/Select.jsx +12 -3
  121. package/src/SkipLink/SkipLink.jsx +179 -0
  122. package/src/SkipLink/index.js +3 -0
  123. package/src/StepTracker/Step.jsx +12 -4
  124. package/src/StepTracker/StepTracker.jsx +11 -10
  125. package/src/Tabs/TabsItem.jsx +3 -2
  126. package/src/TextInput/TextInputBase.jsx +11 -3
  127. package/src/ThemeProvider/ThemeProvider.jsx +22 -3
  128. package/src/ThemeProvider/utils/styles.js +9 -1
  129. package/src/ThemeProvider/utils/theme-tokens.js +1 -1
  130. package/src/ToggleSwitch/ToggleSwitchGroup.jsx +6 -0
  131. package/src/Typography/Typography.jsx +11 -12
  132. package/src/index.js +1 -0
@@ -11,15 +11,17 @@ import {
11
11
  selectSystemProps,
12
12
  a11yProps,
13
13
  viewProps,
14
- useCopy
14
+ useCopy,
15
+ unpackFragment
15
16
  } from '../utils'
16
17
  import { useA11yInfo } from '../A11yInfoProvider'
17
18
  import { CarouselProvider } from './CarouselContext'
18
19
  import CarouselItem from './CarouselItem'
19
- import StepTracker from '../StepTracker'
20
- import StackView from '../StackView'
21
20
  import IconButton from '../IconButton'
22
-
21
+ import SkipLink from '../SkipLink'
22
+ import A11yText from '../A11yText'
23
+ import CarouselStepTracker from './CarouselStepTracker'
24
+ import CarouselThumbnailNavigation from './CarouselThumbnailNavigation'
23
25
  import dictionary from './dictionary'
24
26
 
25
27
  const staticStyles = StyleSheet.create({
@@ -33,19 +35,6 @@ const staticStyles = StyleSheet.create({
33
35
  }
34
36
  })
35
37
 
36
- const staticTokens = {
37
- stackView: {
38
- justifyContent: 'center'
39
- },
40
- stepTracker: {
41
- showStepLabel: false,
42
- showStepTrackerLabel: true,
43
- knobCompletedBackgroundColor: 'none',
44
- connectorCompletedColor: 'none',
45
- connectorColor: 'none'
46
- }
47
- }
48
-
49
38
  const selectContainerStyles = (width) => ({
50
39
  backgroundColor: 'transparent',
51
40
  overflow: 'hidden',
@@ -160,27 +149,36 @@ const Carousel = React.forwardRef(
160
149
  onAnimationStart,
161
150
  onAnimationEnd,
162
151
  onIndexChanged,
152
+ skipLinkHref,
153
+ refocus,
154
+ title = 'carousel',
163
155
  springConfig = undefined,
164
- onRenderPanelNavigation,
156
+ thumbnails = undefined,
157
+ panelNavigation = thumbnails ? (
158
+ <CarouselThumbnailNavigation thumbnails={thumbnails} />
159
+ ) : (
160
+ <CarouselStepTracker />
161
+ ),
165
162
  tag = 'ul',
166
- accessibilityRole = 'adjustable',
167
- accessibilityLabel = 'carousel',
163
+ accessibilityRole,
164
+ accessibilityLabel = title,
165
+ accessibilityLiveRegion = 'polite',
168
166
  copy,
169
167
  ...rest
170
168
  },
171
169
  ref
172
170
  ) => {
173
171
  const viewport = useViewport()
172
+ const themeTokens = useThemeTokens('Carousel', tokens, variant, {
173
+ viewport
174
+ })
174
175
  const {
175
176
  previousIcon,
176
177
  nextIcon,
177
178
  showPreviousNextNavigation,
178
179
  showPanelNavigation,
179
- spaceBetweenSlideAndPreviousNextNavigation,
180
- spaceBetweenSlideAndPanelNavigation
181
- } = useThemeTokens('Carousel', tokens, variant, {
182
- viewport
183
- })
180
+ spaceBetweenSlideAndPreviousNextNavigation
181
+ } = themeTokens
184
182
  const [activeIndex, setActiveIndex] = React.useState(0)
185
183
 
186
184
  const [isAnimating, setIsAnimating] = React.useState(false)
@@ -201,7 +199,7 @@ const Carousel = React.forwardRef(
201
199
 
202
200
  const getCopy = useCopy({ dictionary, copy })
203
201
 
204
- const childrenArray = React.Children.toArray(children)
202
+ const childrenArray = unpackFragment(children)
205
203
  const systemProps = selectProps({
206
204
  ...rest,
207
205
  accessibilityRole,
@@ -220,15 +218,12 @@ const Carousel = React.forwardRef(
220
218
  })
221
219
  const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
222
220
  React.useState(0)
221
+ const firstFocusRef = React.useRef(null)
223
222
  const pan = React.useRef(new Animated.ValueXY()).current
224
223
  const animatedX = React.useRef(0)
225
224
  const animatedY = React.useRef(0)
226
225
  const isFirstSlide = !activeIndex
227
226
  const isLastSlide = activeIndex + 1 >= children.length
228
- const panelNavigationTokens = {
229
- ...staticTokens.stepTracker,
230
- containerPaddingTop: spaceBetweenSlideAndPanelNavigation
231
- }
232
227
 
233
228
  const onContainerLayout = ({
234
229
  nativeEvent: {
@@ -308,8 +303,9 @@ const Carousel = React.forwardRef(
308
303
  updateOffset()
309
304
  handleAnimationStart(activeIndex)
310
305
  updateIndex(delta)
306
+ if (refocus) firstFocusRef.current?.focus()
311
307
  },
312
- [updateIndex, updateOffset, activeIndex, handleAnimationStart]
308
+ [updateIndex, updateOffset, activeIndex, handleAnimationStart, refocus]
313
309
  )
314
310
 
315
311
  const goToNeighboring = React.useCallback(
@@ -405,22 +401,31 @@ const Carousel = React.forwardRef(
405
401
  // Related discussion - https://github.com/telus/universal-design-system/issues/1549
406
402
  const previousNextIconButtonVariants = { size: previousNextIconSize, raised: true }
407
403
 
408
- const getCopyWithPlaceholders = (copyKey) => {
409
- const copyText = getCopy(copyKey)
410
- .replace(/%\{itemLabel\}/g, itemLabel)
411
- .replace(/%\{stepNumber\}/g, activeIndex + 1)
412
- .replace(/%\{stepCount\}/g, childrenArray.length)
404
+ const getCopyWithPlaceholders = React.useCallback(
405
+ (copyKey) => {
406
+ const copyText = getCopy(copyKey)
407
+ .replace(/%\{title\}/g, title)
408
+ .replace(/%\{itemLabel\}/g, itemLabel)
409
+ .replace(/%\{stepNumber\}/g, activeIndex + 1)
410
+ .replace(/%\{stepCount\}/g, childrenArray.length)
413
411
 
414
- // First word might be a lowercase placeholder: capitalize the first letter
415
- return `${copyText[0].toUpperCase()}${copyText.slice(1)}`
416
- }
412
+ // First word might be a lowercase placeholder: capitalize the first letter
413
+ return `${copyText[0].toUpperCase()}${copyText.slice(1)}`
414
+ },
415
+ [activeIndex, childrenArray.length, itemLabel, getCopy, title]
416
+ )
417
417
 
418
418
  return (
419
419
  <CarouselProvider
420
420
  activeIndex={activeIndex}
421
+ goTo={goTo}
422
+ getCopyWithPlaceholders={getCopyWithPlaceholders}
423
+ itemLabel={itemLabel}
421
424
  totalItems={childrenArray.length}
425
+ themeTokens={themeTokens}
426
+ firstFocusRef={firstFocusRef}
427
+ refocus={refocus}
422
428
  width={containerLayout.width}
423
- goTo={goTo}
424
429
  >
425
430
  <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
426
431
  {showPreviousNextNavigation && (
@@ -447,6 +452,19 @@ const Carousel = React.forwardRef(
447
452
  />
448
453
  </View>
449
454
  )}
455
+ {Boolean(skipLinkHref) && (
456
+ <SkipLink ref={firstFocusRef} href={skipLinkHref}>
457
+ {getCopyWithPlaceholders('skipLink')}
458
+ </SkipLink>
459
+ )}
460
+ <A11yText
461
+ // Read the current slide position to screen readers on slide.
462
+ // If it's set to refocus and doesn't have a SkipLink to focus to, focus this.
463
+ ref={!skipLinkHref && refocus ? firstFocusRef : null}
464
+ accessibilityLiveRegion={!skipLinkHref && refocus ? undefined : 'polite'}
465
+ focusable={!skipLinkHref && refocus}
466
+ text={getCopyWithPlaceholders('stepTrackerLabel')}
467
+ />
450
468
  <View style={selectContainerStyles(containerLayout.width)}>
451
469
  <Animated.View
452
470
  style={StyleSheet.flatten([
@@ -457,6 +475,9 @@ const Carousel = React.forwardRef(
457
475
  ])}
458
476
  {...panResponder.panHandlers}
459
477
  {...getA11yPropsFromHtmlTag(tag)}
478
+ // In iframes on Mac (e.g. in Storybook), this content may be misread or read twice.
479
+ // This is a known Voiceover bug: https://github.com/phetsims/a11y-research/issues/132
480
+ accessibilityLiveRegion={accessibilityLiveRegion}
460
481
  >
461
482
  {childrenArray.map((element, index) => {
462
483
  const hidden = !isAnimating && index !== activeIndex
@@ -490,24 +511,7 @@ const Carousel = React.forwardRef(
490
511
  </View>
491
512
  )}
492
513
  </View>
493
- {showPanelNavigation ? (
494
- <StackView direction="row" tokens={staticTokens.stackView}>
495
- {onRenderPanelNavigation ? (
496
- onRenderPanelNavigation({ activeIndex, totalItems: childrenArray.length })
497
- ) : (
498
- <StepTracker
499
- current={activeIndex}
500
- steps={childrenArray.map((_, index) => String(index))}
501
- copy={{
502
- // Give StepTracker copy from Carousel's language and dictionary
503
- stepLabel: getCopyWithPlaceholders('stepLabel'),
504
- stepTrackerLabel: getCopyWithPlaceholders('stepTrackerLabel')
505
- }}
506
- tokens={panelNavigationTokens}
507
- />
508
- )}
509
- </StackView>
510
- ) : null}
514
+ {showPanelNavigation ? panelNavigation : null}
511
515
  </CarouselProvider>
512
516
  )
513
517
  }
@@ -542,6 +546,16 @@ Carousel.propTypes = {
542
546
  * Carousel uses `Animated.spring` to animate slide changes, use this option to pass custom animation configuration
543
547
  */
544
548
  springConfig: PropTypes.object,
549
+ /**
550
+ * An array of objects containing information on the thumbnails to be rendered as navigation panel
551
+ */
552
+ thumbnails: PropTypes.arrayOf(
553
+ PropTypes.shape({
554
+ accessibilityLabel: PropTypes.string,
555
+ alt: PropTypes.string,
556
+ src: PropTypes.string
557
+ })
558
+ ),
545
559
  /**
546
560
  * Minimal part of slide width must be swiped for changing index.
547
561
  * Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index
@@ -569,20 +583,33 @@ Carousel.propTypes = {
569
583
  */
570
584
  onIndexChanged: PropTypes.func,
571
585
  /**
572
- * Use this to render a custom panel navigation element instead of dots navigation
573
- * This function is also provided with an object with the following properties
574
- * activeIndex: index of current slide
575
- * totalItems: total number of slides
586
+ * If this is a complex carousel with a lot of focusable content, pass a href for a skip link. Typically, this will be an anchor link
587
+ * with the ID of a focusable element immediately after the Carousel, e.g. `'#section-2-heading'`.
588
+ */
589
+ skipLinkHref: PropTypes.string,
590
+ /**
591
+ * If true, whenever a new slide comes into view, the focus of the Carousel switches to the start.
592
+ *
593
+ * Pass this as true when using carousel items that contain interactive content, so a user can easily tab into that content.
594
+ *
595
+ * If skipLinkHref is passed, the focus target will be the SkipLink; if not, it'll be an empty element before the slide content.
596
+ */
597
+ refocus: PropTypes.bool,
598
+ /**
599
+ * Use this to render a custom panel navigation element instead of the default StepTracker's based navigation
600
+ * You can make use of `useCarousel` within your custom panel navigation component to hook into various Carousel states such as:
601
+ * - activeIndex: index of current slide
602
+ * - totalItems: total number of slides
576
603
  * Use it as follows:
577
604
  * ```js
578
605
  * <Carousel
579
- * onRenderPanelNavigation={({ totalItems, activeIndex }) => <Text>Showing {activeIndex + 1}</Text>}
606
+ * panelNavigation={<CustomPanelNavigation />}
580
607
  * >
581
608
  * <Carousel.Item>First Slide</Carousel.Item>
582
609
  * </Carousel>
583
610
  * ```
584
611
  */
585
- onRenderPanelNavigation: PropTypes.func,
612
+ panelNavigation: PropTypes.element,
586
613
  /**
587
614
  * When slide animation start
588
615
  * This function is also provided with a parameter indicating the current slide index before animation starts
@@ -1,12 +1,32 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
+ import { getTokensPropType } from '../utils'
3
4
 
4
5
  const CarouselContext = React.createContext()
5
6
 
6
- const CarouselProvider = ({ children, activeIndex, totalItems, width, goTo }) => {
7
+ const CarouselProvider = ({
8
+ activeIndex,
9
+ children,
10
+ goTo,
11
+ getCopyWithPlaceholders,
12
+ itemLabel,
13
+ refocus = false,
14
+ themeTokens,
15
+ totalItems,
16
+ width
17
+ }) => {
7
18
  const value = React.useMemo(
8
- () => ({ activeIndex, totalItems, width, goTo }),
9
- [activeIndex, totalItems, width, goTo]
19
+ () => ({
20
+ activeIndex,
21
+ goTo,
22
+ getCopyWithPlaceholders,
23
+ itemLabel,
24
+ refocus,
25
+ themeTokens,
26
+ totalItems,
27
+ width
28
+ }),
29
+ [activeIndex, goTo, getCopyWithPlaceholders, itemLabel, refocus, totalItems, themeTokens, width]
10
30
  )
11
31
  return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
12
32
  }
@@ -22,9 +42,13 @@ function useCarousel() {
22
42
  CarouselProvider.propTypes = {
23
43
  children: PropTypes.arrayOf(PropTypes.element).isRequired,
24
44
  activeIndex: PropTypes.number.isRequired,
45
+ goTo: PropTypes.func.isRequired,
46
+ getCopyWithPlaceholders: PropTypes.func.isRequired,
47
+ itemLabel: PropTypes.string.isRequired,
48
+ refocus: PropTypes.bool,
49
+ themeTokens: getTokensPropType('Carousel'),
25
50
  totalItems: PropTypes.number.isRequired,
26
- width: PropTypes.number.isRequired,
27
- goTo: PropTypes.func.isRequired
51
+ width: PropTypes.number.isRequired
28
52
  }
29
53
 
30
54
  export { CarouselProvider, useCarousel }
@@ -0,0 +1,49 @@
1
+ import React, { forwardRef } from 'react'
2
+ import { Pressable, Platform, StyleSheet } from 'react-native'
3
+ import { PropTypes } from 'prop-types'
4
+
5
+ import { useCarousel } from '../CarouselContext'
6
+
7
+ /**
8
+ * Focus target so that when a new slide is shown, the user can tab into
9
+ * its content using the keyboard.
10
+ *
11
+ * @TODO rework this after integrating with SkipLink when available.
12
+ */
13
+ const CarouselFirstFocus = forwardRef(({ title }, ref) => {
14
+ const { getCopyWithPlaceholders } = useCarousel()
15
+
16
+ // TODO: integrate skip link description if behaving as skip link.
17
+ // Consider moving this content to aria-live area while only the skip link is focused.
18
+ const accessibilityLabel = `${title}, ${getCopyWithPlaceholders('stepTrackerLabel')}`
19
+
20
+ const accessibilityRole = Platform.select({
21
+ web: 'link', // The focused item will ultimately be a skip link.
22
+ default: 'button' // 'link' role usually denotes opening browser on Native.
23
+ })
24
+
25
+ return (
26
+ <Pressable
27
+ // TODO: integrate skip link functionality, jump focus to after Carousel
28
+ onPress={undefined}
29
+ ref={ref}
30
+ accessibilityLabel={accessibilityLabel}
31
+ accessibilityRole={accessibilityRole}
32
+ style={StyleSheet.absoluteFill}
33
+ focusable
34
+ />
35
+ )
36
+ })
37
+
38
+ CarouselFirstFocus.displayName = 'CarouselFirstFocus'
39
+ CarouselFirstFocus.propTypes = {
40
+ /**
41
+ * Simple description of this carousel for screenreaders, to be read before
42
+ * "{itemLabel} {index} of {count}.
43
+ *
44
+ * For example, "Summer offers" in "Summer offers, offer 1 of 3"
45
+ */
46
+ title: PropTypes.string
47
+ }
48
+
49
+ export default CarouselFirstFocus
@@ -0,0 +1,36 @@
1
+ import React from 'react'
2
+ import { useCarousel } from '../CarouselContext'
3
+ import StepTracker from '../../StepTracker'
4
+ import StackView from '../../StackView'
5
+
6
+ const CarouselStepTracker = () => {
7
+ const { activeIndex, totalItems, getCopyWithPlaceholders, themeTokens } = useCarousel()
8
+ const stackViewTokens = {
9
+ justifyContent: 'center'
10
+ }
11
+ const stepTrackerTokens = {
12
+ showStepLabel: false,
13
+ showStepTrackerLabel: true,
14
+ knobCompletedBackgroundColor: 'none',
15
+ connectorCompletedColor: 'none',
16
+ connectorColor: 'none',
17
+ containerPaddingTop: themeTokens.spaceBetweenSlideAndPanelNavigation
18
+ }
19
+ const steps = Array.from(Array(totalItems)).map((_, index) => String(index))
20
+ return (
21
+ <StackView direction="row" tokens={stackViewTokens}>
22
+ <StepTracker
23
+ current={activeIndex}
24
+ steps={steps}
25
+ copy={{
26
+ // Give StepTracker copy from Carousel's language and dictionary
27
+ stepLabel: getCopyWithPlaceholders('stepLabel'),
28
+ stepTrackerLabel: getCopyWithPlaceholders('stepTrackerLabel')
29
+ }}
30
+ tokens={stepTrackerTokens}
31
+ />
32
+ </StackView>
33
+ )
34
+ }
35
+
36
+ export default CarouselStepTracker
@@ -0,0 +1,3 @@
1
+ import CarouselStepTracker from './CarouselStepTracker'
2
+
3
+ export default CarouselStepTracker
@@ -0,0 +1,37 @@
1
+ import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { useResponsiveProp } from '../../utils'
5
+ import Carousel from '../Carousel'
6
+ import CarouselTabsPanel from './CarouselTabsPanel'
7
+
8
+ const CarouselTabs = forwardRef(({ items, refocus = true, ...carouselProps }, ref) => {
9
+ const panelNavigation = useResponsiveProp({ md: <CarouselTabsPanel items={items} /> })
10
+
11
+ return (
12
+ <Carousel refocus={refocus} {...carouselProps} ref={ref} panelNavigation={panelNavigation}>
13
+ {items.map(({ title, content }) => (
14
+ <React.Fragment key={title}>{content}</React.Fragment>
15
+ ))}
16
+ </Carousel>
17
+ )
18
+ })
19
+
20
+ CarouselTabs.displayName = 'CarouselTabs'
21
+ CarouselTabs.propTypes = {
22
+ /**
23
+ * An array of objects where title is the string shown in the slide's tab and content is the JSX for the slide itself
24
+ */
25
+ items: PropTypes.arrayOf(
26
+ PropTypes.shape({
27
+ title: PropTypes.string.isRequired,
28
+ content: PropTypes.node.isRequired
29
+ })
30
+ ),
31
+ ...Carousel.propTypes
32
+ }
33
+ // CarouselTabs doesn't require `children` prop, it uses `items` instead.
34
+ // eslint-disable-next-line react/forbid-foreign-prop-types
35
+ if (CarouselTabs.propTypes?.children) delete CarouselTabs.propTypes?.children
36
+
37
+ export default CarouselTabs
@@ -0,0 +1,69 @@
1
+ import React, { forwardRef, useRef } from 'react'
2
+ import { View } from 'react-native'
3
+
4
+ import PropTypes from 'prop-types'
5
+ import StackView from '../../StackView'
6
+
7
+ import { useCarousel } from '../CarouselContext'
8
+ import CarouselTabsPanelItem from './CarouselTabsPanelItem'
9
+
10
+ const CarouselTabsPanel = forwardRef(({ items }, ref) => {
11
+ const { activeIndex, goTo } = useCarousel()
12
+ const nextFocusRef = useRef()
13
+ const firstTabRef = useRef()
14
+
15
+ // TODO: figure out a better cross-brand way to specify subcomponent variants.
16
+ // For now, this picks an Allium variant, and does nothing in brands that lack it.
17
+ // See similar comment in Carousel and https://github.com/telus/universal-design-system/issues/1549
18
+ const dividerVariant = { decorative: true }
19
+
20
+ const lastTabSelected = activeIndex === items.length - 1
21
+
22
+ return (
23
+ <>
24
+ <View
25
+ focusable
26
+ accessible
27
+ onFocus={(event) => {
28
+ // When user forward-tabs into this section, focus the next tab; if they backwards-tab
29
+ // (shift-tab) back into the carousel content, don't interfere.
30
+ const previousWebFocus = event.relatedTarget
31
+ if (previousWebFocus !== firstTabRef.current) nextFocusRef.current.focus()
32
+ }}
33
+ />
34
+ <StackView direction="row" space={3} divider={{ variant: dividerVariant }} ref={ref}>
35
+ {items.map(({ title, onPress, ...panelItemProps }, index) => {
36
+ const selected = index === activeIndex
37
+ const isNext = index === activeIndex + 1
38
+
39
+ // Selected item should be always unfocusable and unpressable
40
+ const handlePress = selected
41
+ ? undefined
42
+ : (event) => {
43
+ if (typeof onPress === 'function') onPress(event, index)
44
+ goTo(index)
45
+ }
46
+
47
+ return (
48
+ <CarouselTabsPanelItem
49
+ ref={(isNext && nextFocusRef) || (index === 0 && firstTabRef) || null}
50
+ key={title}
51
+ title={title}
52
+ selected={selected}
53
+ onPress={handlePress}
54
+ {...panelItemProps}
55
+ />
56
+ )
57
+ })}
58
+ </StackView>
59
+ {/* TODO: integrate with skiplink, replace this with focusing skiplink target */}
60
+ <View focusable accessible ref={lastTabSelected ? nextFocusRef : null} />
61
+ </>
62
+ )
63
+ })
64
+ CarouselTabsPanel.displayName = 'CarouselTabsPanel'
65
+ CarouselTabsPanel.propTypes = {
66
+ items: PropTypes.arrayOf(PropTypes.shape(CarouselTabsPanelItem.propTypes || {}))
67
+ }
68
+
69
+ export default CarouselTabsPanel
@@ -0,0 +1,119 @@
1
+ import React, { forwardRef, useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Pressable, Text, Platform } from 'react-native'
4
+
5
+ import { applyTextStyles, useThemeTokensCallback } from '../../ThemeProvider'
6
+ import {
7
+ selectSystemProps,
8
+ a11yProps,
9
+ pressProps,
10
+ viewProps,
11
+ textProps,
12
+ getTokensPropType,
13
+ variantProp,
14
+ resolvePressableState
15
+ } from '../../utils'
16
+
17
+ const [selectPressProps, pressPropTypes] = selectSystemProps([a11yProps, pressProps, viewProps])
18
+ const [selectTextProps, textPropTypes] = selectSystemProps([textProps])
19
+
20
+ const selectContainerStyles = ({
21
+ paddingLeft,
22
+ paddingRight,
23
+ paddingTop,
24
+ paddingBottom = 0,
25
+ borderBottomColor,
26
+ borderBottomWidth = 0,
27
+ borderBottomStyle,
28
+ flex,
29
+ alignItems,
30
+ justifyContent
31
+ }) => ({
32
+ paddingLeft,
33
+ paddingRight,
34
+ paddingTop,
35
+ paddingBottom: paddingBottom - borderBottomWidth,
36
+ borderBottomColor,
37
+ borderBottomWidth,
38
+ borderBottomStyle,
39
+ flex,
40
+ alignItems,
41
+ justifyContent,
42
+ ...Platform.select({
43
+ // Removes the default browser :focus outline
44
+ web: { outline: 'none' }
45
+ })
46
+ })
47
+
48
+ const selectTextStyles = ({
49
+ fontSize,
50
+ fontScaleCap,
51
+ lineHeight,
52
+ letterSpacing,
53
+ fontWeight,
54
+ fontName,
55
+ color
56
+ }) =>
57
+ applyTextStyles({
58
+ fontSize,
59
+ fontScaleCap,
60
+ lineHeight,
61
+ letterSpacing,
62
+ fontWeight,
63
+ fontName,
64
+ color
65
+ })
66
+
67
+ const CarouselTabsPanelItem = forwardRef(
68
+ ({ title, selected, inactive, variant, tokens, accessibilityRole = 'tab', ...rest }, ref) => {
69
+ // Workaround for React Native Web https://github.com/necolas/react-native-web/issues/2357
70
+ // Don't allow disabled to be set while focus is true else focus state gets locked `true`
71
+ // (must refocus _after_ calling `goTo`, else focus target content is not up to date)
72
+ const [isFocused, setIsFocused] = useState(false)
73
+ const disabled = (inactive || selected) && !isFocused
74
+
75
+ const getTokens = useThemeTokensCallback('CarouselTabsPanelItem', tokens, variant)
76
+ const resolveTokens = (pressState) => getTokens(resolvePressableState(pressState, { selected }))
77
+
78
+ const getContainerStyle = (pressState) => selectContainerStyles(resolveTokens(pressState))
79
+
80
+ const getTextStyle = (pressState) => selectTextStyles(resolveTokens(pressState))
81
+
82
+ const { onPress, ...selectedPressProps } = selectPressProps(rest)
83
+ const handleKeyDown = (event) => {
84
+ // Allow using the spacebar for navigation
85
+ if (event?.key === ' ') onPress(event)
86
+ }
87
+
88
+ return (
89
+ <Pressable
90
+ onPress={onPress}
91
+ style={getContainerStyle}
92
+ accessibilityRole={accessibilityRole}
93
+ ref={ref}
94
+ onKeyDown={handleKeyDown}
95
+ onFocus={() => setIsFocused(true)}
96
+ onBlur={() => setIsFocused(false)}
97
+ disabled={disabled}
98
+ {...selectedPressProps}
99
+ >
100
+ {(pressState) => (
101
+ <Text style={getTextStyle(pressState)} {...selectTextProps(rest)}>
102
+ {title}
103
+ </Text>
104
+ )}
105
+ </Pressable>
106
+ )
107
+ }
108
+ )
109
+ CarouselTabsPanelItem.displayName = 'CarouselTabsPanelItem'
110
+ CarouselTabsPanelItem.propTypes = {
111
+ ...pressPropTypes,
112
+ ...textPropTypes,
113
+ title: PropTypes.string.isRequired,
114
+ selected: PropTypes.bool,
115
+ tokens: getTokensPropType('CarouselTabsPanelItem'),
116
+ variant: variantProp.propType
117
+ }
118
+
119
+ export default CarouselTabsPanelItem
@@ -0,0 +1,3 @@
1
+ import CarouselTabs from './CarouselTabs'
2
+
3
+ export default CarouselTabs