@telus-uds/components-base 1.10.0 → 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.
- package/CHANGELOG.md +21 -3
- package/component-docs.json +346 -51
- package/lib/Carousel/Carousel.js +81 -28
- package/lib/Carousel/CarouselItem/CarouselItem.js +24 -9
- package/lib/Carousel/dictionary.js +23 -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 +10 -10
- package/lib/TextInput/TextInput.js +3 -1
- package/lib/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 +76 -29
- package/lib-module/Carousel/CarouselItem/CarouselItem.js +25 -10
- package/lib-module/Carousel/dictionary.js +16 -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 +9 -10
- package/lib-module/TextInput/TextInput.js +3 -1
- package/lib-module/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 +3 -3
- package/src/Carousel/Carousel.jsx +93 -30
- package/src/Carousel/CarouselItem/CarouselItem.jsx +26 -8
- package/src/Carousel/dictionary.js +16 -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 +9 -3
- package/src/TextInput/TextInput.jsx +1 -1
- package/src/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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
-
|
|
2
|
+
import Platform from "react-native-web/dist/exports/Platform";
|
|
3
|
+
import getPropSelector from './getPropSelector';
|
|
4
|
+
const focusHandlerProps = {
|
|
3
5
|
types: {
|
|
4
6
|
/**
|
|
5
7
|
* onBlur handler
|
|
@@ -10,19 +12,10 @@ export const focusHandlerProps = {
|
|
|
10
12
|
* onFocus handler
|
|
11
13
|
*/
|
|
12
14
|
onFocus: PropTypes.func
|
|
13
|
-
},
|
|
14
|
-
select: _ref => {
|
|
15
|
-
let {
|
|
16
|
-
onBlur,
|
|
17
|
-
onFocus
|
|
18
|
-
} = _ref;
|
|
19
|
-
return {
|
|
20
|
-
onBlur,
|
|
21
|
-
onFocus
|
|
22
|
-
};
|
|
23
15
|
}
|
|
24
16
|
};
|
|
25
|
-
|
|
17
|
+
focusHandlerProps.select = getPropSelector(focusHandlerProps.types);
|
|
18
|
+
const textInputHandlerProps = {
|
|
26
19
|
types: {
|
|
27
20
|
/**
|
|
28
21
|
* onChange handler
|
|
@@ -42,24 +35,78 @@ export const textInputHandlerProps = {
|
|
|
42
35
|
/**
|
|
43
36
|
* onSubmitEditing handler
|
|
44
37
|
*/
|
|
45
|
-
onSubmitEditing: PropTypes.func
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
38
|
+
onSubmitEditing: PropTypes.func,
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* onContentSizeChange handler
|
|
42
|
+
*/
|
|
43
|
+
onContentSizeChange: PropTypes.func,
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* onEndEditing handler
|
|
47
|
+
*/
|
|
48
|
+
onEndEditing: PropTypes.func,
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* onScroll handler
|
|
52
|
+
*/
|
|
53
|
+
onScroll: PropTypes.func,
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* onSelectionChange handler
|
|
57
|
+
*/
|
|
58
|
+
onSelectionChange: PropTypes.func,
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* onKeyPress handler
|
|
62
|
+
*/
|
|
63
|
+
onKeyPress: PropTypes.func,
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* onKeyUp handler (only supported on Web)
|
|
67
|
+
*/
|
|
68
|
+
onKeyUp: PropTypes.func,
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* onKeyDown handler (only supported on Web)
|
|
72
|
+
*/
|
|
73
|
+
onKeyDown: PropTypes.func
|
|
60
74
|
}
|
|
61
75
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
const selectTextInputHandlers = getPropSelector(textInputHandlerProps.types);
|
|
77
|
+
|
|
78
|
+
textInputHandlerProps.select = props => {
|
|
79
|
+
// Support for onKeyPress/onKeyUp/onKeyDown is inconsistent between React Native and React Native Web
|
|
80
|
+
const {
|
|
81
|
+
onKeyPress,
|
|
82
|
+
onKeyUp,
|
|
83
|
+
onKeyDown,
|
|
84
|
+
...resolvedProps
|
|
85
|
+
} = selectTextInputHandlers(props);
|
|
86
|
+
|
|
87
|
+
if (onKeyPress || onKeyUp || onKeyDown) {
|
|
88
|
+
if (Platform.OS !== 'web') {
|
|
89
|
+
// React Native only supports onKeyPress. Call any key handlers supplied in expected order.
|
|
90
|
+
resolvedProps.onKeyPress = event => {
|
|
91
|
+
if (typeof onKeyDown === 'function') onKeyDown(event);
|
|
92
|
+
if (typeof onKeyPress === 'function') onKeyPress(event);
|
|
93
|
+
if (typeof onKeyUp === 'function') onKeyUp(event);
|
|
94
|
+
};
|
|
95
|
+
} else {
|
|
96
|
+
// React Native Web supports onKeyUp the normal way.
|
|
97
|
+
if (onKeyUp) resolvedProps.onKeyUp = onKeyUp; // React Native Web doesn't support the `onKeyDown` prop name, but maps a supplied onKeyPress handler
|
|
98
|
+
// to the onKeyDown event and calls it with a keydown event. Make React Native Web call either or both.
|
|
99
|
+
|
|
100
|
+
if (onKeyPress || onKeyDown) {
|
|
101
|
+
resolvedProps.onKeyPress = event => {
|
|
102
|
+
if (typeof onKeyDown === 'function') onKeyDown(event);
|
|
103
|
+
if (typeof onKeyPress === 'function') onKeyPress(event);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return resolvedProps;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export { focusHandlerProps, textInputHandlerProps };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const addScrollBlocking = (preventScrolling, stopPropagation, ref) => {
|
|
4
|
+
var _ref$current;
|
|
5
|
+
|
|
6
|
+
document.body.addEventListener('touchmove', preventScrolling, {
|
|
7
|
+
passive: false
|
|
8
|
+
});
|
|
9
|
+
(_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.addEventListener('touchmove', stopPropagation);
|
|
10
|
+
document.body.style.overflow = 'hidden';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const removeScrollBlocking = (preventScrolling, stopPropagation, ref) => {
|
|
14
|
+
var _ref$current2;
|
|
15
|
+
|
|
16
|
+
document.body.removeEventListener('touchmove', preventScrolling);
|
|
17
|
+
(_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.removeEventListener('touchmove', stopPropagation);
|
|
18
|
+
document.body.style.overflow = 'inherit';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Disables scrolling when passed `true` or an array where all items are `true`.
|
|
22
|
+
*
|
|
23
|
+
* Returns an optional callback ref. Pass this to an element if it or its children
|
|
24
|
+
* should allow touch-based scrolling within that element's bounds.
|
|
25
|
+
*
|
|
26
|
+
* @param {boolean | boolean[]} conditionProps
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const useScrollBlocking = conditionProps => {
|
|
32
|
+
// useRef refs are null on first render and don't trigger a re-render when they get their
|
|
33
|
+
// element. Force re-run when ref mounts to ensure the stopPropagation listener is attached.
|
|
34
|
+
const ref = useRef();
|
|
35
|
+
const [refIsMounted, setRefIsMounted] = useState(false);
|
|
36
|
+
const callbackRef = useCallback(element => {
|
|
37
|
+
ref.current = element;
|
|
38
|
+
setRefIsMounted(Boolean(element));
|
|
39
|
+
}, []);
|
|
40
|
+
const conditionsMet = Array.isArray(conditionProps) ? conditionProps.every(condition => condition) : Boolean(conditionProps);
|
|
41
|
+
const preventScrolling = useCallback(event => event.preventDefault(), []);
|
|
42
|
+
const stopPropagation = useCallback(event => event.stopPropagation(), []);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const cleanup = () => removeScrollBlocking(preventScrolling, stopPropagation, ref);
|
|
45
|
+
|
|
46
|
+
if (conditionsMet) {
|
|
47
|
+
addScrollBlocking(preventScrolling, stopPropagation, ref);
|
|
48
|
+
} else {
|
|
49
|
+
cleanup();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cleanup; // preventScrolling and stopPropagation are stable callbacks with no deps, so this
|
|
53
|
+
// will re-run when conditionsMet or refIsMounted flip between true and false.
|
|
54
|
+
}, [preventScrolling, conditionsMet, stopPropagation, refIsMounted]);
|
|
55
|
+
return callbackRef;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default useScrollBlocking;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telus-uds/components-base",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Base components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base"
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": "^17.0.2",
|
|
48
48
|
"react-dom": "^17.0.2",
|
|
49
|
-
"react-native": "
|
|
50
|
-
"react-native-web": "
|
|
49
|
+
"react-native": "*",
|
|
50
|
+
"react-native-web": "~0.17.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@storybook/addon-a11y": "^6.5.6",
|
|
@@ -3,7 +3,16 @@ import { View, Animated, PanResponder, StyleSheet, Platform } from 'react-native
|
|
|
3
3
|
import PropTypes from 'prop-types'
|
|
4
4
|
import { useThemeTokens } from '../ThemeProvider'
|
|
5
5
|
import { useViewport } from '../ViewportProvider'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getTokensPropType,
|
|
8
|
+
getA11yPropsFromHtmlTag,
|
|
9
|
+
layoutTags,
|
|
10
|
+
variantProp,
|
|
11
|
+
selectSystemProps,
|
|
12
|
+
a11yProps,
|
|
13
|
+
viewProps,
|
|
14
|
+
useCopy
|
|
15
|
+
} from '../utils'
|
|
7
16
|
import { useA11yInfo } from '../A11yInfoProvider'
|
|
8
17
|
import { CarouselProvider } from './CarouselContext'
|
|
9
18
|
import CarouselItem from './CarouselItem'
|
|
@@ -11,6 +20,8 @@ import StepTracker from '../StepTracker'
|
|
|
11
20
|
import StackView from '../StackView'
|
|
12
21
|
import IconButton from '../IconButton'
|
|
13
22
|
|
|
23
|
+
import dictionary from './dictionary'
|
|
24
|
+
|
|
14
25
|
const staticStyles = StyleSheet.create({
|
|
15
26
|
root: {
|
|
16
27
|
backgroundColor: 'transparent',
|
|
@@ -79,15 +90,6 @@ const selectPreviousNextNavigationButtonStyles = (
|
|
|
79
90
|
return styles
|
|
80
91
|
}
|
|
81
92
|
|
|
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
93
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -150,6 +152,7 @@ const Carousel = React.forwardRef(
|
|
|
150
152
|
tokens,
|
|
151
153
|
variant,
|
|
152
154
|
children,
|
|
155
|
+
itemLabel = 'item',
|
|
153
156
|
previousNextNavigationPosition = 'inside',
|
|
154
157
|
previousNextIconSize = 'default',
|
|
155
158
|
minDistanceToCapture = 5,
|
|
@@ -159,9 +162,10 @@ const Carousel = React.forwardRef(
|
|
|
159
162
|
onIndexChanged,
|
|
160
163
|
springConfig = undefined,
|
|
161
164
|
onRenderPanelNavigation,
|
|
162
|
-
|
|
165
|
+
tag = 'ul',
|
|
163
166
|
accessibilityRole = 'adjustable',
|
|
164
167
|
accessibilityLabel = 'carousel',
|
|
168
|
+
copy,
|
|
165
169
|
...rest
|
|
166
170
|
},
|
|
167
171
|
ref
|
|
@@ -178,6 +182,25 @@ const Carousel = React.forwardRef(
|
|
|
178
182
|
viewport
|
|
179
183
|
})
|
|
180
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
|
+
|
|
181
204
|
const childrenArray = React.Children.toArray(children)
|
|
182
205
|
const systemProps = selectProps({
|
|
183
206
|
...rest,
|
|
@@ -230,18 +253,21 @@ const Carousel = React.forwardRef(
|
|
|
230
253
|
}, [activeIndex, containerLayout.width, pan, animatedX])
|
|
231
254
|
|
|
232
255
|
const animate = React.useCallback(
|
|
233
|
-
(toValue) => {
|
|
256
|
+
(toValue, toIndex) => {
|
|
257
|
+
const handleAnimationEndToIndex = (...args) => handleAnimationEnd(toIndex, ...args)
|
|
234
258
|
if (reduceMotionEnabled) {
|
|
235
|
-
Animated.timing(pan, { toValue, duration: 1, useNativeDriver: false }).start(
|
|
259
|
+
Animated.timing(pan, { toValue, duration: 1, useNativeDriver: false }).start(
|
|
260
|
+
handleAnimationEndToIndex
|
|
261
|
+
)
|
|
236
262
|
} else {
|
|
237
263
|
Animated.spring(pan, {
|
|
238
264
|
...springConfig,
|
|
239
265
|
toValue,
|
|
240
266
|
useNativeDriver: false
|
|
241
|
-
}).start()
|
|
267
|
+
}).start(handleAnimationEndToIndex)
|
|
242
268
|
}
|
|
243
269
|
},
|
|
244
|
-
[pan, springConfig, reduceMotionEnabled]
|
|
270
|
+
[pan, springConfig, reduceMotionEnabled, handleAnimationEnd]
|
|
245
271
|
)
|
|
246
272
|
|
|
247
273
|
const updateIndex = React.useCallback(
|
|
@@ -258,32 +284,32 @@ const Carousel = React.forwardRef(
|
|
|
258
284
|
calcDelta = -1 * activeIndex + delta - 1
|
|
259
285
|
}
|
|
260
286
|
|
|
287
|
+
const index = activeIndex + calcDelta
|
|
288
|
+
|
|
261
289
|
if (skipChanges) {
|
|
262
|
-
animate(toValue)
|
|
290
|
+
animate(toValue, index)
|
|
263
291
|
return calcDelta
|
|
264
292
|
}
|
|
265
293
|
|
|
266
|
-
const index = activeIndex + calcDelta
|
|
267
294
|
setActiveIndex(index)
|
|
268
295
|
|
|
269
296
|
toValue.x = containerLayout.width * -1 * calcDelta
|
|
270
297
|
|
|
271
|
-
animate(toValue)
|
|
298
|
+
animate(toValue, index)
|
|
272
299
|
|
|
273
300
|
if (onIndexChanged) onIndexChanged(calcDelta)
|
|
274
|
-
if (onAnimationEnd) onAnimationEnd(index)
|
|
275
301
|
return calcDelta
|
|
276
302
|
},
|
|
277
|
-
[containerLayout.width, activeIndex, animate, children.length, onIndexChanged
|
|
303
|
+
[containerLayout.width, activeIndex, animate, children.length, onIndexChanged]
|
|
278
304
|
)
|
|
279
305
|
|
|
280
306
|
const fixOffsetAndGo = React.useCallback(
|
|
281
307
|
(delta) => {
|
|
282
308
|
updateOffset()
|
|
283
|
-
|
|
309
|
+
handleAnimationStart(activeIndex)
|
|
284
310
|
updateIndex(delta)
|
|
285
311
|
},
|
|
286
|
-
[updateIndex, updateOffset, activeIndex,
|
|
312
|
+
[updateIndex, updateOffset, activeIndex, handleAnimationStart]
|
|
287
313
|
)
|
|
288
314
|
|
|
289
315
|
const goToNeighboring = React.useCallback(
|
|
@@ -310,7 +336,7 @@ const Carousel = React.forwardRef(
|
|
|
310
336
|
return false
|
|
311
337
|
}
|
|
312
338
|
|
|
313
|
-
|
|
339
|
+
handleAnimationStart(activeIndex)
|
|
314
340
|
|
|
315
341
|
return Math.abs(gestureState.dx) > minDistanceToCapture
|
|
316
342
|
},
|
|
@@ -322,7 +348,7 @@ const Carousel = React.forwardRef(
|
|
|
322
348
|
const correction = gesture.moveX - gesture.x0
|
|
323
349
|
|
|
324
350
|
if (Math.abs(correction) < containerLayout.width * minDistanceForAction) {
|
|
325
|
-
animate({ x: 0, y: 0 })
|
|
351
|
+
animate({ x: 0, y: 0 }, 0)
|
|
326
352
|
} else {
|
|
327
353
|
const delta = correction > 0 ? -1 : 1
|
|
328
354
|
updateIndex(delta)
|
|
@@ -337,7 +363,7 @@ const Carousel = React.forwardRef(
|
|
|
337
363
|
isSwipeAllowed,
|
|
338
364
|
activeIndex,
|
|
339
365
|
minDistanceForAction,
|
|
340
|
-
|
|
366
|
+
handleAnimationStart,
|
|
341
367
|
minDistanceToCapture,
|
|
342
368
|
pan.x
|
|
343
369
|
]
|
|
@@ -379,6 +405,16 @@ const Carousel = React.forwardRef(
|
|
|
379
405
|
// Related discussion - https://github.com/telus/universal-design-system/issues/1549
|
|
380
406
|
const previousNextIconButtonVariants = { size: previousNextIconSize, raised: true }
|
|
381
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
|
+
|
|
382
418
|
return (
|
|
383
419
|
<CarouselProvider
|
|
384
420
|
activeIndex={activeIndex}
|
|
@@ -404,7 +440,10 @@ const Carousel = React.forwardRef(
|
|
|
404
440
|
icon={previousIcon}
|
|
405
441
|
onPress={goToPrev}
|
|
406
442
|
variant={previousNextIconButtonVariants}
|
|
407
|
-
accessibilityLabel=
|
|
443
|
+
accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
|
|
444
|
+
'%{targetStep}',
|
|
445
|
+
activeIndex
|
|
446
|
+
)}
|
|
408
447
|
/>
|
|
409
448
|
</View>
|
|
410
449
|
)}
|
|
@@ -417,9 +456,11 @@ const Carousel = React.forwardRef(
|
|
|
417
456
|
}
|
|
418
457
|
])}
|
|
419
458
|
{...panResponder.panHandlers}
|
|
459
|
+
{...getA11yPropsFromHtmlTag(tag)}
|
|
420
460
|
>
|
|
421
461
|
{childrenArray.map((element, index) => {
|
|
422
|
-
const
|
|
462
|
+
const hidden = !isAnimating && index !== activeIndex
|
|
463
|
+
const clonedElement = React.cloneElement(element, { elementIndex: index, hidden })
|
|
423
464
|
return <React.Fragment key={index.toFixed(2)}>{clonedElement}</React.Fragment>
|
|
424
465
|
})}
|
|
425
466
|
</Animated.View>
|
|
@@ -441,7 +482,10 @@ const Carousel = React.forwardRef(
|
|
|
441
482
|
icon={nextIcon}
|
|
442
483
|
onPress={goToNext}
|
|
443
484
|
variant={previousNextIconButtonVariants}
|
|
444
|
-
accessibilityLabel=
|
|
485
|
+
accessibilityLabel={getCopyWithPlaceholders('iconButtonLabel').replace(
|
|
486
|
+
'%{targetStep}',
|
|
487
|
+
activeIndex + 2
|
|
488
|
+
)}
|
|
445
489
|
/>
|
|
446
490
|
</View>
|
|
447
491
|
)}
|
|
@@ -454,7 +498,11 @@ const Carousel = React.forwardRef(
|
|
|
454
498
|
<StepTracker
|
|
455
499
|
current={activeIndex}
|
|
456
500
|
steps={childrenArray.map((_, index) => String(index))}
|
|
457
|
-
|
|
501
|
+
copy={{
|
|
502
|
+
// Give StepTracker copy from Carousel's language and dictionary
|
|
503
|
+
stepLabel: getCopyWithPlaceholders('stepLabel'),
|
|
504
|
+
stepTrackerLabel: getCopyWithPlaceholders('stepTrackerLabel')
|
|
505
|
+
}}
|
|
458
506
|
tokens={panelNavigationTokens}
|
|
459
507
|
/>
|
|
460
508
|
)}
|
|
@@ -473,6 +521,13 @@ Carousel.propTypes = {
|
|
|
473
521
|
* Slides to render in Carousel. Wrap individual slides in `Carousel.Item`
|
|
474
522
|
*/
|
|
475
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,
|
|
476
531
|
/**
|
|
477
532
|
* `inside` renders the previous and next buttons inside the slide
|
|
478
533
|
* `outside` renders the previous and next buttons outside the slide
|
|
@@ -576,7 +631,15 @@ Carousel.propTypes = {
|
|
|
576
631
|
/**
|
|
577
632
|
* Provide custom accessibilityLabel for Carousel container
|
|
578
633
|
*/
|
|
579
|
-
accessibilityLabel: PropTypes.string
|
|
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)
|
|
580
643
|
}
|
|
581
644
|
|
|
582
645
|
Carousel.Item = CarouselItem
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
3
|
import { View, Platform } from 'react-native'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
layoutTags,
|
|
6
|
+
getA11yPropsFromHtmlTag,
|
|
7
|
+
selectSystemProps,
|
|
8
|
+
a11yProps,
|
|
9
|
+
viewProps
|
|
10
|
+
} from '../../utils'
|
|
5
11
|
import { useCarousel } from '../CarouselContext'
|
|
6
12
|
|
|
7
13
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
@@ -10,17 +16,21 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
|
|
|
10
16
|
* `Carousel.Item` is used to wrap the content of an individual slide and is suppsoed to be the
|
|
11
17
|
* only top-level component passed to the `Carousel`
|
|
12
18
|
*/
|
|
13
|
-
const CarouselItem = ({ children, elementIndex, ...rest }) => {
|
|
14
|
-
const { width, activeIndex
|
|
19
|
+
const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) => {
|
|
20
|
+
const { width, activeIndex } = useCarousel()
|
|
15
21
|
const selectedProps = selectProps({
|
|
16
22
|
...rest,
|
|
17
|
-
|
|
18
|
-
accessibilityRole: Platform.OS === 'android' ? 'none' : 'group',
|
|
19
|
-
accessibilityLabel: `Showing ${elementIndex + 1} of ${totalItems}`
|
|
23
|
+
...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
|
|
20
24
|
})
|
|
25
|
+
|
|
21
26
|
const focusabilityProps = activeIndex === elementIndex ? {} : a11yProps.nonFocusableProps
|
|
27
|
+
const style = { width }
|
|
28
|
+
if (hidden && Platform.OS === 'web') {
|
|
29
|
+
// On web, visibility: hidden makes all children non-focusable. It doesn't exist on native.
|
|
30
|
+
style.visibility = 'hidden'
|
|
31
|
+
}
|
|
22
32
|
return (
|
|
23
|
-
<View style={
|
|
33
|
+
<View style={style} {...selectedProps} {...focusabilityProps}>
|
|
24
34
|
{children}
|
|
25
35
|
</View>
|
|
26
36
|
)
|
|
@@ -40,7 +50,15 @@ CarouselItem.propTypes = {
|
|
|
40
50
|
/**
|
|
41
51
|
* Content of the slide
|
|
42
52
|
*/
|
|
43
|
-
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
|
|
53
|
+
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
|
54
|
+
/**
|
|
55
|
+
* Sets the HTML tag of the outer container. By default `'li'` so that assistive technology sees
|
|
56
|
+
* the Carousel as a list of items.
|
|
57
|
+
*
|
|
58
|
+
* Carousel's innermost container defaults to `'ul'` which can be overridden. If the tag of either
|
|
59
|
+
* `Carousel` or `Carousel.Item` is overriden, the other should be too, to avoid producing invalid HTML.
|
|
60
|
+
*/
|
|
61
|
+
tag: PropTypes.oneOf(layoutTags)
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
CarouselItem.displayName = 'Carousel.Item'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 'stepLabel' and 'stepTrackerLabel' are passed down to StepTracker
|
|
2
|
+
export default {
|
|
3
|
+
en: {
|
|
4
|
+
carouselLabel: '%{stepCount} items',
|
|
5
|
+
iconButtonLabel: 'Show %{itemLabel} %{targetStep} of %{stepCount}',
|
|
6
|
+
stepLabel: '%{itemLabel} %{stepNumber}',
|
|
7
|
+
stepTrackerLabel: '%{itemLabel} %{stepNumber} of %{stepCount}'
|
|
8
|
+
},
|
|
9
|
+
fr: {
|
|
10
|
+
// TODO: French translations here
|
|
11
|
+
carouselLabel: '(fr) %{stepCount} items',
|
|
12
|
+
iconButtonLabel: '(fr) Show %{itemLabel} %{targetStep} of %{stepCount}',
|
|
13
|
+
stepLabel: '(fr) %{itemLabel} %{stepNumber}',
|
|
14
|
+
stepTrackerLabel: '(fr) %{itemLabel} %{stepNumber} of %{stepCount}'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -9,7 +9,17 @@ import useInputSupports from './useInputSupports'
|
|
|
9
9
|
|
|
10
10
|
const InputSupports = forwardRef(
|
|
11
11
|
(
|
|
12
|
-
{
|
|
12
|
+
{
|
|
13
|
+
children,
|
|
14
|
+
copy = 'en',
|
|
15
|
+
label,
|
|
16
|
+
hint,
|
|
17
|
+
hintPosition = 'inline',
|
|
18
|
+
feedback,
|
|
19
|
+
tooltip,
|
|
20
|
+
validation,
|
|
21
|
+
nativeID
|
|
22
|
+
},
|
|
13
23
|
ref
|
|
14
24
|
) => {
|
|
15
25
|
const { space } = useThemeTokens('InputSupports')
|
|
@@ -18,7 +28,8 @@ const InputSupports = forwardRef(
|
|
|
18
28
|
feedback,
|
|
19
29
|
hint,
|
|
20
30
|
label,
|
|
21
|
-
validation
|
|
31
|
+
validation,
|
|
32
|
+
nativeID
|
|
22
33
|
})
|
|
23
34
|
|
|
24
35
|
return (
|
|
@@ -72,7 +83,11 @@ InputSupports.propTypes = {
|
|
|
72
83
|
/**
|
|
73
84
|
* Use to visually mark an input as valid or invalid.
|
|
74
85
|
*/
|
|
75
|
-
validation: PropTypes.oneOf(['error', 'success'])
|
|
86
|
+
validation: PropTypes.oneOf(['error', 'success']),
|
|
87
|
+
/**
|
|
88
|
+
* ID for DOM element on web
|
|
89
|
+
*/
|
|
90
|
+
nativeID: PropTypes.string
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
export default InputSupports
|
|
@@ -2,7 +2,7 @@ import useUniqueId from '../utils/useUniqueId'
|
|
|
2
2
|
|
|
3
3
|
const joinDefined = (array) => array.filter((item) => item !== undefined).join(' ')
|
|
4
4
|
|
|
5
|
-
const useInputSupports = ({ label, feedback, validation, hint }) => {
|
|
5
|
+
const useInputSupports = ({ label, feedback, validation, hint, nativeID }) => {
|
|
6
6
|
const hasValidationError = validation === 'error'
|
|
7
7
|
|
|
8
8
|
const inputId = useUniqueId('input')
|
|
@@ -20,7 +20,7 @@ const useInputSupports = ({ label, feedback, validation, hint }) => {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
return {
|
|
23
|
-
inputId,
|
|
23
|
+
inputId: nativeID || inputId,
|
|
24
24
|
hintId,
|
|
25
25
|
feedbackId,
|
|
26
26
|
a11yProps
|
package/src/Modal/Modal.jsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useViewport } from '../ViewportProvider'
|
|
23
23
|
import IconButton from '../IconButton'
|
|
24
24
|
import dictionary from './dictionary'
|
|
25
|
+
import useScrollBlocking from '../utils/useScrollBlocking'
|
|
25
26
|
|
|
26
27
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps])
|
|
27
28
|
|
|
@@ -89,6 +90,7 @@ const Modal = forwardRef(
|
|
|
89
90
|
({ children, isOpen, onClose, maxWidth, tokens, variant, copy, closeButton, ...rest }, ref) => {
|
|
90
91
|
const viewport = useViewport()
|
|
91
92
|
const themeTokens = useThemeTokens('Modal', tokens, variant, { viewport, maxWidth })
|
|
93
|
+
const modalRef = useScrollBlocking(isOpen)
|
|
92
94
|
|
|
93
95
|
const { closeIcon: CloseIconComponent } = themeTokens
|
|
94
96
|
|
|
@@ -113,7 +115,7 @@ const Modal = forwardRef(
|
|
|
113
115
|
|
|
114
116
|
return (
|
|
115
117
|
<NativeModal transparent {...selectProps(rest)}>
|
|
116
|
-
<View style={[staticStyles.positioningContainer]}>
|
|
118
|
+
<View style={[staticStyles.positioningContainer]} ref={modalRef}>
|
|
117
119
|
<View
|
|
118
120
|
style={[staticStyles.sizingContainer, selectContainerStyles(themeTokens)]}
|
|
119
121
|
pointerEvents="box-none" // don't capture backdrop press events
|
|
@@ -155,13 +155,19 @@ const StepTracker = forwardRef(
|
|
|
155
155
|
)
|
|
156
156
|
StepTracker.displayName = 'StepTracker'
|
|
157
157
|
|
|
158
|
+
// If a language dictionary entry is provided, it must contain every key
|
|
159
|
+
const dictionaryContentShape = PropTypes.shape({
|
|
160
|
+
stepLabel: PropTypes.string.isRequired,
|
|
161
|
+
stepTrackerLabel: PropTypes.string.isRequired
|
|
162
|
+
})
|
|
163
|
+
|
|
158
164
|
StepTracker.propTypes = {
|
|
159
165
|
...selectedSystemPropTypes,
|
|
160
166
|
current: PropTypes.number,
|
|
161
|
-
copy: PropTypes.oneOf(['en', 'fr']),
|
|
167
|
+
copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), dictionaryContentShape]),
|
|
162
168
|
dictionary: PropTypes.shape({
|
|
163
|
-
en:
|
|
164
|
-
fr:
|
|
169
|
+
en: dictionaryContentShape,
|
|
170
|
+
fr: dictionaryContentShape
|
|
165
171
|
}),
|
|
166
172
|
steps: PropTypes.arrayOf(PropTypes.string),
|
|
167
173
|
tokens: getTokensPropType('StepTracker'),
|