@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/component-docs.json +346 -51
  3. package/lib/Carousel/Carousel.js +81 -28
  4. package/lib/Carousel/CarouselItem/CarouselItem.js +24 -9
  5. package/lib/Carousel/dictionary.js +23 -0
  6. package/lib/InputSupports/InputSupports.js +10 -3
  7. package/lib/InputSupports/useInputSupports.js +3 -2
  8. package/lib/Modal/Modal.js +4 -0
  9. package/lib/Skeleton/Skeleton.js +1 -0
  10. package/lib/StepTracker/StepTracker.js +10 -10
  11. package/lib/TextInput/TextInput.js +3 -1
  12. package/lib/index.js +9 -0
  13. package/lib/utils/props/clickProps.js +2 -2
  14. package/lib/utils/props/handlerProps.js +77 -31
  15. package/lib/utils/useScrollBlocking.js +66 -0
  16. package/lib/utils/useScrollBlocking.native.js +11 -0
  17. package/lib-module/Carousel/Carousel.js +76 -29
  18. package/lib-module/Carousel/CarouselItem/CarouselItem.js +25 -10
  19. package/lib-module/Carousel/dictionary.js +16 -0
  20. package/lib-module/InputSupports/InputSupports.js +10 -3
  21. package/lib-module/InputSupports/useInputSupports.js +3 -2
  22. package/lib-module/Modal/Modal.js +3 -0
  23. package/lib-module/Skeleton/Skeleton.js +1 -0
  24. package/lib-module/StepTracker/StepTracker.js +9 -10
  25. package/lib-module/TextInput/TextInput.js +3 -1
  26. package/lib-module/index.js +1 -0
  27. package/lib-module/utils/props/clickProps.js +2 -2
  28. package/lib-module/utils/props/handlerProps.js +78 -31
  29. package/lib-module/utils/useScrollBlocking.js +58 -0
  30. package/lib-module/utils/useScrollBlocking.native.js +2 -0
  31. package/package.json +3 -3
  32. package/src/Carousel/Carousel.jsx +93 -30
  33. package/src/Carousel/CarouselItem/CarouselItem.jsx +26 -8
  34. package/src/Carousel/dictionary.js +16 -0
  35. package/src/InputSupports/InputSupports.jsx +18 -3
  36. package/src/InputSupports/useInputSupports.js +2 -2
  37. package/src/Modal/Modal.jsx +3 -1
  38. package/src/Skeleton/Skeleton.jsx +1 -0
  39. package/src/StepTracker/StepTracker.jsx +9 -3
  40. package/src/TextInput/TextInput.jsx +1 -1
  41. package/src/index.js +1 -0
  42. package/src/utils/props/clickProps.js +2 -2
  43. package/src/utils/props/handlerProps.js +64 -16
  44. package/src/utils/useScrollBlocking.js +57 -0
  45. package/src/utils/useScrollBlocking.native.js +2 -0
@@ -1,5 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
- export const focusHandlerProps = {
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
- export const textInputHandlerProps = {
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
- select: _ref2 => {
48
- let {
49
- onChange,
50
- onChangeText,
51
- onSubmit,
52
- onSubmitEditing
53
- } = _ref2;
54
- return {
55
- onChange,
56
- onChangeText,
57
- onSubmit,
58
- onSubmitEditing
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
- export default {
63
- focusHandlerProps,
64
- textInputHandlerProps
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;
@@ -0,0 +1,2 @@
1
+ // This is a no-op to emphasize that the original hook is web-only
2
+ export default (() => {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telus-uds/components-base",
3
- "version": "1.10.0",
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": "0.68.2",
50
- "react-native-web": "^0.17.0"
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 { getTokensPropType, variantProp, selectSystemProps, a11yProps, viewProps } from '../utils'
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
- panelNavigationTextDictionary = defaultPanelNavigationDictionary,
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, onAnimationEnd]
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
- if (onAnimationStart) onAnimationStart(activeIndex)
309
+ handleAnimationStart(activeIndex)
284
310
  updateIndex(delta)
285
311
  },
286
- [updateIndex, updateOffset, activeIndex, onAnimationStart]
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
- if (onAnimationStart) onAnimationStart(activeIndex)
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
- onAnimationStart,
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="previous-button"
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 clonedElement = React.cloneElement(element, { elementIndex: index })
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="next-button"
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
- dictionary={panelNavigationTextDictionary}
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 { selectSystemProps, a11yProps, viewProps } from '../../utils'
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, totalItems } = useCarousel()
19
+ const CarouselItem = ({ children, elementIndex, tag = 'li', hidden, ...rest }) => {
20
+ const { width, activeIndex } = useCarousel()
15
21
  const selectedProps = selectProps({
16
22
  ...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}`
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={{ width }} {...selectedProps} {...focusabilityProps}>
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
- { children, copy = 'en', label, hint, hintPosition = 'inline', feedback, tooltip, validation },
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
@@ -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
@@ -22,6 +22,7 @@ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, vie
22
22
  const selectSkeletonStyles = ({ color, radius, fadeAnimation }) => ({
23
23
  backgroundColor: color,
24
24
  borderRadius: radius,
25
+ maxWidth: '100%',
25
26
  ...fadeAnimation
26
27
  })
27
28
 
@@ -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: PropTypes.shape({ stepLabel: PropTypes.string, stepTrackerLabel: PropTypes.string }),
164
- fr: PropTypes.shape({ stepLabel: PropTypes.string, stepTrackerLabel: PropTypes.string })
169
+ en: dictionaryContentShape,
170
+ fr: dictionaryContentShape
165
171
  }),
166
172
  steps: PropTypes.arrayOf(PropTypes.string),
167
173
  tokens: getTokensPropType('StepTracker'),