@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.
- package/CHANGELOG.md +41 -2
- package/component-docs.json +933 -55
- package/lib/BaseProvider/index.js +7 -2
- package/lib/Button/ButtonBase.js +52 -19
- package/lib/Button/ButtonGroup.js +7 -0
- package/lib/Button/propTypes.js +18 -0
- package/lib/Carousel/Carousel.js +83 -58
- package/lib/Carousel/CarouselContext.js +22 -8
- package/lib/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +73 -0
- package/lib/Carousel/CarouselStepTracker/CarouselStepTracker.js +56 -0
- package/lib/Carousel/CarouselStepTracker/index.js +13 -0
- package/lib/Carousel/CarouselTabs/CarouselTabs.js +70 -0
- package/lib/Carousel/CarouselTabs/CarouselTabsPanel.js +95 -0
- package/lib/Carousel/CarouselTabs/CarouselTabsPanelItem.js +148 -0
- package/lib/Carousel/CarouselTabs/index.js +13 -0
- package/lib/Carousel/CarouselThumbnail.js +99 -0
- package/lib/Carousel/CarouselThumbnailNavigation.js +87 -0
- package/lib/Carousel/dictionary.js +4 -2
- package/lib/Carousel/index.js +10 -1
- package/lib/Checkbox/Checkbox.js +7 -3
- package/lib/Checkbox/CheckboxGroup.js +8 -1
- package/lib/Feedback/Feedback.js +18 -10
- package/lib/Icon/IconText.js +6 -1
- package/lib/InputLabel/InputLabel.js +11 -5
- package/lib/Link/InlinePressable.js +1 -8
- package/lib/Link/LinkBase.js +13 -10
- package/lib/List/ListItem.js +8 -4
- package/lib/Notification/Notification.js +44 -24
- package/lib/Pagination/Pagination.js +7 -3
- package/lib/Radio/RadioGroup.js +8 -0
- package/lib/RadioCard/RadioCard.js +6 -1
- package/lib/RadioCard/RadioCardGroup.js +7 -0
- package/lib/Select/Select.js +7 -3
- package/lib/SkipLink/SkipLink.js +216 -0
- package/lib/SkipLink/index.js +13 -0
- package/lib/StepTracker/Step.js +8 -4
- package/lib/StepTracker/StepTracker.js +7 -3
- package/lib/Tabs/TabsItem.js +4 -0
- package/lib/TextInput/TextInputBase.js +7 -3
- package/lib/ThemeProvider/ThemeProvider.js +25 -3
- package/lib/ThemeProvider/utils/styles.js +8 -1
- package/lib/ThemeProvider/utils/theme-tokens.js +1 -1
- package/lib/ToggleSwitch/ToggleSwitchGroup.js +7 -0
- package/lib/Typography/Typography.js +6 -2
- package/lib/index.js +9 -0
- package/lib-module/BaseProvider/index.js +7 -2
- package/lib-module/Button/ButtonBase.js +41 -9
- package/lib-module/Button/ButtonGroup.js +7 -0
- package/lib-module/Button/propTypes.js +17 -0
- package/lib-module/Carousel/Carousel.js +80 -57
- package/lib-module/Carousel/CarouselContext.js +21 -8
- package/lib-module/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +51 -0
- package/lib-module/Carousel/CarouselStepTracker/CarouselStepTracker.js +42 -0
- package/lib-module/Carousel/CarouselStepTracker/index.js +2 -0
- package/lib-module/Carousel/CarouselTabs/CarouselTabs.js +50 -0
- package/lib-module/Carousel/CarouselTabs/CarouselTabsPanel.js +76 -0
- package/lib-module/Carousel/CarouselTabs/CarouselTabsPanelItem.js +126 -0
- package/lib-module/Carousel/CarouselTabs/index.js +2 -0
- package/lib-module/Carousel/CarouselThumbnail.js +85 -0
- package/lib-module/Carousel/CarouselThumbnailNavigation.js +66 -0
- package/lib-module/Carousel/dictionary.js +4 -2
- package/lib-module/Carousel/index.js +2 -1
- package/lib-module/Checkbox/Checkbox.js +8 -4
- package/lib-module/Checkbox/CheckboxGroup.js +8 -1
- package/lib-module/Feedback/Feedback.js +19 -11
- package/lib-module/Icon/IconText.js +6 -1
- package/lib-module/InputLabel/InputLabel.js +12 -6
- package/lib-module/Link/InlinePressable.js +1 -8
- package/lib-module/Link/LinkBase.js +14 -11
- package/lib-module/List/ListItem.js +9 -5
- package/lib-module/Notification/Notification.js +46 -26
- package/lib-module/Pagination/Pagination.js +8 -4
- package/lib-module/Radio/RadioGroup.js +8 -0
- package/lib-module/RadioCard/RadioCard.js +7 -2
- package/lib-module/RadioCard/RadioCardGroup.js +7 -0
- package/lib-module/Select/Select.js +8 -4
- package/lib-module/SkipLink/SkipLink.js +188 -0
- package/lib-module/SkipLink/index.js +2 -0
- package/lib-module/StepTracker/Step.js +9 -5
- package/lib-module/StepTracker/StepTracker.js +8 -4
- package/lib-module/Tabs/TabsItem.js +5 -1
- package/lib-module/TextInput/TextInputBase.js +8 -4
- package/lib-module/ThemeProvider/ThemeProvider.js +24 -3
- package/lib-module/ThemeProvider/utils/styles.js +8 -1
- package/lib-module/ThemeProvider/utils/theme-tokens.js +1 -1
- package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +7 -0
- package/lib-module/Typography/Typography.js +7 -3
- package/lib-module/index.js +1 -0
- package/package.json +46 -47
- package/src/BaseProvider/index.jsx +6 -3
- package/src/Button/ButtonBase.jsx +36 -12
- package/src/Button/ButtonGroup.jsx +6 -0
- package/src/Button/propTypes.js +14 -0
- package/src/Carousel/Carousel.jsx +91 -64
- package/src/Carousel/CarouselContext.jsx +29 -5
- package/src/Carousel/CarouselFirstFocus/CarouselFirstFocus.jsx +49 -0
- package/src/Carousel/CarouselStepTracker/CarouselStepTracker.jsx +36 -0
- package/src/Carousel/CarouselStepTracker/index.js +3 -0
- package/src/Carousel/CarouselTabs/CarouselTabs.jsx +37 -0
- package/src/Carousel/CarouselTabs/CarouselTabsPanel.jsx +69 -0
- package/src/Carousel/CarouselTabs/CarouselTabsPanelItem.jsx +119 -0
- package/src/Carousel/CarouselTabs/index.js +3 -0
- package/src/Carousel/CarouselThumbnail.jsx +77 -0
- package/src/Carousel/CarouselThumbnailNavigation.jsx +53 -0
- package/src/Carousel/dictionary.js +4 -2
- package/src/Carousel/index.js +1 -0
- package/src/Checkbox/Checkbox.jsx +14 -11
- package/src/Checkbox/CheckboxGroup.jsx +8 -1
- package/src/Feedback/Feedback.jsx +14 -7
- package/src/Icon/IconText.jsx +3 -1
- package/src/InputLabel/InputLabel.jsx +13 -12
- package/src/Link/InlinePressable.jsx +2 -8
- package/src/Link/LinkBase.jsx +18 -21
- package/src/List/ListItem.jsx +10 -5
- package/src/Notification/Notification.jsx +40 -23
- package/src/Pagination/Pagination.jsx +6 -4
- package/src/Radio/RadioGroup.jsx +7 -0
- package/src/RadioCard/RadioCard.jsx +3 -2
- package/src/RadioCard/RadioCardGroup.jsx +6 -0
- package/src/Select/Select.jsx +12 -3
- package/src/SkipLink/SkipLink.jsx +179 -0
- package/src/SkipLink/index.js +3 -0
- package/src/StepTracker/Step.jsx +12 -4
- package/src/StepTracker/StepTracker.jsx +11 -10
- package/src/Tabs/TabsItem.jsx +3 -2
- package/src/TextInput/TextInputBase.jsx +11 -3
- package/src/ThemeProvider/ThemeProvider.jsx +22 -3
- package/src/ThemeProvider/utils/styles.js +9 -1
- package/src/ThemeProvider/utils/theme-tokens.js +1 -1
- package/src/ToggleSwitch/ToggleSwitchGroup.jsx +6 -0
- package/src/Typography/Typography.jsx +11 -12
- 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
|
-
|
|
156
|
+
thumbnails = undefined,
|
|
157
|
+
panelNavigation = thumbnails ? (
|
|
158
|
+
<CarouselThumbnailNavigation thumbnails={thumbnails} />
|
|
159
|
+
) : (
|
|
160
|
+
<CarouselStepTracker />
|
|
161
|
+
),
|
|
165
162
|
tag = 'ul',
|
|
166
|
-
accessibilityRole
|
|
167
|
-
accessibilityLabel =
|
|
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
|
-
|
|
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 =
|
|
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 = (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
*
|
|
573
|
-
*
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
*
|
|
606
|
+
* panelNavigation={<CustomPanelNavigation />}
|
|
580
607
|
* >
|
|
581
608
|
* <Carousel.Item>First Slide</Carousel.Item>
|
|
582
609
|
* </Carousel>
|
|
583
610
|
* ```
|
|
584
611
|
*/
|
|
585
|
-
|
|
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 = ({
|
|
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
|
-
() => ({
|
|
9
|
-
|
|
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,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
|