@telus-uds/components-base 1.12.1 → 1.14.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 (84) hide show
  1. package/CHANGELOG.md +41 -2
  2. package/component-docs.json +888 -66
  3. package/lib/Button/ButtonBase.js +36 -7
  4. package/lib/Button/ButtonGroup.js +7 -0
  5. package/lib/Button/propTypes.js +18 -0
  6. package/lib/Carousel/Carousel.js +69 -12
  7. package/lib/Carousel/CarouselContext.js +17 -11
  8. package/lib/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +73 -0
  9. package/lib/Carousel/CarouselTabs/CarouselTabs.js +70 -0
  10. package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +95 -0
  11. package/lib/Carousel/CarouselTabs/CarouselTabsPanelItem.js +148 -0
  12. package/lib/Carousel/CarouselTabs/index.js +13 -0
  13. package/lib/Carousel/CarouselThumbnail.js +99 -0
  14. package/lib/Carousel/CarouselThumbnailNavigation.js +87 -0
  15. package/lib/Carousel/dictionary.js +4 -2
  16. package/lib/Carousel/index.js +10 -1
  17. package/lib/Checkbox/CheckboxGroup.js +7 -0
  18. package/lib/Icon/IconText.js +1 -1
  19. package/lib/Link/InlinePressable.js +1 -8
  20. package/lib/Link/LinkBase.js +6 -7
  21. package/lib/List/ListItem.js +1 -1
  22. package/lib/Notification/Notification.js +37 -22
  23. package/lib/Radio/RadioGroup.js +8 -0
  24. package/lib/RadioCard/RadioCardGroup.js +7 -0
  25. package/lib/SkipLink/SkipLink.js +216 -0
  26. package/lib/SkipLink/index.js +13 -0
  27. package/lib/ThemeProvider/ThemeProvider.js +6 -1
  28. package/lib/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  29. package/lib/index.js +9 -0
  30. package/lib-module/Button/ButtonBase.js +35 -7
  31. package/lib-module/Button/ButtonGroup.js +7 -0
  32. package/lib-module/Button/propTypes.js +17 -0
  33. package/lib-module/Carousel/Carousel.js +66 -11
  34. package/lib-module/Carousel/CarouselContext.js +17 -11
  35. package/lib-module/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +51 -0
  36. package/lib-module/Carousel/CarouselTabs/CarouselTabs.js +50 -0
  37. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +76 -0
  38. package/lib-module/Carousel/CarouselTabs/CarouselTabsPanelItem.js +126 -0
  39. package/lib-module/Carousel/CarouselTabs/index.js +2 -0
  40. package/lib-module/Carousel/CarouselThumbnail.js +85 -0
  41. package/lib-module/Carousel/CarouselThumbnailNavigation.js +66 -0
  42. package/lib-module/Carousel/dictionary.js +4 -2
  43. package/lib-module/Carousel/index.js +2 -1
  44. package/lib-module/Checkbox/CheckboxGroup.js +7 -0
  45. package/lib-module/Icon/IconText.js +1 -1
  46. package/lib-module/Link/InlinePressable.js +1 -8
  47. package/lib-module/Link/LinkBase.js +6 -7
  48. package/lib-module/List/ListItem.js +1 -1
  49. package/lib-module/Notification/Notification.js +38 -23
  50. package/lib-module/Radio/RadioGroup.js +8 -0
  51. package/lib-module/RadioCard/RadioCardGroup.js +7 -0
  52. package/lib-module/SkipLink/SkipLink.js +188 -0
  53. package/lib-module/SkipLink/index.js +2 -0
  54. package/lib-module/ThemeProvider/ThemeProvider.js +5 -1
  55. package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +7 -0
  56. package/lib-module/index.js +1 -0
  57. package/package.json +46 -47
  58. package/src/Button/ButtonBase.jsx +28 -9
  59. package/src/Button/ButtonGroup.jsx +6 -0
  60. package/src/Button/propTypes.js +14 -0
  61. package/src/Carousel/Carousel.jsx +68 -10
  62. package/src/Carousel/CarouselContext.jsx +22 -9
  63. package/src/Carousel/CarouselFirstFocus/CarouselFirstFocus.jsx +49 -0
  64. package/src/Carousel/CarouselTabs/CarouselTabs.jsx +37 -0
  65. package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +69 -0
  66. package/src/Carousel/CarouselTabs/CarouselTabsPanelItem.jsx +119 -0
  67. package/src/Carousel/CarouselTabs/index.js +3 -0
  68. package/src/Carousel/CarouselThumbnail.jsx +77 -0
  69. package/src/Carousel/CarouselThumbnailNavigation.jsx +53 -0
  70. package/src/Carousel/dictionary.js +4 -2
  71. package/src/Carousel/index.js +1 -0
  72. package/src/Checkbox/CheckboxGroup.jsx +7 -0
  73. package/src/Icon/IconText.jsx +1 -1
  74. package/src/Link/InlinePressable.jsx +2 -8
  75. package/src/Link/LinkBase.jsx +8 -17
  76. package/src/List/ListItem.jsx +1 -1
  77. package/src/Notification/Notification.jsx +35 -20
  78. package/src/Radio/RadioGroup.jsx +7 -0
  79. package/src/RadioCard/RadioCardGroup.jsx +6 -0
  80. package/src/SkipLink/SkipLink.jsx +179 -0
  81. package/src/SkipLink/index.js +3 -0
  82. package/src/ThemeProvider/ThemeProvider.jsx +7 -1
  83. package/src/ToggleSwitch/ToggleSwitchGroup.jsx +6 -0
  84. package/src/index.js +1 -0
@@ -11,13 +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
20
  import IconButton from '../IconButton'
20
- import CarouselStepTracker from './CarouselStepTracker/CarouselStepTracker'
21
+ import SkipLink from '../SkipLink'
22
+ import A11yText from '../A11yText'
23
+ import CarouselStepTracker from './CarouselStepTracker'
24
+ import CarouselThumbnailNavigation from './CarouselThumbnailNavigation'
21
25
  import dictionary from './dictionary'
22
26
 
23
27
  const staticStyles = StyleSheet.create({
@@ -145,11 +149,20 @@ const Carousel = React.forwardRef(
145
149
  onAnimationStart,
146
150
  onAnimationEnd,
147
151
  onIndexChanged,
152
+ skipLinkHref,
153
+ refocus,
154
+ title = 'carousel',
148
155
  springConfig = undefined,
149
- panelNavigation = <CarouselStepTracker />,
156
+ thumbnails = undefined,
157
+ panelNavigation = thumbnails ? (
158
+ <CarouselThumbnailNavigation thumbnails={thumbnails} />
159
+ ) : (
160
+ <CarouselStepTracker />
161
+ ),
150
162
  tag = 'ul',
151
- accessibilityRole = 'adjustable',
152
- accessibilityLabel = 'carousel',
163
+ accessibilityRole,
164
+ accessibilityLabel = title,
165
+ accessibilityLiveRegion = 'polite',
153
166
  copy,
154
167
  ...rest
155
168
  },
@@ -186,7 +199,7 @@ const Carousel = React.forwardRef(
186
199
 
187
200
  const getCopy = useCopy({ dictionary, copy })
188
201
 
189
- const childrenArray = React.Children.toArray(children)
202
+ const childrenArray = unpackFragment(children)
190
203
  const systemProps = selectProps({
191
204
  ...rest,
192
205
  accessibilityRole,
@@ -205,6 +218,7 @@ const Carousel = React.forwardRef(
205
218
  })
206
219
  const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
207
220
  React.useState(0)
221
+ const firstFocusRef = React.useRef(null)
208
222
  const pan = React.useRef(new Animated.ValueXY()).current
209
223
  const animatedX = React.useRef(0)
210
224
  const animatedY = React.useRef(0)
@@ -289,8 +303,9 @@ const Carousel = React.forwardRef(
289
303
  updateOffset()
290
304
  handleAnimationStart(activeIndex)
291
305
  updateIndex(delta)
306
+ if (refocus) firstFocusRef.current?.focus()
292
307
  },
293
- [updateIndex, updateOffset, activeIndex, handleAnimationStart]
308
+ [updateIndex, updateOffset, activeIndex, handleAnimationStart, refocus]
294
309
  )
295
310
 
296
311
  const goToNeighboring = React.useCallback(
@@ -389,6 +404,7 @@ const Carousel = React.forwardRef(
389
404
  const getCopyWithPlaceholders = React.useCallback(
390
405
  (copyKey) => {
391
406
  const copyText = getCopy(copyKey)
407
+ .replace(/%\{title\}/g, title)
392
408
  .replace(/%\{itemLabel\}/g, itemLabel)
393
409
  .replace(/%\{stepNumber\}/g, activeIndex + 1)
394
410
  .replace(/%\{stepCount\}/g, childrenArray.length)
@@ -396,17 +412,20 @@ const Carousel = React.forwardRef(
396
412
  // First word might be a lowercase placeholder: capitalize the first letter
397
413
  return `${copyText[0].toUpperCase()}${copyText.slice(1)}`
398
414
  },
399
- [activeIndex, childrenArray.length, itemLabel, getCopy]
415
+ [activeIndex, childrenArray.length, itemLabel, getCopy, title]
400
416
  )
401
417
 
402
418
  return (
403
419
  <CarouselProvider
404
420
  activeIndex={activeIndex}
405
- totalItems={childrenArray.length}
406
- width={containerLayout.width}
407
421
  goTo={goTo}
408
422
  getCopyWithPlaceholders={getCopyWithPlaceholders}
423
+ itemLabel={itemLabel}
424
+ totalItems={childrenArray.length}
409
425
  themeTokens={themeTokens}
426
+ firstFocusRef={firstFocusRef}
427
+ refocus={refocus}
428
+ width={containerLayout.width}
410
429
  >
411
430
  <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
412
431
  {showPreviousNextNavigation && (
@@ -433,6 +452,19 @@ const Carousel = React.forwardRef(
433
452
  />
434
453
  </View>
435
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
+ />
436
468
  <View style={selectContainerStyles(containerLayout.width)}>
437
469
  <Animated.View
438
470
  style={StyleSheet.flatten([
@@ -443,6 +475,9 @@ const Carousel = React.forwardRef(
443
475
  ])}
444
476
  {...panResponder.panHandlers}
445
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}
446
481
  >
447
482
  {childrenArray.map((element, index) => {
448
483
  const hidden = !isAnimating && index !== activeIndex
@@ -511,6 +546,16 @@ Carousel.propTypes = {
511
546
  * Carousel uses `Animated.spring` to animate slide changes, use this option to pass custom animation configuration
512
547
  */
513
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
+ ),
514
559
  /**
515
560
  * Minimal part of slide width must be swiped for changing index.
516
561
  * Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index
@@ -537,6 +582,19 @@ Carousel.propTypes = {
537
582
  * Caution: Always consider wrapping your callback for `onIndexChanged` in `useCallback` in order to avoid bugs and performance issues
538
583
  */
539
584
  onIndexChanged: PropTypes.func,
585
+ /**
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,
540
598
  /**
541
599
  * Use this to render a custom panel navigation element instead of the default StepTracker's based navigation
542
600
  * You can make use of `useCarousel` within your custom panel navigation component to hook into various Carousel states such as:
@@ -5,17 +5,28 @@ import { getTokensPropType } from '../utils'
5
5
  const CarouselContext = React.createContext()
6
6
 
7
7
  const CarouselProvider = ({
8
- children,
9
8
  activeIndex,
10
- totalItems,
11
- width,
9
+ children,
12
10
  goTo,
13
11
  getCopyWithPlaceholders,
14
- themeTokens
12
+ itemLabel,
13
+ refocus = false,
14
+ themeTokens,
15
+ totalItems,
16
+ width
15
17
  }) => {
16
18
  const value = React.useMemo(
17
- () => ({ activeIndex, totalItems, width, goTo, getCopyWithPlaceholders, themeTokens }),
18
- [activeIndex, totalItems, width, goTo, getCopyWithPlaceholders, themeTokens]
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]
19
30
  )
20
31
  return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
21
32
  }
@@ -31,11 +42,13 @@ function useCarousel() {
31
42
  CarouselProvider.propTypes = {
32
43
  children: PropTypes.arrayOf(PropTypes.element).isRequired,
33
44
  activeIndex: PropTypes.number.isRequired,
34
- totalItems: PropTypes.number.isRequired,
35
- width: PropTypes.number.isRequired,
36
45
  goTo: PropTypes.func.isRequired,
37
46
  getCopyWithPlaceholders: PropTypes.func.isRequired,
38
- themeTokens: getTokensPropType('Carousel')
47
+ itemLabel: PropTypes.string.isRequired,
48
+ refocus: PropTypes.bool,
49
+ themeTokens: getTokensPropType('Carousel'),
50
+ totalItems: PropTypes.number.isRequired,
51
+ width: PropTypes.number.isRequired
39
52
  }
40
53
 
41
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,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
@@ -0,0 +1,77 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { Pressable, Image } from 'react-native'
4
+ import { useCarousel } from './CarouselContext'
5
+
6
+ /**
7
+ * `Carousel.Thumbnail` is used to wrap the content of an individual slide and is suppsoed to be the
8
+ * only top-level component passed to the `Carousel`
9
+ */
10
+ const CarouselThumbnail = ({ accessibilityLabel, alt, index, src }) => {
11
+ const { activeIndex, itemLabel, totalItems, getCopyWithPlaceholders, goTo, themeTokens } =
12
+ useCarousel()
13
+ const thumbnailTitle =
14
+ alt ??
15
+ getCopyWithPlaceholders('stepTrackerLabel')
16
+ .replace(/%\{itemLabel\}/g, itemLabel)
17
+ .replace(/%\{stepNumber\}/g, index)
18
+ .replace(/%\{stepCount\}/g, totalItems)
19
+ const handlePress = () => goTo(index)
20
+ const handleKeyDown = (event) => {
21
+ // Allow using the spacebar for navigation
22
+ if (event?.key === ' ') goTo(index)
23
+ }
24
+ const {
25
+ thumbnailBorderColor,
26
+ thumbnailBorderRadius,
27
+ thumbnailBorderWidth,
28
+ thumbnailMargin,
29
+ thumbnailPadding,
30
+ thumbnailSelectedBorderColor,
31
+ thumbnailSelectedBorderWidth,
32
+ thumbnailSize
33
+ } = themeTokens
34
+ const styles = {
35
+ pressable: {
36
+ borderColor: thumbnailBorderColor,
37
+ borderRadius: thumbnailBorderRadius,
38
+ borderWidth: thumbnailBorderWidth,
39
+ margin: thumbnailMargin,
40
+ padding: thumbnailPadding
41
+ },
42
+ image: {
43
+ height: thumbnailSize,
44
+ width: thumbnailSize
45
+ },
46
+ selected: {
47
+ borderColor: thumbnailSelectedBorderColor,
48
+ borderWidth: thumbnailSelectedBorderWidth,
49
+ padding: thumbnailPadding - thumbnailSelectedBorderWidth + thumbnailBorderWidth
50
+ }
51
+ }
52
+
53
+ return (
54
+ <Pressable
55
+ key={src}
56
+ onKeyDown={handleKeyDown}
57
+ onPress={handlePress}
58
+ style={[styles.pressable, index === activeIndex && styles.selected]}
59
+ >
60
+ <Image
61
+ accessibilityIgnoresInvertColors
62
+ accessibilityLabel={accessibilityLabel ?? alt}
63
+ source={src}
64
+ style={styles.image}
65
+ title={thumbnailTitle}
66
+ />
67
+ </Pressable>
68
+ )
69
+ }
70
+ CarouselThumbnail.propTypes = {
71
+ accessibilityLabel: PropTypes.string,
72
+ alt: PropTypes.string,
73
+ index: PropTypes.number,
74
+ src: PropTypes.string
75
+ }
76
+
77
+ export default CarouselThumbnail
@@ -0,0 +1,53 @@
1
+ import React, { forwardRef } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View } from 'react-native'
4
+ import { useCarousel } from './CarouselContext'
5
+ import CarouselThumbnail from './CarouselThumbnail'
6
+ import { StackWrap } from '../StackView'
7
+
8
+ const CarouselThumbnailNavigation = forwardRef(({ thumbnails = [] }, ref) => {
9
+ const { totalItems, themeTokens } = useCarousel()
10
+ if (thumbnails.length !== totalItems) {
11
+ throw new Error('Thumbnail set provided does not match the number of slides in the carousel')
12
+ }
13
+ const { thumbnailContainerPaddingTop, thumbnailMargin } = themeTokens
14
+ const stackWrapTokens = {
15
+ justifyContent: 'flex-start'
16
+ }
17
+ const containerStyles = {
18
+ justifyContent: 'center',
19
+ alignItems: 'center',
20
+ paddingTop: thumbnailContainerPaddingTop - thumbnailMargin
21
+ }
22
+
23
+ return (
24
+ <View style={containerStyles}>
25
+ <StackWrap direction="row" tokens={stackWrapTokens} ref={ref}>
26
+ {thumbnails.map(({ accessibilityLabel, alt, src }, index) => (
27
+ <CarouselThumbnail
28
+ accessibilityLabel={accessibilityLabel}
29
+ alt={alt}
30
+ index={index}
31
+ key={src}
32
+ src={src}
33
+ />
34
+ ))}
35
+ </StackWrap>
36
+ </View>
37
+ )
38
+ })
39
+ CarouselThumbnailNavigation.displayName = 'CarouselThumbnailNavigation'
40
+ CarouselThumbnailNavigation.propTypes = {
41
+ /**
42
+ * An array of objects containing information on the thumbnail images.
43
+ */
44
+ thumbnails: PropTypes.arrayOf(
45
+ PropTypes.shape({
46
+ accessibilityLabel: PropTypes.string,
47
+ alt: PropTypes.string,
48
+ src: PropTypes.string
49
+ })
50
+ ).isRequired
51
+ }
52
+
53
+ export default CarouselThumbnailNavigation
@@ -4,13 +4,15 @@ export default {
4
4
  carouselLabel: '%{stepCount} items',
5
5
  iconButtonLabel: 'Show %{itemLabel} %{targetStep} of %{stepCount}',
6
6
  stepLabel: '%{itemLabel} %{stepNumber}',
7
- stepTrackerLabel: '%{itemLabel} %{stepNumber} of %{stepCount}'
7
+ stepTrackerLabel: '%{itemLabel} %{stepNumber} of %{stepCount}',
8
+ skipLink: 'Skip %{title}'
8
9
  },
9
10
  fr: {
10
11
  // TODO: French translations here
11
12
  carouselLabel: '(fr) %{stepCount} items',
12
13
  iconButtonLabel: '(fr) Show %{itemLabel} %{targetStep} of %{stepCount}',
13
14
  stepLabel: '(fr) %{itemLabel} %{stepNumber}',
14
- stepTrackerLabel: '(fr) %{itemLabel} %{stepNumber} of %{stepCount}'
15
+ stepTrackerLabel: '(fr) %{itemLabel} %{stepNumber} of %{stepCount}',
16
+ skipLink: '(fr) Skip %{title}'
15
17
  }
16
18
  }
@@ -1,2 +1,3 @@
1
1
  export * from './CarouselContext'
2
2
  export { default as Carousel } from './Carousel'
3
+ export { default as CarouselTabs } from './CarouselTabs'
@@ -82,6 +82,8 @@ const CheckboxGroup = forwardRef(
82
82
  legend,
83
83
  tooltip,
84
84
  hint,
85
+ hintPosition = 'inline',
86
+
85
87
  validation,
86
88
  feedback,
87
89
  initialCheckedIds,
@@ -145,6 +147,7 @@ const CheckboxGroup = forwardRef(
145
147
  legend={legend}
146
148
  tooltip={tooltip}
147
149
  hint={hint}
150
+ hintPosition={hintPosition}
148
151
  space={fieldSpace}
149
152
  feedback={feedback}
150
153
  inactive={inactive}
@@ -192,6 +195,10 @@ CheckboxGroup.propTypes = {
192
195
  * Optional additional text giving more detail to help a user make a choice.
193
196
  */
194
197
  hint: PropTypes.string,
198
+ /**
199
+ * Position of the hint relative to label. Use `below` to display a larger hint below the label.
200
+ */
201
+ hintPosition: PropTypes.oneOf(['inline', 'below']),
195
202
  /**
196
203
  * Optional tooltip text content to include alongside the legend and hint.
197
204
  */