@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.
- package/CHANGELOG.md +41 -2
- package/component-docs.json +888 -66
- package/lib/Button/ButtonBase.js +36 -7
- package/lib/Button/ButtonGroup.js +7 -0
- package/lib/Button/propTypes.js +18 -0
- package/lib/Carousel/Carousel.js +69 -12
- package/lib/Carousel/CarouselContext.js +17 -11
- package/lib/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +73 -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/CheckboxGroup.js +7 -0
- package/lib/Icon/IconText.js +1 -1
- package/lib/Link/InlinePressable.js +1 -8
- package/lib/Link/LinkBase.js +6 -7
- package/lib/List/ListItem.js +1 -1
- package/lib/Notification/Notification.js +37 -22
- package/lib/Radio/RadioGroup.js +8 -0
- package/lib/RadioCard/RadioCardGroup.js +7 -0
- package/lib/SkipLink/SkipLink.js +216 -0
- package/lib/SkipLink/index.js +13 -0
- package/lib/ThemeProvider/ThemeProvider.js +6 -1
- package/lib/ToggleSwitch/ToggleSwitchGroup.js +7 -0
- package/lib/index.js +9 -0
- package/lib-module/Button/ButtonBase.js +35 -7
- package/lib-module/Button/ButtonGroup.js +7 -0
- package/lib-module/Button/propTypes.js +17 -0
- package/lib-module/Carousel/Carousel.js +66 -11
- package/lib-module/Carousel/CarouselContext.js +17 -11
- package/lib-module/Carousel/CarouselFirstFocus/CarouselFirstFocus.js +51 -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/CheckboxGroup.js +7 -0
- package/lib-module/Icon/IconText.js +1 -1
- package/lib-module/Link/InlinePressable.js +1 -8
- package/lib-module/Link/LinkBase.js +6 -7
- package/lib-module/List/ListItem.js +1 -1
- package/lib-module/Notification/Notification.js +38 -23
- package/lib-module/Radio/RadioGroup.js +8 -0
- package/lib-module/RadioCard/RadioCardGroup.js +7 -0
- package/lib-module/SkipLink/SkipLink.js +188 -0
- package/lib-module/SkipLink/index.js +2 -0
- package/lib-module/ThemeProvider/ThemeProvider.js +5 -1
- package/lib-module/ToggleSwitch/ToggleSwitchGroup.js +7 -0
- package/lib-module/index.js +1 -0
- package/package.json +46 -47
- package/src/Button/ButtonBase.jsx +28 -9
- package/src/Button/ButtonGroup.jsx +6 -0
- package/src/Button/propTypes.js +14 -0
- package/src/Carousel/Carousel.jsx +68 -10
- package/src/Carousel/CarouselContext.jsx +22 -9
- package/src/Carousel/CarouselFirstFocus/CarouselFirstFocus.jsx +49 -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/CheckboxGroup.jsx +7 -0
- package/src/Icon/IconText.jsx +1 -1
- package/src/Link/InlinePressable.jsx +2 -8
- package/src/Link/LinkBase.jsx +8 -17
- package/src/List/ListItem.jsx +1 -1
- package/src/Notification/Notification.jsx +35 -20
- package/src/Radio/RadioGroup.jsx +7 -0
- package/src/RadioCard/RadioCardGroup.jsx +6 -0
- package/src/SkipLink/SkipLink.jsx +179 -0
- package/src/SkipLink/index.js +3 -0
- package/src/ThemeProvider/ThemeProvider.jsx +7 -1
- package/src/ToggleSwitch/ToggleSwitchGroup.jsx +6 -0
- 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
|
|
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
|
-
|
|
156
|
+
thumbnails = undefined,
|
|
157
|
+
panelNavigation = thumbnails ? (
|
|
158
|
+
<CarouselThumbnailNavigation thumbnails={thumbnails} />
|
|
159
|
+
) : (
|
|
160
|
+
<CarouselStepTracker />
|
|
161
|
+
),
|
|
150
162
|
tag = 'ul',
|
|
151
|
-
accessibilityRole
|
|
152
|
-
accessibilityLabel =
|
|
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 =
|
|
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
|
-
|
|
11
|
-
width,
|
|
9
|
+
children,
|
|
12
10
|
goTo,
|
|
13
11
|
getCopyWithPlaceholders,
|
|
14
|
-
|
|
12
|
+
itemLabel,
|
|
13
|
+
refocus = false,
|
|
14
|
+
themeTokens,
|
|
15
|
+
totalItems,
|
|
16
|
+
width
|
|
15
17
|
}) => {
|
|
16
18
|
const value = React.useMemo(
|
|
17
|
-
() => ({
|
|
18
|
-
|
|
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
|
-
|
|
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,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
|
}
|
package/src/Carousel/index.js
CHANGED
|
@@ -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
|
*/
|