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