@telus-uds/components-base 1.8.5 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/component-docs.json +666 -27
  3. package/lib/Card/Card.js +9 -4
  4. package/lib/Carousel/Carousel.js +672 -0
  5. package/lib/Carousel/CarouselContext.js +59 -0
  6. package/lib/Carousel/CarouselItem/CarouselItem.js +92 -0
  7. package/lib/Carousel/CarouselItem/index.js +13 -0
  8. package/lib/Carousel/dictionary.js +23 -0
  9. package/lib/Carousel/index.js +32 -0
  10. package/lib/ExpandCollapse/Panel.js +10 -1
  11. package/lib/InputSupports/InputSupports.js +10 -3
  12. package/lib/InputSupports/useInputSupports.js +3 -2
  13. package/lib/Modal/Modal.js +4 -0
  14. package/lib/Skeleton/Skeleton.js +1 -0
  15. package/lib/StepTracker/StepTracker.js +15 -12
  16. package/lib/TextInput/TextInput.js +3 -12
  17. package/lib/TextInput/TextInputBase.js +9 -0
  18. package/lib/TextInput/propTypes.js +3 -8
  19. package/lib/index.js +23 -0
  20. package/lib/utils/props/clickProps.js +2 -2
  21. package/lib/utils/props/handlerProps.js +77 -31
  22. package/lib/utils/props/textInputProps.js +8 -1
  23. package/lib/utils/useScrollBlocking.js +66 -0
  24. package/lib/utils/useScrollBlocking.native.js +11 -0
  25. package/lib-module/Card/Card.js +5 -4
  26. package/lib-module/Carousel/Carousel.js +617 -0
  27. package/lib-module/Carousel/CarouselContext.js +43 -0
  28. package/lib-module/Carousel/CarouselItem/CarouselItem.js +75 -0
  29. package/lib-module/Carousel/CarouselItem/index.js +2 -0
  30. package/lib-module/Carousel/dictionary.js +16 -0
  31. package/lib-module/Carousel/index.js +2 -0
  32. package/lib-module/ExpandCollapse/Panel.js +9 -1
  33. package/lib-module/InputSupports/InputSupports.js +10 -3
  34. package/lib-module/InputSupports/useInputSupports.js +3 -2
  35. package/lib-module/Modal/Modal.js +3 -0
  36. package/lib-module/Skeleton/Skeleton.js +1 -0
  37. package/lib-module/StepTracker/StepTracker.js +14 -12
  38. package/lib-module/TextInput/TextInput.js +3 -9
  39. package/lib-module/TextInput/TextInputBase.js +10 -1
  40. package/lib-module/TextInput/propTypes.js +4 -8
  41. package/lib-module/index.js +2 -0
  42. package/lib-module/utils/props/clickProps.js +2 -2
  43. package/lib-module/utils/props/handlerProps.js +78 -31
  44. package/lib-module/utils/props/textInputProps.js +8 -1
  45. package/lib-module/utils/useScrollBlocking.js +58 -0
  46. package/lib-module/utils/useScrollBlocking.native.js +2 -0
  47. package/package.json +3 -3
  48. package/src/Card/Card.jsx +6 -4
  49. package/src/Carousel/Carousel.jsx +649 -0
  50. package/src/Carousel/CarouselContext.jsx +30 -0
  51. package/src/Carousel/CarouselItem/CarouselItem.jsx +66 -0
  52. package/src/Carousel/CarouselItem/index.js +3 -0
  53. package/src/Carousel/dictionary.js +16 -0
  54. package/src/Carousel/index.js +2 -0
  55. package/src/ExpandCollapse/Panel.jsx +8 -1
  56. package/src/InputSupports/InputSupports.jsx +18 -3
  57. package/src/InputSupports/useInputSupports.js +2 -2
  58. package/src/Modal/Modal.jsx +3 -1
  59. package/src/Skeleton/Skeleton.jsx +1 -0
  60. package/src/StepTracker/StepTracker.jsx +21 -8
  61. package/src/TextInput/TextInput.jsx +2 -9
  62. package/src/TextInput/TextInputBase.jsx +11 -1
  63. package/src/TextInput/propTypes.js +3 -7
  64. package/src/index.js +2 -0
  65. package/src/utils/props/clickProps.js +2 -2
  66. package/src/utils/props/handlerProps.js +64 -16
  67. package/src/utils/props/textInputProps.js +7 -1
  68. package/src/utils/useScrollBlocking.js +57 -0
  69. package/src/utils/useScrollBlocking.native.js +2 -0
@@ -0,0 +1,649 @@
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 {
7
+ getTokensPropType,
8
+ getA11yPropsFromHtmlTag,
9
+ layoutTags,
10
+ variantProp,
11
+ selectSystemProps,
12
+ a11yProps,
13
+ viewProps,
14
+ useCopy
15
+ } from '../utils'
16
+ import { useA11yInfo } from '../A11yInfoProvider'
17
+ import { CarouselProvider } from './CarouselContext'
18
+ import CarouselItem from './CarouselItem'
19
+ import StepTracker from '../StepTracker'
20
+ import StackView from '../StackView'
21
+ import IconButton from '../IconButton'
22
+
23
+ import dictionary from './dictionary'
24
+
25
+ const staticStyles = StyleSheet.create({
26
+ root: {
27
+ backgroundColor: 'transparent',
28
+ justifyContent: 'center',
29
+ alignItems: 'center',
30
+ position: 'relative',
31
+ top: 0,
32
+ left: 0
33
+ }
34
+ })
35
+
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
+ const selectContainerStyles = (width) => ({
50
+ backgroundColor: 'transparent',
51
+ overflow: 'hidden',
52
+ width
53
+ })
54
+
55
+ const selectSwipeAreaStyles = (count, width) => ({
56
+ width: width * count,
57
+ justifyContent: 'space-between',
58
+ flexDirection: 'row'
59
+ })
60
+
61
+ const selectPreviousNextNavigationButtonStyles = (
62
+ previousNextNavigationButtonWidth,
63
+ previousNextNavigationPosition,
64
+ spaceBetweenSlideAndPreviousNextNavigation,
65
+ isFirstSlide,
66
+ isLastSlide,
67
+ areStylesAppliedOnPreviousButton
68
+ ) => {
69
+ const styles = {
70
+ zIndex: 1,
71
+ position: 'absolute'
72
+ }
73
+ const dynamicPositionProperty = areStylesAppliedOnPreviousButton ? 'left' : 'right'
74
+ if (isFirstSlide) {
75
+ styles.visibility = areStylesAppliedOnPreviousButton ? 'hidden' : 'visible'
76
+ } else if (isLastSlide) {
77
+ styles.visibility = areStylesAppliedOnPreviousButton ? 'visible' : 'hidden'
78
+ } else {
79
+ styles.visibility = 'visible'
80
+ }
81
+
82
+ if (previousNextNavigationPosition === 'edge') {
83
+ styles[dynamicPositionProperty] = -1 * (previousNextNavigationButtonWidth / 2)
84
+ } else if (previousNextNavigationPosition === 'inside') {
85
+ styles[dynamicPositionProperty] = 0
86
+ } else if (previousNextNavigationPosition === 'outside') {
87
+ styles[dynamicPositionProperty] =
88
+ -1 * (spaceBetweenSlideAndPreviousNextNavigation + previousNextNavigationButtonWidth)
89
+ }
90
+ return styles
91
+ }
92
+
93
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
94
+
95
+ /**
96
+ * Carousel is a general-purpose content slider that can be used to render content in terms of slides.
97
+
98
+ ## Usage
99
+ - `Carousel` is a top-level export from `@telus-uds/components-base` which is used to render a Carousel
100
+ - Immediately within `Carousel`, individual slides are wrapped in `Carousel.Item` for the top-level `Carousel` to know how to identify an individual slide
101
+ - You can use any UDS component or other platform-specific component, (based on the platform you're rendering) to achieve any desired layout
102
+ - 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
103
+ - You may want to wrap Carousel in other layout components like `Box`, `FlexGrid` etc, to achieve a responsive layout of your need
104
+
105
+ ## `useCarousel` custom hook
106
+
107
+ ```jsx
108
+ import { useCarousel } from '@telus-uds/components-base'
109
+
110
+ const SomeComponentWithinCarouselItem = () => {
111
+ const {
112
+ activeIndex,
113
+ totalItems,
114
+ width,
115
+ goTo
116
+ } = useCarousel()
117
+ return <Text>Hi!</Text>
118
+ }
119
+ ```
120
+
121
+ You can use `useCarousel` to hook into internal state of the Carousel component like:
122
+ - `activeIndex`: Index of the current slide
123
+ - `totalItems`: Total number of items/slides passed to the Carousel
124
+ - `width`: Width of the individual carousel slide
125
+ - `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
126
+
127
+ ## Accessibility
128
+
129
+ - Top-level `Carousel` and `Carousel.Item` can take all possible React Native's `View` and `a11y` props
130
+ - 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
131
+
132
+ ## Platform considerations
133
+ The component is available on both native platforms and web.
134
+
135
+ ## Other considerations
136
+ - You may want to use the same kind of layout in all your slides to avoid visual and height differences
137
+ - `previous` and `next` navigation buttons are automatically removed in `sm` and `xs` viewports, as these smaller viewports offers swipe functionality
138
+
139
+ ## Tokens
140
+
141
+ You can override the following tokens in exceptional circumstances:
142
+ - `previousIcon` - Icon of the previous button
143
+ - `nextIcon` - Icon of the next button
144
+ - `showPreviousNextNavigation` - If you want to show/hide the previous/next navigation
145
+ - `showPanelNavigation` - If you want to show/hide the panel navigation
146
+ - `spaceBetweenSlideAndPreviousNextNavigation` - Horizontal space between slide and previous/next navigational buttons
147
+ - `spaceBetweenSlideAndPanelNavigation` - Vertical space between slide area and panel navigation area
148
+ */
149
+ const Carousel = React.forwardRef(
150
+ (
151
+ {
152
+ tokens,
153
+ variant,
154
+ children,
155
+ itemLabel = 'item',
156
+ previousNextNavigationPosition = 'inside',
157
+ previousNextIconSize = 'default',
158
+ minDistanceToCapture = 5,
159
+ minDistanceForAction = 0.2,
160
+ onAnimationStart,
161
+ onAnimationEnd,
162
+ onIndexChanged,
163
+ springConfig = undefined,
164
+ onRenderPanelNavigation,
165
+ tag = 'ul',
166
+ accessibilityRole = 'adjustable',
167
+ accessibilityLabel = 'carousel',
168
+ copy,
169
+ ...rest
170
+ },
171
+ ref
172
+ ) => {
173
+ const viewport = useViewport()
174
+ const {
175
+ previousIcon,
176
+ nextIcon,
177
+ showPreviousNextNavigation,
178
+ showPanelNavigation,
179
+ spaceBetweenSlideAndPreviousNextNavigation,
180
+ spaceBetweenSlideAndPanelNavigation
181
+ } = useThemeTokens('Carousel', tokens, variant, {
182
+ viewport
183
+ })
184
+ const [activeIndex, setActiveIndex] = React.useState(0)
185
+
186
+ const [isAnimating, setIsAnimating] = React.useState(false)
187
+ const handleAnimationStart = React.useCallback(
188
+ (...args) => {
189
+ if (typeof onAnimationStart === 'function') onAnimationStart(...args)
190
+ setIsAnimating(true)
191
+ },
192
+ [onAnimationStart]
193
+ )
194
+ const handleAnimationEnd = React.useCallback(
195
+ (...args) => {
196
+ if (typeof onAnimationEnd === 'function') onAnimationEnd(...args)
197
+ setIsAnimating(false)
198
+ },
199
+ [onAnimationEnd]
200
+ )
201
+
202
+ const getCopy = useCopy({ dictionary, copy })
203
+
204
+ const childrenArray = React.Children.toArray(children)
205
+ const systemProps = selectProps({
206
+ ...rest,
207
+ accessibilityRole,
208
+ accessibilityLabel,
209
+ accessibilityValue: {
210
+ min: 1,
211
+ max: childrenArray.length,
212
+ now: activeIndex + 1
213
+ }
214
+ })
215
+ const { reduceMotionEnabled } = useA11yInfo()
216
+ const [containerLayout, setContainerLayout] = React.useState({
217
+ x: 0,
218
+ y: 0,
219
+ width: 0
220
+ })
221
+ const [previousNextNavigationButtonWidth, setPreviousNextNavigationButtonWidth] =
222
+ React.useState(0)
223
+ const pan = React.useRef(new Animated.ValueXY()).current
224
+ const animatedX = React.useRef(0)
225
+ const animatedY = React.useRef(0)
226
+ const isFirstSlide = !activeIndex
227
+ const isLastSlide = activeIndex + 1 >= children.length
228
+ const panelNavigationTokens = {
229
+ ...staticTokens.stepTracker,
230
+ containerPaddingTop: spaceBetweenSlideAndPanelNavigation
231
+ }
232
+
233
+ const onContainerLayout = ({
234
+ nativeEvent: {
235
+ layout: { x, y, width }
236
+ }
237
+ }) => setContainerLayout((prevState) => ({ ...prevState, x, y, width }))
238
+
239
+ const onPreviousNextNavigationButtonLayout = ({
240
+ nativeEvent: {
241
+ layout: { width }
242
+ }
243
+ }) => setPreviousNextNavigationButtonWidth(width)
244
+
245
+ const updateOffset = React.useCallback(() => {
246
+ animatedX.current = containerLayout.width * activeIndex * -1
247
+ animatedY.current = 0
248
+ pan.setOffset({
249
+ x: animatedX.current,
250
+ y: animatedY.current
251
+ })
252
+ pan.setValue({ x: 0, y: 0 })
253
+ }, [activeIndex, containerLayout.width, pan, animatedX])
254
+
255
+ const animate = React.useCallback(
256
+ (toValue, toIndex) => {
257
+ const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
258
+ if (reduceMotionEnabled) {
259
+ Animated.timing(pan, { toValue, duration: 1, useNativeDriver: false }).start(
260
+ handleAnimationEndToIndex
261
+ )
262
+ } else {
263
+ Animated.spring(pan, {
264
+ ...springConfig,
265
+ toValue,
266
+ useNativeDriver: false
267
+ }).start(handleAnimationEndToIndex)
268
+ }
269
+ },
270
+ [pan, springConfig, reduceMotionEnabled, handleAnimationEnd]
271
+ )
272
+
273
+ const updateIndex = React.useCallback(
274
+ (delta = 1) => {
275
+ const toValue = { x: 0, y: 0 }
276
+ let skipChanges = !delta
277
+ let calcDelta = delta
278
+
279
+ if (activeIndex <= 0 && delta < 0) {
280
+ skipChanges = true
281
+ calcDelta = children.length + delta
282
+ } else if (activeIndex + 1 >= children.length && delta > 0) {
283
+ skipChanges = true
284
+ calcDelta = -1 * activeIndex + delta - 1
285
+ }
286
+
287
+ const index = activeIndex + calcDelta
288
+
289
+ if (skipChanges) {
290
+ animate(toValue, index)
291
+ return calcDelta
292
+ }
293
+
294
+ setActiveIndex(index)
295
+
296
+ toValue.x = containerLayout.width * -1 * calcDelta
297
+
298
+ animate(toValue, index)
299
+
300
+ if (onIndexChanged) onIndexChanged(calcDelta)
301
+ return calcDelta
302
+ },
303
+ [containerLayout.width, activeIndex, animate, children.length, onIndexChanged]
304
+ )
305
+
306
+ const fixOffsetAndGo = React.useCallback(
307
+ (delta) => {
308
+ updateOffset()
309
+ handleAnimationStart(activeIndex)
310
+ updateIndex(delta)
311
+ },
312
+ [updateIndex, updateOffset, activeIndex, handleAnimationStart]
313
+ )
314
+
315
+ const goToNeighboring = React.useCallback(
316
+ (toPrev = false) => {
317
+ fixOffsetAndGo(toPrev ? -1 : 1)
318
+ },
319
+ [fixOffsetAndGo]
320
+ )
321
+
322
+ const isSwipeAllowed = React.useCallback(() => {
323
+ if (Platform.OS === 'web') {
324
+ return !!(viewport === 'xs' || viewport === 'sm')
325
+ }
326
+ return true
327
+ }, [viewport])
328
+
329
+ const panResponder = React.useMemo(
330
+ () =>
331
+ PanResponder.create({
332
+ onPanResponderTerminationRequest: () => false,
333
+ onMoveShouldSetResponderCapture: () => true,
334
+ onMoveShouldSetPanResponderCapture: (_, gestureState) => {
335
+ if (!isSwipeAllowed()) {
336
+ return false
337
+ }
338
+
339
+ handleAnimationStart(activeIndex)
340
+
341
+ return Math.abs(gestureState.dx) > minDistanceToCapture
342
+ },
343
+ onPanResponderGrant: () => updateOffset(),
344
+ onPanResponderMove: Animated.event([null, { dx: pan.x }], {
345
+ useNativeDriver: false
346
+ }),
347
+ onPanResponderRelease: (_, gesture) => {
348
+ const correction = gesture.moveX - gesture.x0
349
+
350
+ if (Math.abs(correction) < containerLayout.width * minDistanceForAction) {
351
+ animate({ x: 0, y: 0 }, 0)
352
+ } else {
353
+ const delta = correction > 0 ? -1 : 1
354
+ updateIndex(delta)
355
+ }
356
+ }
357
+ }),
358
+ [
359
+ containerLayout.width,
360
+ updateIndex,
361
+ updateOffset,
362
+ animate,
363
+ isSwipeAllowed,
364
+ activeIndex,
365
+ minDistanceForAction,
366
+ handleAnimationStart,
367
+ minDistanceToCapture,
368
+ pan.x
369
+ ]
370
+ )
371
+
372
+ React.useEffect(() => {
373
+ pan.x.addListener(({ value }) => {
374
+ animatedX.current = value
375
+ })
376
+ pan.y.addListener(({ value }) => {
377
+ animatedY.current = value
378
+ })
379
+ return () => {
380
+ pan.x.removeAllListeners()
381
+ pan.y.removeAllListeners()
382
+ }
383
+ }, [pan.x, pan.y])
384
+
385
+ const goToNext = React.useCallback(() => {
386
+ goToNeighboring()
387
+ }, [goToNeighboring])
388
+
389
+ const goToPrev = React.useCallback(() => {
390
+ goToNeighboring(true)
391
+ }, [goToNeighboring])
392
+
393
+ const goTo = React.useCallback(
394
+ (index = 0) => {
395
+ const delta = index - activeIndex
396
+ if (delta) {
397
+ fixOffsetAndGo(delta)
398
+ }
399
+ },
400
+ [fixOffsetAndGo, activeIndex]
401
+ )
402
+
403
+ // @TODO: - these are Allium-theme variants and won't have any effect in themes that don't implement them.
404
+ // Normally we avoid setting variants of subcomponents, however this could be re-considered.
405
+ // Related discussion - https://github.com/telus/universal-design-system/issues/1549
406
+ const previousNextIconButtonVariants = { size: previousNextIconSize, raised: true }
407
+
408
+ const getCopyWithPlaceholders = (copyKey) => {
409
+ const copyText = getCopy(copyKey)
410
+ .replace(/%\{itemLabel\}/g, itemLabel)
411
+ .replace(/%\{stepNumber\}/g, activeIndex + 1)
412
+ .replace(/%\{stepCount\}/g, childrenArray.length)
413
+
414
+ // First word might be a lowercase placeholder: capitalize the first letter
415
+ return `${copyText[0].toUpperCase()}${copyText.slice(1)}`
416
+ }
417
+
418
+ return (
419
+ <CarouselProvider
420
+ activeIndex={activeIndex}
421
+ totalItems={childrenArray.length}
422
+ width={containerLayout.width}
423
+ goTo={goTo}
424
+ >
425
+ <View style={staticStyles.root} onLayout={onContainerLayout} ref={ref} {...systemProps}>
426
+ {showPreviousNextNavigation && (
427
+ <View
428
+ style={selectPreviousNextNavigationButtonStyles(
429
+ previousNextNavigationButtonWidth,
430
+ previousNextNavigationPosition,
431
+ spaceBetweenSlideAndPreviousNextNavigation,
432
+ isFirstSlide,
433
+ isLastSlide,
434
+ true
435
+ )}
436
+ testID="previous-button-container"
437
+ >
438
+ <IconButton
439
+ onLayout={onPreviousNextNavigationButtonLayout}
440
+ icon={previousIcon}
441
+ onPress={goToPrev}
442
+ variant={previousNextIconButtonVariants}
443
+ accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
444
+ '%{targetStep}',
445
+ activeIndex
446
+ )}
447
+ />
448
+ </View>
449
+ )}
450
+ <View style={selectContainerStyles(containerLayout.width)}>
451
+ <Animated.View
452
+ style={StyleSheet.flatten([
453
+ selectSwipeAreaStyles(children.length, containerLayout.width),
454
+ {
455
+ transform: [{ translateX: pan.x }, { translateY: pan.y }]
456
+ }
457
+ ])}
458
+ {...panResponder.panHandlers}
459
+ {...getA11yPropsFromHtmlTag(tag)}
460
+ >
461
+ {childrenArray.map((element, index) => {
462
+ const hidden = !isAnimating && index !== activeIndex
463
+ const clonedElement = React.cloneElement(element, { elementIndex: index, hidden })
464
+ return <React.Fragment key={index.toFixed(2)}>{clonedElement}</React.Fragment>
465
+ })}
466
+ </Animated.View>
467
+ </View>
468
+ {showPreviousNextNavigation && (
469
+ <View
470
+ style={selectPreviousNextNavigationButtonStyles(
471
+ previousNextNavigationButtonWidth,
472
+ previousNextNavigationPosition,
473
+ spaceBetweenSlideAndPreviousNextNavigation,
474
+ isFirstSlide,
475
+ isLastSlide,
476
+ false
477
+ )}
478
+ testID="next-button-container"
479
+ >
480
+ <IconButton
481
+ onLayout={onPreviousNextNavigationButtonLayout}
482
+ icon={nextIcon}
483
+ onPress={goToNext}
484
+ variant={previousNextIconButtonVariants}
485
+ accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
486
+ '%{targetStep}',
487
+ activeIndex + 2
488
+ )}
489
+ />
490
+ </View>
491
+ )}
492
+ </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}
511
+ </CarouselProvider>
512
+ )
513
+ }
514
+ )
515
+
516
+ Carousel.propTypes = {
517
+ ...selectedSystemPropTypes,
518
+ tokens: getTokensPropType('Carousel'),
519
+ variant: variantProp.propType,
520
+ /**
521
+ * Slides to render in Carousel. Wrap individual slides in `Carousel.Item`
522
+ */
523
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
524
+ /**
525
+ * Lowercase language-appropriate user-facing description of what each Carousel slide represents.
526
+ * This is used when generating item labels. For example, if a carousel contains offers,
527
+ * pass itemLabel="summer offer" (or copy="fr" and an appropriate French translation) to genereate
528
+ * accessible labels such as "Summer offer 1 of 3" and "Show summer offer 2 of 3".
529
+ */
530
+ itemLabel: PropTypes.string,
531
+ /**
532
+ * `inside` renders the previous and next buttons inside the slide
533
+ * `outside` renders the previous and next buttons outside the slide
534
+ * `edge` renders the previous and next buttons at the edge of the slide
535
+ */
536
+ previousNextNavigationPosition: PropTypes.oneOf(['inside', 'outside', 'edge']),
537
+ /**
538
+ * Defines the size of the `IconButton` which is being used to render next and previous buttons
539
+ */
540
+ previousNextIconSize: PropTypes.oneOf(['default', 'small', 'large']),
541
+ /**
542
+ * Carousel uses `Animated.spring` to animate slide changes, use this option to pass custom animation configuration
543
+ */
544
+ springConfig: PropTypes.object,
545
+ /**
546
+ * Minimal part of slide width must be swiped for changing index.
547
+ * Otherwise animation restore current slide. Default value 0.2 means that 20% must be swiped for change index
548
+ */
549
+ minDistanceForAction: PropTypes.number,
550
+ /**
551
+ * Initiate animation after swipe this distance.
552
+ */
553
+ minDistanceToCapture: PropTypes.number,
554
+ /**
555
+ * Called when active index changed
556
+ * This function is also provided with a parameter indicating changed index (either 1, or -1)
557
+ * Use it as follows:
558
+ * ```js
559
+ * const onIndexChangedCallback = React.useCallback((changedIndex) => {
560
+ * console.log(changedIndex)
561
+ * }, []) // pass local dependencies as per your component
562
+ * <Carousel
563
+ * onIndexChanged={onIndexChangedCallback}
564
+ * >
565
+ * <Carousel.Item>First Slide</Carousel.Item>
566
+ * </Carousel>
567
+ * ```
568
+ * Caution: Always consider wrapping your callback for `onIndexChanged` in `useCallback` in order to avoid bugs and performance issues
569
+ */
570
+ onIndexChanged: PropTypes.func,
571
+ /**
572
+ * Use this to render a custom panel navigation element instead of dots navigation
573
+ * This function is also provided with an object with the following properties
574
+ * activeIndex: index of current slide
575
+ * totalItems: total number of slides
576
+ * Use it as follows:
577
+ * ```js
578
+ * <Carousel
579
+ * onRenderPanelNavigation={({ totalItems, activeIndex }) => <Text>Showing {activeIndex + 1}</Text>}
580
+ * >
581
+ * <Carousel.Item>First Slide</Carousel.Item>
582
+ * </Carousel>
583
+ * ```
584
+ */
585
+ onRenderPanelNavigation: PropTypes.func,
586
+ /**
587
+ * When slide animation start
588
+ * This function is also provided with a parameter indicating the current slide index before animation starts
589
+ * Use it as follows:
590
+ * ```js
591
+ * const onAnimationStartCallback = React.useCallback((currentIndex) => {
592
+ * console.log(currentIndex)
593
+ * }, []) // pass local dependencies as per your component
594
+ * <Carousel
595
+ * onAnimationStart={onAnimationStartCallback}
596
+ * >
597
+ * <Carousel.Item>First Slide</Carousel.Item>
598
+ * </Carousel>
599
+ * ```
600
+ * Caution: Always consider wrapping your callback for `onAnimationStart` in `useCallback` in order to avoid bugs and performance issues
601
+ */
602
+ onAnimationStart: PropTypes.func,
603
+ /**
604
+ * When slide animation end with parameter of current index (after animation ends)
605
+ * This function is also provided with a parameter indicating the updated slide index after animation ends
606
+ * Use it as follows:
607
+ * ```js
608
+ * const onAnimationEndCallback = React.useCallback((changedIndex) => {
609
+ * console.log(changedIndex)
610
+ * }, []) // pass local dependencies as per your component
611
+ * <Carousel
612
+ * onAnimationEnd={onAnimationEndCallback}
613
+ * >
614
+ * <Carousel.Item>First Slide</Carousel.Item>
615
+ * </Carousel>
616
+ * ```
617
+ * Caution: Always consider wrapping your callback for `onAnimationEnd` in `useCallback` in order to avoid bugs and performance issues
618
+ */
619
+ onAnimationEnd: PropTypes.func,
620
+ /**
621
+ * Use this to override the default text for panel navigation
622
+ */
623
+ panelNavigationTextDictionary: PropTypes.shape({
624
+ en: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired }),
625
+ fr: PropTypes.shape({ stepTrackerLabel: PropTypes.string.isRequired })
626
+ }),
627
+ /**
628
+ * Provide custom accessibilityRole for Carousel container
629
+ */
630
+ accessibilityRole: PropTypes.string,
631
+ /**
632
+ * Provide custom accessibilityLabel for Carousel container
633
+ */
634
+ accessibilityLabel: PropTypes.string,
635
+ /**
636
+ * HTML tag to use for the Carousel item's immediate parent. Defaults to `'ul'` so that
637
+ * assistive technology tools know to intepret the carousel as a list.
638
+ *
639
+ * Note that if the immediate Carousel children do not all render as `'li'` elements,
640
+ * this should be changed (e.g. pass tag="div") because only 'li' is a valid child of 'ul'.
641
+ */
642
+ tag: PropTypes.oneOf(layoutTags)
643
+ }
644
+
645
+ Carousel.Item = CarouselItem
646
+
647
+ Carousel.displayName = 'Carousel'
648
+
649
+ 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 }