@telus-uds/components-base 1.9.0 → 1.10.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.
@@ -3,6 +3,7 @@ export { default as ActivityIndicator } from './ActivityIndicator';
3
3
  export { default as Box } from './Box';
4
4
  export * from './Button';
5
5
  export { default as Card, PressableCardBase } from './Card';
6
+ export * from './Carousel';
6
7
  export { default as Checkbox } from './Checkbox';
7
8
  export * from './Checkbox';
8
9
  export { default as Divider } from './Divider';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telus-uds/components-base",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Base components",
5
5
  "keywords": [
6
6
  "base"
@@ -66,7 +66,7 @@
66
66
  "dependencies": {
67
67
  "airbnb-prop-types": "^2.16.0",
68
68
  "@telus-uds/system-constants": "^1.0.4",
69
- "@telus-uds/system-theme-tokens": "^2.0.2",
69
+ "@telus-uds/system-theme-tokens": "^2.1.0",
70
70
  "lodash.debounce": "^4.0.8",
71
71
  "lodash.merge": "^4.6.2",
72
72
  "prop-types": "^15.7.2",
@@ -0,0 +1,586 @@
1
+ import React from 'react'
2
+ import { View, Animated, PanResponder, StyleSheet, Platform } from 'react-native'
3
+ import PropTypes from 'prop-types'
4
+ import { useThemeTokens } from '../ThemeProvider'
5
+ import { useViewport } from '../ViewportProvider'
6
+ import { getTokensPropType, variantProp, selectSystemProps, a11yProps, viewProps } from '../utils'
7
+ import { useA11yInfo } from '../A11yInfoProvider'
8
+ import { CarouselProvider } from './CarouselContext'
9
+ import CarouselItem from './CarouselItem'
10
+ import StepTracker from '../StepTracker'
11
+ import StackView from '../StackView'
12
+ import IconButton from '../IconButton'
13
+
14
+ const staticStyles = StyleSheet.create({
15
+ root: {
16
+ backgroundColor: 'transparent',
17
+ justifyContent: 'center',
18
+ alignItems: 'center',
19
+ position: 'relative',
20
+ top: 0,
21
+ left: 0
22
+ }
23
+ })
24
+
25
+ const staticTokens = {
26
+ stackView: {
27
+ justifyContent: 'center'
28
+ },
29
+ stepTracker: {
30
+ showStepLabel: false,
31
+ showStepTrackerLabel: true,
32
+ knobCompletedBackgroundColor: 'none',
33
+ connectorCompletedColor: 'none',
34
+ connectorColor: 'none'
35
+ }
36
+ }
37
+
38
+ const selectContainerStyles = (width) => ({
39
+ backgroundColor: 'transparent',
40
+ overflow: 'hidden',
41
+ width
42
+ })
43
+
44
+ const selectSwipeAreaStyles = (count, width) => ({
45
+ width: width * count,
46
+ justifyContent: 'space-between',
47
+ flexDirection: 'row'
48
+ })
49
+
50
+ const selectPreviousNextNavigationButtonStyles = (
51
+ previousNextNavigationButtonWidth,
52
+ previousNextNavigationPosition,
53
+ spaceBetweenSlideAndPreviousNextNavigation,
54
+ isFirstSlide,
55
+ isLastSlide,
56
+ areStylesAppliedOnPreviousButton
57
+ ) => {
58
+ const styles = {
59
+ zIndex: 1,
60
+ position: 'absolute'
61
+ }
62
+ const dynamicPositionProperty = areStylesAppliedOnPreviousButton ? 'left' : 'right'
63
+ if (isFirstSlide) {
64
+ styles.visibility = areStylesAppliedOnPreviousButton ? 'hidden' : 'visible'
65
+ } else if (isLastSlide) {
66
+ styles.visibility = areStylesAppliedOnPreviousButton ? 'visible' : 'hidden'
67
+ } else {
68
+ styles.visibility = 'visible'
69
+ }
70
+
71
+ if (previousNextNavigationPosition === 'edge') {
72
+ styles[dynamicPositionProperty] = -1 * (previousNextNavigationButtonWidth / 2)
73
+ } else if (previousNextNavigationPosition === 'inside') {
74
+ styles[dynamicPositionProperty] = 0
75
+ } else if (previousNextNavigationPosition === 'outside') {
76
+ styles[dynamicPositionProperty] =
77
+ -1 * (spaceBetweenSlideAndPreviousNextNavigation + previousNextNavigationButtonWidth)
78
+ }
79
+ return styles
80
+ }
81
+
82
+ const defaultPanelNavigationDictionary = {
83
+ en: {
84
+ stepTrackerLabel: 'Showing %{stepNumber} of %{stepCount}'
85
+ },
86
+ fr: {
87
+ stepTrackerLabel: 'Étape %{stepNumber} sur %{stepCount}: %{stepLabel}'
88
+ }
89
+ }
90
+
91
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
92
+
93
+ /**
94
+ * Carousel is a general-purpose content slider that can be used to render content in terms of slides.
95
+
96
+ ## Usage
97
+ - `Carousel` is a top-level export from `@telus-uds/components-base` which is used to render a Carousel
98
+ - Immediately within `Carousel`, individual slides are wrapped in `Carousel.Item` for the top-level `Carousel` to know how to identify an individual slide
99
+ - You can use any UDS component or other platform-specific component, (based on the platform you're rendering) to achieve any desired layout
100
+ - By default, Carousel takea all the `width` available to it and the `height` is determined based on the content in the slide with more content
101
+ - You may want to wrap Carousel in other layout components like `Box`, `FlexGrid` etc, to achieve a responsive layout of your need
102
+
103
+ ## `useCarousel` custom hook
104
+
105
+ ```jsx
106
+ import { useCarousel } from '@telus-uds/components-base'
107
+
108
+ const SomeComponentWithinCarouselItem = () => {
109
+ const {
110
+ activeIndex,
111
+ totalItems,
112
+ width,
113
+ goTo
114
+ } = useCarousel()
115
+ return <Text>Hi!</Text>
116
+ }
117
+ ```
118
+
119
+ You can use `useCarousel` to hook into internal state of the Carousel component like:
120
+ - `activeIndex`: Index of the current slide
121
+ - `totalItems`: Total number of items/slides passed to the Carousel
122
+ - `width`: Width of the individual carousel slide
123
+ - `goTo`: A function to go to a particular slide by passing the index of that slide, e.g: goTo(0) where `0` is the index of the first slide
124
+
125
+ ## Accessibility
126
+
127
+ - Top-level `Carousel` and `Carousel.Item` can take all possible React Native's `View` and `a11y` props
128
+ - If your slide contains input elements like buttons, you may want to configure them to be only focusable when `activeIndex` is equal to the current slide index in order to avoid tabbing going between slides
129
+
130
+ ## Platform considerations
131
+ The component is available on both native platforms and web.
132
+
133
+ ## Other considerations
134
+ - You may want to use the same kind of layout in all your slides to avoid visual and height differences
135
+ - `previous` and `next` navigation buttons are automatically removed in `sm` and `xs` viewports, as these smaller viewports offers swipe functionality
136
+
137
+ ## Tokens
138
+
139
+ You can override the following tokens in exceptional circumstances:
140
+ - `previousIcon` - Icon of the previous button
141
+ - `nextIcon` - Icon of the next button
142
+ - `showPreviousNextNavigation` - If you want to show/hide the previous/next navigation
143
+ - `showPanelNavigation` - If you want to show/hide the panel navigation
144
+ - `spaceBetweenSlideAndPreviousNextNavigation` - Horizontal space between slide and previous/next navigational buttons
145
+ - `spaceBetweenSlideAndPanelNavigation` - Vertical space between slide area and panel navigation area
146
+ */
147
+ const Carousel = React.forwardRef(
148
+ (
149
+ {
150
+ tokens,
151
+ variant,
152
+ children,
153
+ previousNextNavigationPosition = 'inside',
154
+ previousNextIconSize = 'default',
155
+ minDistanceToCapture = 5,
156
+ minDistanceForAction = 0.2,
157
+ onAnimationStart,
158
+ onAnimationEnd,
159
+ onIndexChanged,
160
+ springConfig = undefined,
161
+ onRenderPanelNavigation,
162
+ panelNavigationTextDictionary = defaultPanelNavigationDictionary,
163
+ accessibilityRole = 'adjustable',
164
+ accessibilityLabel = 'carousel',
165
+ ...rest
166
+ },
167
+ ref
168
+ ) => {
169
+ const viewport = useViewport()
170
+ const {
171
+ previousIcon,
172
+ nextIcon,
173
+ showPreviousNextNavigation,
174
+ showPanelNavigation,
175
+ spaceBetweenSlideAndPreviousNextNavigation,
176
+ spaceBetweenSlideAndPanelNavigation
177
+ } = useThemeTokens('Carousel', tokens, variant, {
178
+ viewport
179
+ })
180
+ const [activeIndex, setActiveIndex] = React.useState(0)
181
+ const childrenArray = React.Children.toArray(children)
182
+ const systemProps = selectProps({
183
+ ...rest,
184
+ accessibilityRole,
185
+ accessibilityLabel,
186
+ accessibilityValue: {
187
+ min: 1,
188
+ max: childrenArray.length,
189
+ now: activeIndex + 1
190
+ }
191
+ })
192
+ const { reduceMotionEnabled } = useA11yInfo()
193
+ const [containerLayout, setContainerLayout] = React.useState({
194
+ x: 0,
195
+ y: 0,
196
+ width: 0
197
+ })
198
+ const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
199
+ React.useState(0)
200
+ const pan = React.useRef(new Animated.ValueXY()).current
201
+ const animatedX = React.useRef(0)
202
+ const animatedY = React.useRef(0)
203
+ const isFirstSlide = !activeIndex
204
+ const isLastSlide = activeIndex + 1 >= children.length
205
+ const panelNavigationTokens = {
206
+ ...staticTokens.stepTracker,
207
+ containerPaddingTop: spaceBetweenSlideAndPanelNavigation
208
+ }
209
+
210
+ const onContainerLayout = ({
211
+ nativeEvent: {
212
+ layout: { x, y, width }
213
+ }
214
+ }) => setContainerLayout((prevState) => ({ ...prevState, x, y, width }))
215
+
216
+ const onPreviousNextNavigationButtonLayout = ({
217
+ nativeEvent: {
218
+ layout: { width }
219
+ }
220
+ }) => setPreviousNextNavigationButtonWidth(width)
221
+
222
+ const updateOffset = React.useCallback(() => {
223
+ animatedX.current = containerLayout.width * activeIndex * -1
224
+ animatedY.current = 0
225
+ pan.setOffset({
226
+ x: animatedX.current,
227
+ y: animatedY.current
228
+ })
229
+ pan.setValue({ x: 0, y: 0 })
230
+ }, [activeIndex, containerLayout.width, pan, animatedX])
231
+
232
+ const animate = React.useCallback(
233
+ (toValue) => {
234
+ if (reduceMotionEnabled) {
235
+ Animated.timing(pan, { toValue, duration: 1, useNativeDriver: false }).start()
236
+ } else {
237
+ Animated.spring(pan, {
238
+ ...springConfig,
239
+ toValue,
240
+ useNativeDriver: false
241
+ }).start()
242
+ }
243
+ },
244
+ [pan, springConfig, reduceMotionEnabled]
245
+ )
246
+
247
+ const updateIndex = React.useCallback(
248
+ (delta = 1) => {
249
+ const toValue = { x: 0, y: 0 }
250
+ let skipChanges = !delta
251
+ let calcDelta = delta
252
+
253
+ if (activeIndex <= 0 && delta < 0) {
254
+ skipChanges = true
255
+ calcDelta = children.length + delta
256
+ } else if (activeIndex + 1 >= children.length && delta > 0) {
257
+ skipChanges = true
258
+ calcDelta = -1 * activeIndex + delta - 1
259
+ }
260
+
261
+ if (skipChanges) {
262
+ animate(toValue)
263
+ return calcDelta
264
+ }
265
+
266
+ const index = activeIndex + calcDelta
267
+ setActiveIndex(index)
268
+
269
+ toValue.x = containerLayout.width * -1 * calcDelta
270
+
271
+ animate(toValue)
272
+
273
+ if (onIndexChanged) onIndexChanged(calcDelta)
274
+ if (onAnimationEnd) onAnimationEnd(index)
275
+ return calcDelta
276
+ },
277
+ [containerLayout.width, activeIndex, animate, children.length, onIndexChanged, onAnimationEnd]
278
+ )
279
+
280
+ const fixOffsetAndGo = React.useCallback(
281
+ (delta) => {
282
+ updateOffset()
283
+ if (onAnimationStart) onAnimationStart(activeIndex)
284
+ updateIndex(delta)
285
+ },
286
+ [updateIndex, updateOffset, activeIndex, onAnimationStart]
287
+ )
288
+
289
+ const goToNeighboring = React.useCallback(
290
+ (toPrev = false) => {
291
+ fixOffsetAndGo(toPrev ? -1 : 1)
292
+ },
293
+ [fixOffsetAndGo]
294
+ )
295
+
296
+ const isSwipeAllowed = React.useCallback(() => {
297
+ if (Platform.OS === 'web') {
298
+ return !!(viewport === 'xs' || viewport === 'sm')
299
+ }
300
+ return true
301
+ }, [viewport])
302
+
303
+ const panResponder = React.useMemo(
304
+ () =>
305
+ PanResponder.create({
306
+ onPanResponderTerminationRequest: () => false,
307
+ onMoveShouldSetResponderCapture: () => true,
308
+ onMoveShouldSetPanResponderCapture: (_, gestureState) => {
309
+ if (!isSwipeAllowed()) {
310
+ return false
311
+ }
312
+
313
+ if (onAnimationStart) onAnimationStart(activeIndex)
314
+
315
+ return Math.abs(gestureState.dx) > minDistanceToCapture
316
+ },
317
+ onPanResponderGrant: () => updateOffset(),
318
+ onPanResponderMove: Animated.event([null, { dx: pan.x }], {
319
+ useNativeDriver: false
320
+ }),
321
+ onPanResponderRelease: (_, gesture) => {
322
+ const correction = gesture.moveX - gesture.x0
323
+
324
+ if (Math.abs(correction) < containerLayout.width * minDistanceForAction) {
325
+ animate({ x: 0, y: 0 })
326
+ } else {
327
+ const delta = correction > 0 ? -1 : 1
328
+ updateIndex(delta)
329
+ }
330
+ }
331
+ }),
332
+ [
333
+ containerLayout.width,
334
+ updateIndex,
335
+ updateOffset,
336
+ animate,
337
+ isSwipeAllowed,
338
+ activeIndex,
339
+ minDistanceForAction,
340
+ onAnimationStart,
341
+ minDistanceToCapture,
342
+ pan.x
343
+ ]
344
+ )
345
+
346
+ React.useEffect(() => {
347
+ pan.x.addListener(({ value }) => {
348
+ animatedX.current = value
349
+ })
350
+ pan.y.addListener(({ value }) => {
351
+ animatedY.current = value
352
+ })
353
+ return () => {
354
+ pan.x.removeAllListeners()
355
+ pan.y.removeAllListeners()
356
+ }
357
+ }, [pan.x, pan.y])
358
+
359
+ const goToNext = React.useCallback(() => {
360
+ goToNeighboring()
361
+ }, [goToNeighboring])
362
+
363
+ const goToPrev = React.useCallback(() => {
364
+ goToNeighboring(true)
365
+ }, [goToNeighboring])
366
+
367
+ const goTo = React.useCallback(
368
+ (index = 0) => {
369
+ const delta = index - activeIndex
370
+ if (delta) {
371
+ fixOffsetAndGo(delta)
372
+ }
373
+ },
374
+ [fixOffsetAndGo, activeIndex]
375
+ )
376
+
377
+ // @TODO: - these are Allium-theme variants and won't have any effect in themes that don't implement them.
378
+ // Normally we avoid setting variants of subcomponents, however this could be re-considered.
379
+ // Related discussion - https://github.com/telus/universal-design-system/issues/1549
380
+ const previousNextIconButtonVariants = { size: previousNextIconSize, raised: true }
381
+
382
+ return (
383
+ <CarouselProvider
384
+ activeIndex={activeIndex}
385
+ totalItems={childrenArray.length}
386
+ width={containerLayout.width}
387
+ goTo={goTo}
388
+ >
389
+ <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
390
+ {showPreviousNextNavigation && (
391
+ <View
392
+ style={selectPreviousNextNavigationButtonStyles(
393
+ previousNextNavigationButtonWidth,
394
+ previousNextNavigationPosition,
395
+ spaceBetweenSlideAndPreviousNextNavigation,
396
+ isFirstSlide,
397
+ isLastSlide,
398
+ true
399
+ )}
400
+ testID="previous-button-container"
401
+ >
402
+ <IconButton
403
+ onLayout={onPreviousNextNavigationButtonLayout}
404
+ icon={previousIcon}
405
+ onPress={goToPrev}
406
+ variant={previousNextIconButtonVariants}
407
+ accessibilityLabel="previous-button"
408
+ />
409
+ </View>
410
+ )}
411
+ <View style={selectContainerStyles(containerLayout.width)}>
412
+ <Animated.View
413
+ style={StyleSheet.flatten([
414
+ selectSwipeAreaStyles(children.length, containerLayout.width),
415
+ {
416
+ transform: [{ translateX: pan.x }, { translateY: pan.y }]
417
+ }
418
+ ])}
419
+ {...panResponder.panHandlers}
420
+ >
421
+ {childrenArray.map((element, index) => {
422
+ const clonedElement = React.cloneElement(element, { elementIndex: index })
423
+ return <React.Fragment key={index.toFixed(2)}>{clonedElement}</React.Fragment>
424
+ })}
425
+ </Animated.View>
426
+ </View>
427
+ {showPreviousNextNavigation && (
428
+ <View
429
+ style={selectPreviousNextNavigationButtonStyles(
430
+ previousNextNavigationButtonWidth,
431
+ previousNextNavigationPosition,
432
+ spaceBetweenSlideAndPreviousNextNavigation,
433
+ isFirstSlide,
434
+ isLastSlide,
435
+ false
436
+ )}
437
+ testID="next-button-container"
438
+ >
439
+ <IconButton
440
+ onLayout={onPreviousNextNavigationButtonLayout}
441
+ icon={nextIcon}
442
+ onPress={goToNext}
443
+ variant={previousNextIconButtonVariants}
444
+ accessibilityLabel="next-button"
445
+ />
446
+ </View>
447
+ )}
448
+ </View>
449
+ {showPanelNavigation ? (
450
+ <StackView direction="row" tokens={staticTokens.stackView}>
451
+ {onRenderPanelNavigation ? (
452
+ onRenderPanelNavigation({ activeIndex, totalItems: childrenArray.length })
453
+ ) : (
454
+ <StepTracker
455
+ current={activeIndex}
456
+ steps={childrenArray.map((_, index) => String(index))}
457
+ dictionary={panelNavigationTextDictionary}
458
+ tokens={panelNavigationTokens}
459
+ />
460
+ )}
461
+ </StackView>
462
+ ) : null}
463
+ </CarouselProvider>
464
+ )
465
+ }
466
+ )
467
+
468
+ Carousel.propTypes = {
469
+ ...selectedSystemPropTypes,
470
+ tokens: getTokensPropType('Carousel'),
471
+ variant: variantProp.propType,
472
+ /**
473
+ * Slides to render in Carousel. Wrap individual slides in `Carousel.Item`
474
+ */
475
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
476
+ /**
477
+ * `inside` renders the previous and next buttons inside the slide
478
+ * `outside` renders the previous and next buttons outside the slide
479
+ * `edge` renders the previous and next buttons at the edge of the slide
480
+ */
481
+ previousNextNavigationPosition: PropTypes.oneOf(['inside', 'outside', 'edge']),
482
+ /**
483
+ * Defines the size of the `IconButton` which is being used to render next and previous buttons
484
+ */
485
+ previousNextIconSize: PropTypes.oneOf(['default', 'small', 'large']),
486
+ /**
487
+ * Carousel uses `Animated.spring` to animate slide changes, use this option to pass custom animation configuration
488
+ */
489
+ springConfig: PropTypes.object,
490
+ /**
491
+ * Minimal part of slide width must be swiped for changing index.
492
+ * Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index
493
+ */
494
+ minDistanceForAction: PropTypes.number,
495
+ /**
496
+ * Initiate animation after swipe this distance.
497
+ */
498
+ minDistanceToCapture: PropTypes.number,
499
+ /**
500
+ * Called when active index changed
501
+ * This function is also provided with a parameter indicating changed index (either 1, or -1)
502
+ * Use it as follows:
503
+ * ```js
504
+ * const onIndexChangedCallback = React.useCallback((changedIndex) => {
505
+ * console.log(changedIndex)
506
+ * }, []) // pass local dependencies as per your component
507
+ * <Carousel
508
+ * onIndexChanged={onIndexChangedCallback}
509
+ * >
510
+ * <Carousel.Item>First Slide</Carousel.Item>
511
+ * </Carousel>
512
+ * ```
513
+ * Caution: Always consider wrapping your callback for `onIndexChanged` in `useCallback` in order to avoid bugs and performance issues
514
+ */
515
+ onIndexChanged: PropTypes.func,
516
+ /**
517
+ * Use this to render a custom panel navigation element instead of dots navigation
518
+ * This function is also provided with an object with the following properties
519
+ * activeIndex: index of current slide
520
+ * totalItems: total number of slides
521
+ * Use it as follows:
522
+ * ```js
523
+ * <Carousel
524
+ * onRenderPanelNavigation={({ totalItems, activeIndex }) => <Text>Showing {activeIndex + 1}</Text>}
525
+ * >
526
+ * <Carousel.Item>First Slide</Carousel.Item>
527
+ * </Carousel>
528
+ * ```
529
+ */
530
+ onRenderPanelNavigation: PropTypes.func,
531
+ /**
532
+ * When slide animation start
533
+ * This function is also provided with a parameter indicating the current slide index before animation starts
534
+ * Use it as follows:
535
+ * ```js
536
+ * const onAnimationStartCallback = React.useCallback((currentIndex) => {
537
+ * console.log(currentIndex)
538
+ * }, []) // pass local dependencies as per your component
539
+ * <Carousel
540
+ * onAnimationStart={onAnimationStartCallback}
541
+ * >
542
+ * <Carousel.Item>First Slide</Carousel.Item>
543
+ * </Carousel>
544
+ * ```
545
+ * Caution: Always consider wrapping your callback for `onAnimationStart` in `useCallback` in order to avoid bugs and performance issues
546
+ */
547
+ onAnimationStart: PropTypes.func,
548
+ /**
549
+ * When slide animation end with parameter of current index (after animation ends)
550
+ * This function is also provided with a parameter indicating the updated slide index after animation ends
551
+ * Use it as follows:
552
+ * ```js
553
+ * const onAnimationEndCallback = React.useCallback((changedIndex) => {
554
+ * console.log(changedIndex)
555
+ * }, []) // pass local dependencies as per your component
556
+ * <Carousel
557
+ * onAnimationEnd={onAnimationEndCallback}
558
+ * >
559
+ * <Carousel.Item>First Slide</Carousel.Item>
560
+ * </Carousel>
561
+ * ```
562
+ * Caution: Always consider wrapping your callback for `onAnimationEnd` in `useCallback` in order to avoid bugs and performance issues
563
+ */
564
+ onAnimationEnd: PropTypes.func,
565
+ /**
566
+ * Use this to override the default text for panel navigation
567
+ */
568
+ panelNavigationTextDictionary: PropTypes.shape({
569
+ en: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired }),
570
+ fr: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired })
571
+ }),
572
+ /**
573
+ * Provide custom accessibilityRole for Carousel container
574
+ */
575
+ accessibilityRole: PropTypes.string,
576
+ /**
577
+ * Provide custom accessibilityLabel for Carousel container
578
+ */
579
+ accessibilityLabel: PropTypes.string
580
+ }
581
+
582
+ Carousel.Item = CarouselItem
583
+
584
+ Carousel.displayName = 'Carousel'
585
+
586
+ export default Carousel
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ const CarouselContext = React.createContext()
5
+
6
+ const CarouselProvider = ({ children, activeIndex, totalItems, width, goTo }) => {
7
+ const value = React.useMemo(
8
+ () => ({ activeIndex, totalItems, width, goTo }),
9
+ [activeIndex, totalItems, width, goTo]
10
+ )
11
+ return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
12
+ }
13
+
14
+ function useCarousel() {
15
+ const context = React.useContext(CarouselContext)
16
+ if (context === undefined) {
17
+ throw new Error(`'useCarousel' must be used within a 'CarouselProvider'`)
18
+ }
19
+ return context
20
+ }
21
+
22
+ CarouselProvider.propTypes = {
23
+ children: PropTypes.arrayOf(PropTypes.element).isRequired,
24
+ activeIndex: PropTypes.number.isRequired,
25
+ totalItems: PropTypes.number.isRequired,
26
+ width: PropTypes.number.isRequired,
27
+ goTo: PropTypes.func.isRequired
28
+ }
29
+
30
+ export { CarouselProvider, useCarousel }
@@ -0,0 +1,48 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View, Platform } from 'react-native'
4
+ import { selectSystemProps, a11yProps, viewProps } from '../../utils'
5
+ import { useCarousel } from '../CarouselContext'
6
+
7
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
8
+
9
+ /**
10
+ * `Carousel.Item` is used to wrap the content of an individual slide and is suppsoed to be the
11
+ * only top-level component passed to the `Carousel`
12
+ */
13
+ const CarouselItem = ({ children, elementIndex, ...rest }) => {
14
+ const { width, activeIndex, totalItems } = useCarousel()
15
+ const selectedProps = selectProps({
16
+ ...rest,
17
+ // `group` role crashes the app on Android so setting it to `none` for Android
18
+ accessibilityRole: Platform.OS === 'android' ? 'none' : 'group',
19
+ accessibilityLabel: `Showing ${elementIndex + 1} of ${totalItems}`
20
+ })
21
+ const focusabilityProps = activeIndex === elementIndex ? {} : a11yProps.nonFocusableProps
22
+ return (
23
+ <View style={{ width }} {...selectedProps} {...focusabilityProps}>
24
+ {children}
25
+ </View>
26
+ )
27
+ }
28
+
29
+ CarouselItem.propTypes = {
30
+ ...selectedSystemPropTypes,
31
+ /**
32
+ * Index of the current slide
33
+ * Don't pass this prop when using `Carousel.Item` as it is already being passed by `Carousel` top-level component
34
+ */
35
+ elementIndex: PropTypes.number,
36
+ /**
37
+ * Provide custom accessibilityLabelledBy for Carousel slide
38
+ */
39
+ accessibilityLabelledBy: PropTypes.string,
40
+ /**
41
+ * Content of the slide
42
+ */
43
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
44
+ }
45
+
46
+ CarouselItem.displayName = 'Carousel.Item'
47
+
48
+ export default CarouselItem
@@ -0,0 +1,3 @@
1
+ import CarouselItem from './CarouselItem'
2
+
3
+ export default CarouselItem
@@ -0,0 +1,2 @@
1
+ export * from './CarouselContext'
2
+ export { default as Carousel } from './Carousel'