@telus-uds/components-base 4.0.0-alpha.0 → 4.0.0-alpha.2
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 +62 -6
- package/jest.config.cjs +1 -0
- package/lib/cjs/ActionCard/ActionCard.js +10 -3
- package/lib/cjs/Card/Card.js +3 -3
- package/lib/cjs/Carousel/Carousel.js +6 -1
- package/lib/cjs/Carousel/CarouselThumbnail.js +125 -33
- package/lib/cjs/Carousel/CarouselThumbnailNavigation.js +8 -1
- package/lib/cjs/Footnote/FootnoteLink.js +18 -17
- package/lib/cjs/InputSupports/InputSupports.js +1 -2
- package/lib/cjs/InputSupports/useInputSupports.js +1 -3
- package/lib/cjs/Link/ChevronLink.js +1 -2
- package/lib/cjs/Notification/Notification.js +4 -12
- package/lib/cjs/Progress/Progress.js +30 -5
- package/lib/cjs/Progress/ProgressBar.js +3 -3
- package/lib/cjs/Progress/ProgressBarBackground.js +2 -3
- package/lib/cjs/QuickLinks/QuickLinks.js +7 -0
- package/lib/cjs/Scroll/Scroll.js +472 -0
- package/lib/cjs/Scroll/ScrollContext.js +55 -0
- package/lib/cjs/Scroll/ScrollItem.js +106 -0
- package/lib/cjs/Scroll/ScrollProgress.js +100 -0
- package/lib/cjs/Scroll/dictionary.js +18 -0
- package/lib/cjs/StackView/StackWrapBox.js +9 -1
- package/lib/cjs/StackView/StackWrapGap.js +3 -1
- package/lib/cjs/StackView/getStackedContent.js +20 -10
- package/lib/cjs/index.js +14 -0
- package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
- package/lib/cjs/utils/children.js +2 -1
- package/lib/esm/ActionCard/ActionCard.js +10 -3
- package/lib/esm/Card/Card.js +3 -3
- package/lib/esm/Carousel/Carousel.js +6 -1
- package/lib/esm/Carousel/CarouselThumbnail.js +126 -34
- package/lib/esm/Carousel/CarouselThumbnailNavigation.js +8 -1
- package/lib/esm/Footnote/FootnoteLink.js +18 -17
- package/lib/esm/InputSupports/InputSupports.js +1 -2
- package/lib/esm/InputSupports/useInputSupports.js +1 -3
- package/lib/esm/Link/ChevronLink.js +1 -2
- package/lib/esm/Notification/Notification.js +4 -12
- package/lib/esm/Progress/Progress.js +30 -5
- package/lib/esm/Progress/ProgressBar.js +3 -3
- package/lib/esm/Progress/ProgressBarBackground.js +2 -3
- package/lib/esm/QuickLinks/QuickLinks.js +7 -0
- package/lib/esm/Scroll/Scroll.js +465 -0
- package/lib/esm/Scroll/ScrollContext.js +46 -0
- package/lib/esm/Scroll/ScrollItem.js +100 -0
- package/lib/esm/Scroll/ScrollProgress.js +93 -0
- package/lib/esm/Scroll/dictionary.js +12 -0
- package/lib/esm/StackView/StackWrapBox.js +9 -1
- package/lib/esm/StackView/StackWrapGap.js +3 -1
- package/lib/esm/StackView/getStackedContent.js +20 -10
- package/lib/esm/index.js +2 -0
- package/lib/esm/utils/animation/useAnimatedValue.js +38 -0
- package/lib/esm/utils/children.js +2 -1
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/ActionCard/ActionCard.jsx +13 -4
- package/src/Card/Card.jsx +3 -3
- package/src/Carousel/Carousel.jsx +6 -1
- package/src/Carousel/CarouselThumbnail.jsx +117 -30
- package/src/Carousel/CarouselThumbnailNavigation.jsx +8 -2
- package/src/Footnote/FootnoteLink.jsx +17 -12
- package/src/InputSupports/InputSupports.jsx +1 -6
- package/src/InputSupports/useInputSupports.js +1 -1
- package/src/Link/ChevronLink.jsx +1 -2
- package/src/Notification/Notification.jsx +5 -21
- package/src/Progress/Progress.jsx +22 -3
- package/src/Progress/ProgressBar.jsx +3 -3
- package/src/Progress/ProgressBarBackground.jsx +2 -3
- package/src/QuickLinks/QuickLinks.jsx +8 -0
- package/src/Scroll/Scroll.jsx +483 -0
- package/src/Scroll/ScrollContext.jsx +39 -0
- package/src/Scroll/ScrollItem.jsx +87 -0
- package/src/Scroll/ScrollProgress.jsx +74 -0
- package/src/Scroll/dictionary.js +12 -0
- package/src/StackView/StackWrapBox.jsx +13 -1
- package/src/StackView/StackWrapGap.jsx +2 -1
- package/src/StackView/getStackedContent.jsx +22 -8
- package/src/index.js +2 -0
- package/src/utils/animation/useAnimatedValue.js +35 -0
- package/src/utils/children.jsx +4 -1
- package/types/Link.d.ts +1 -2
- package/types/Scroll.d.ts +66 -0
- package/types/index.d.ts +9 -0
|
@@ -25,9 +25,7 @@ export const useInputSupports = _ref => {
|
|
|
25
25
|
accessibilityLabel: label,
|
|
26
26
|
accessibilityHint: joinDefined([!hasValidationError && feedback, hint, maxCharacterAllowed ? getCopy('maxCharacters').replace(/%\{charCount\}/g, maxCharacterAllowed) : undefined]),
|
|
27
27
|
// native only -> replaced with describedBy on web
|
|
28
|
-
accessibilityDescribedBy: joinDefined([
|
|
29
|
-
// feedback receives a11yRole=alert on error, so there's no need to include it here
|
|
30
|
-
hint && hintId, charactersCount ? getCopy('charactersRemaining').replace(/%\{charCount\}/g, charactersCount) : undefined]),
|
|
28
|
+
accessibilityDescribedBy: joinDefined([hint && hintId, feedback && feedbackId || undefined, charactersCount ? getCopy('charactersRemaining').replace(/%\{charCount\}/g, charactersCount) : undefined]),
|
|
31
29
|
accessibilityInvalid: hasValidationError
|
|
32
30
|
};
|
|
33
31
|
return {
|
|
@@ -57,8 +57,7 @@ ChevronLink.propTypes = {
|
|
|
57
57
|
children: PropTypes.node,
|
|
58
58
|
variant: PropTypes.exact({
|
|
59
59
|
size: PropTypes.oneOf(['large', 'small', 'micro']),
|
|
60
|
-
|
|
61
|
-
inverse: PropTypes.bool
|
|
60
|
+
style: PropTypes.oneOf(['inline', 'subtle', 'feature', 'danger', 'inverse'])
|
|
62
61
|
}),
|
|
63
62
|
...linkProps.types,
|
|
64
63
|
tokens: getTokensPropType('ChevronLink', 'Link'),
|
|
@@ -248,11 +248,10 @@ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, view
|
|
|
248
248
|
* ### Variants
|
|
249
249
|
* - Use `variant.style` to set the visual style of the notification
|
|
250
250
|
* - Use `dismissible` prop to enable dismissible functionality
|
|
251
|
-
* - Use `system` prop to set the visual style of the notification and denote an announcement from the system or application
|
|
252
251
|
*
|
|
253
|
-
* ### When to use the system
|
|
254
|
-
* - Use `system
|
|
255
|
-
* - Don’t use `system` when the message is in response to user action
|
|
252
|
+
* ### When to use the system style variant?
|
|
253
|
+
* - Use `variant.style ‘system’` to show system-based messages coming from the application
|
|
254
|
+
* - Don’t use the `system` style variant when the message is in response to user action
|
|
256
255
|
*
|
|
257
256
|
* ## Variants
|
|
258
257
|
*
|
|
@@ -285,7 +284,6 @@ const getDefaultStyles = (themeTokens, themeOptions, maxWidth, dismissible, view
|
|
|
285
284
|
export const Notification = /*#__PURE__*/React.forwardRef((_ref7, ref) => {
|
|
286
285
|
let {
|
|
287
286
|
children,
|
|
288
|
-
system,
|
|
289
287
|
dismissible,
|
|
290
288
|
copy = 'en',
|
|
291
289
|
tokens,
|
|
@@ -300,9 +298,7 @@ export const Notification = /*#__PURE__*/React.forwardRef((_ref7, ref) => {
|
|
|
300
298
|
dictionary,
|
|
301
299
|
copy
|
|
302
300
|
});
|
|
303
|
-
|
|
304
|
-
// TODO: Remove this once the system style variant is deprecated
|
|
305
|
-
const isSystemEnabled = system || variant?.style?.includes('system');
|
|
301
|
+
const isSystemEnabled = variant?.style?.includes('system');
|
|
306
302
|
const {
|
|
307
303
|
themeOptions
|
|
308
304
|
} = useTheme();
|
|
@@ -418,10 +414,6 @@ Notification.propTypes = {
|
|
|
418
414
|
* Content of the `Notification`.
|
|
419
415
|
*/
|
|
420
416
|
children: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.func]),
|
|
421
|
-
/**
|
|
422
|
-
* Use `system` prop to set the visual style of the notification and denote an announcement from the system or application
|
|
423
|
-
*/
|
|
424
|
-
system: PropTypes.bool,
|
|
425
417
|
/**
|
|
426
418
|
* Use the `dismissible` prop to allow users to dismiss the Notification at any time.
|
|
427
419
|
*/
|
|
@@ -12,6 +12,8 @@ import { viewProps } from '../utils/props/viewProps';
|
|
|
12
12
|
import { ProgressContext } from './ProgressContext';
|
|
13
13
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
14
|
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
|
|
15
|
+
const BORDER_SIDES = 2;
|
|
16
|
+
const CENTER_DIVISOR = 2;
|
|
15
17
|
const selectProgressStyles = _ref => {
|
|
16
18
|
let {
|
|
17
19
|
backgroundColor,
|
|
@@ -30,6 +32,21 @@ const selectProgressStyles = _ref => {
|
|
|
30
32
|
...applyShadowToken(shadow)
|
|
31
33
|
};
|
|
32
34
|
};
|
|
35
|
+
const selectBarWrapperStyles = _ref2 => {
|
|
36
|
+
let {
|
|
37
|
+
barHeight,
|
|
38
|
+
borderRadius,
|
|
39
|
+
borderWidth,
|
|
40
|
+
height
|
|
41
|
+
} = _ref2;
|
|
42
|
+
return {
|
|
43
|
+
height: barHeight,
|
|
44
|
+
width: '100%',
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
top: (height - borderWidth * BORDER_SIDES - barHeight) / CENTER_DIVISOR,
|
|
47
|
+
borderRadius
|
|
48
|
+
};
|
|
49
|
+
};
|
|
33
50
|
|
|
34
51
|
/**
|
|
35
52
|
* The `Progress` is a container for displaying one or several `ProgressBar`s.
|
|
@@ -43,10 +60,11 @@ const selectProgressStyles = _ref => {
|
|
|
43
60
|
*
|
|
44
61
|
* - Use the following tokens to customize the appearance:
|
|
45
62
|
* - `backgroundColor` for the background color of the progress container,
|
|
63
|
+
* - `barHeight` to control the height of the progress bars displayed within the container,
|
|
46
64
|
* - `borderColor` to control the color of the border,
|
|
47
65
|
* - `borderRadius` for the rounded corners,
|
|
48
66
|
* - `borderWidth` to change the border width.
|
|
49
|
-
* - `height` to control the height of the progress
|
|
67
|
+
* - `height` to control the height of the progress bar container.
|
|
50
68
|
* - `shadow` to apply a box shadow to the progress bar container.
|
|
51
69
|
*
|
|
52
70
|
* ## Variants
|
|
@@ -67,16 +85,23 @@ const selectProgressStyles = _ref => {
|
|
|
67
85
|
* role.
|
|
68
86
|
*
|
|
69
87
|
*/
|
|
70
|
-
export const Progress = /*#__PURE__*/React.forwardRef((
|
|
88
|
+
export const Progress = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
|
|
71
89
|
let {
|
|
72
90
|
children,
|
|
73
91
|
tokens,
|
|
74
92
|
variant,
|
|
75
93
|
...rest
|
|
76
|
-
} =
|
|
94
|
+
} = _ref3;
|
|
77
95
|
const themeTokens = useThemeTokens('Progress', tokens, variant);
|
|
78
|
-
// Default to false (vertical layout) to preserve existing behavior and avoid breaking changes
|
|
79
96
|
const layers = variant?.layers ?? false;
|
|
97
|
+
const {
|
|
98
|
+
barHeight,
|
|
99
|
+
height
|
|
100
|
+
} = themeTokens;
|
|
101
|
+
const content = barHeight && barHeight !== height ? /*#__PURE__*/_jsx(View, {
|
|
102
|
+
style: selectBarWrapperStyles(themeTokens),
|
|
103
|
+
children: children
|
|
104
|
+
}) : children;
|
|
80
105
|
return /*#__PURE__*/_jsx(ProgressContext.Provider, {
|
|
81
106
|
value: {
|
|
82
107
|
layers
|
|
@@ -85,7 +110,7 @@ export const Progress = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
|
|
|
85
110
|
ref: ref,
|
|
86
111
|
style: [staticStyles.progressContainer, selectProgressStyles(themeTokens)],
|
|
87
112
|
...selectProps(rest),
|
|
88
|
-
children:
|
|
113
|
+
children: content
|
|
89
114
|
})
|
|
90
115
|
});
|
|
91
116
|
});
|
|
@@ -62,9 +62,9 @@ const selectBarStyles = _ref => {
|
|
|
62
62
|
*
|
|
63
63
|
* ## Variants
|
|
64
64
|
*
|
|
65
|
-
* - Use
|
|
66
|
-
* - `negative
|
|
67
|
-
* - `inactive
|
|
65
|
+
* - Use `variant={{ style: 'value' }}` to render specific types of progress bars:
|
|
66
|
+
* - `style: 'negative'` for negative theming for progress bar filling,
|
|
67
|
+
* - `style: 'inactive'` to style the progress bar as inactive.
|
|
68
68
|
*
|
|
69
69
|
* ## Usability and A11y guidelines
|
|
70
70
|
*
|
|
@@ -11,10 +11,9 @@ export const ProgressBarBackground = /*#__PURE__*/React.forwardRef((_ref, ref) =
|
|
|
11
11
|
variant
|
|
12
12
|
} = _ref;
|
|
13
13
|
let source = null;
|
|
14
|
-
|
|
15
|
-
if (variant?.inactive || variant?.style === INACTIVE_VARIANT) {
|
|
14
|
+
if (variant?.style === INACTIVE_VARIANT) {
|
|
16
15
|
source = inactiveBackground;
|
|
17
|
-
} else if (variant?.
|
|
16
|
+
} else if (variant?.style === NEGATIVE_VARIANT) {
|
|
18
17
|
source = negativeBackground;
|
|
19
18
|
} else {
|
|
20
19
|
return null;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
+
import Platform from "react-native-web/dist/exports/Platform";
|
|
3
4
|
import { useThemeTokens } from '../ThemeProvider/useThemeTokens';
|
|
4
5
|
import { useViewport } from '../ViewportProvider/useViewport';
|
|
5
6
|
import { getTokensPropType } from '../utils/props/tokens';
|
|
6
7
|
import { variantProp } from '../utils/props/variantProp';
|
|
8
|
+
import { tagsToRoles } from '../utils/a11y/semantics';
|
|
7
9
|
import { List } from '../List/List';
|
|
8
10
|
import { StackWrap } from '../StackView/StackWrap';
|
|
9
11
|
import { QuickLinksCard } from './QuickLinksCard';
|
|
@@ -36,6 +38,10 @@ export const QuickLinks = /*#__PURE__*/React.forwardRef((_ref, ref) => {
|
|
|
36
38
|
} = useThemeTokens('QuickLinks', tokens, variant, {
|
|
37
39
|
viewport
|
|
38
40
|
});
|
|
41
|
+
const itemAccessibilityRole = Platform.select({
|
|
42
|
+
web: ['ul', 'ol'].includes(tag) ? tagsToRoles.li : undefined,
|
|
43
|
+
default: undefined
|
|
44
|
+
});
|
|
39
45
|
const content = list && /*#__PURE__*/_jsx(List, {
|
|
40
46
|
ref: ref,
|
|
41
47
|
tokens: listTokens,
|
|
@@ -50,6 +56,7 @@ export const QuickLinks = /*#__PURE__*/React.forwardRef((_ref, ref) => {
|
|
|
50
56
|
justifyContent: stackJustify
|
|
51
57
|
},
|
|
52
58
|
tag: tag,
|
|
59
|
+
itemAccessibilityRole: itemAccessibilityRole,
|
|
53
60
|
...rest,
|
|
54
61
|
children: children
|
|
55
62
|
});
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Platform from "react-native-web/dist/exports/Platform";
|
|
4
|
+
import Pressable from "react-native-web/dist/exports/Pressable";
|
|
5
|
+
import StyleSheet from "react-native-web/dist/exports/StyleSheet";
|
|
6
|
+
import View from "react-native-web/dist/exports/View";
|
|
7
|
+
import { useThemeTokensCallback } from '../ThemeProvider/useThemeTokens';
|
|
8
|
+
import { a11yProps } from '../utils/props/a11yProps';
|
|
9
|
+
import { copyPropTypes } from '../utils/props/copyPropTypes';
|
|
10
|
+
import { getTokensPropType } from '../utils/props/tokens';
|
|
11
|
+
import { selectSystemProps } from '../utils/props/selectSystemProps';
|
|
12
|
+
import { useCopy } from '../utils/useCopy';
|
|
13
|
+
import { useUniqueId } from '../utils/useUniqueId';
|
|
14
|
+
import { variantProp } from '../utils/props/variantProp';
|
|
15
|
+
import { viewProps } from '../utils/props/viewProps';
|
|
16
|
+
import { A11yText } from '../A11yText/A11yText';
|
|
17
|
+
import { ScrollViewEnd } from '../HorizontalScroll/ScrollViewEnd';
|
|
18
|
+
import { dictionary } from './dictionary';
|
|
19
|
+
import { ScrollProgress } from './ScrollProgress';
|
|
20
|
+
import { ScrollProvider } from './ScrollContext';
|
|
21
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
|
|
23
|
+
const SCROLL_DESCRIPTION_PREFIX = 'scroll-description';
|
|
24
|
+
const KEY_ARROW_RIGHT = 'ArrowRight';
|
|
25
|
+
const KEY_ARROW_LEFT = 'ArrowLeft';
|
|
26
|
+
const KEY_SPACE = ' ';
|
|
27
|
+
const MAX_PERCENTAGE = 100;
|
|
28
|
+
const MIN_BAR_PERCENTAGE = 1;
|
|
29
|
+
const ROLE_REGION = 'region';
|
|
30
|
+
const ROLE_NONE = 'none';
|
|
31
|
+
const COMPONENT_NAME = 'Scroll';
|
|
32
|
+
const SCROLL_ITEM_SELECTOR = '[data-scroll-item]';
|
|
33
|
+
const TAB_INDEX = 0;
|
|
34
|
+
const INITIAL_DIMENSION = 0;
|
|
35
|
+
const HALF_DIVISOR = 2;
|
|
36
|
+
const selectPressableStyles = _ref => {
|
|
37
|
+
let {
|
|
38
|
+
progressHitAreaHeight,
|
|
39
|
+
progressSpacing
|
|
40
|
+
} = _ref;
|
|
41
|
+
return {
|
|
42
|
+
height: progressHitAreaHeight,
|
|
43
|
+
marginTop: progressSpacing
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const selectThumbWrapperStyles = _ref2 => {
|
|
47
|
+
let {
|
|
48
|
+
thumbWidthPercent,
|
|
49
|
+
thumbPositionPercent
|
|
50
|
+
} = _ref2;
|
|
51
|
+
return {
|
|
52
|
+
width: `${thumbWidthPercent}%`,
|
|
53
|
+
marginLeft: `${thumbPositionPercent}%`
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const selectContainerA11yProps = _ref3 => {
|
|
57
|
+
let {
|
|
58
|
+
accessibilityLabel,
|
|
59
|
+
descriptionId,
|
|
60
|
+
handleKeyDown
|
|
61
|
+
} = _ref3;
|
|
62
|
+
return Platform.OS === 'web' ? {
|
|
63
|
+
role: ROLE_REGION,
|
|
64
|
+
'aria-label': accessibilityLabel,
|
|
65
|
+
'aria-describedby': descriptionId,
|
|
66
|
+
tabIndex: TAB_INDEX,
|
|
67
|
+
onKeyDown: handleKeyDown
|
|
68
|
+
} : {
|
|
69
|
+
accessibilityRole: ROLE_NONE,
|
|
70
|
+
accessibilityLabel
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const selectScrollContentStyle = itemGap => ({
|
|
74
|
+
columnGap: itemGap
|
|
75
|
+
});
|
|
76
|
+
const calculateThumbWidth = _ref4 => {
|
|
77
|
+
let {
|
|
78
|
+
containerWidth,
|
|
79
|
+
contentWidth
|
|
80
|
+
} = _ref4;
|
|
81
|
+
if (contentWidth <= INITIAL_DIMENSION) return MAX_PERCENTAGE;
|
|
82
|
+
return Math.max(MIN_BAR_PERCENTAGE, Math.min(MAX_PERCENTAGE, containerWidth / contentWidth * MAX_PERCENTAGE));
|
|
83
|
+
};
|
|
84
|
+
const calculateThumbPosition = _ref5 => {
|
|
85
|
+
let {
|
|
86
|
+
scrollOffset,
|
|
87
|
+
scrollMax,
|
|
88
|
+
thumbWidthPercent
|
|
89
|
+
} = _ref5;
|
|
90
|
+
if (scrollMax <= INITIAL_DIMENSION) return INITIAL_DIMENSION;
|
|
91
|
+
return scrollOffset / scrollMax * (MAX_PERCENTAGE - thumbWidthPercent);
|
|
92
|
+
};
|
|
93
|
+
const getScrollableNode = ref => ref.current?.getScrollableNode?.() ?? null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The `Scroll` component provides a continuous horizontal scrollable container with a progress
|
|
97
|
+
* bar indicator. Items are displayed in a flow layout where all items share the width of the
|
|
98
|
+
* widest item and the height of the tallest item.
|
|
99
|
+
*
|
|
100
|
+
* ## Props
|
|
101
|
+
*
|
|
102
|
+
* - Use `children` to pass `ScrollItem` components
|
|
103
|
+
* - Use `accessibilityLabel` to provide a meaningful label for the scrollable region
|
|
104
|
+
* - Use `copy` to select English or French for accessible labels
|
|
105
|
+
* - Use `dictionary` to override the default translations
|
|
106
|
+
*
|
|
107
|
+
* ## Usability and A11y guidelines
|
|
108
|
+
*
|
|
109
|
+
* - The scroll container is keyboard-focusable with `tabIndex="0"`
|
|
110
|
+
* - Each `ScrollItem` is individually focusable via Tab navigation
|
|
111
|
+
* - Use left and right arrow keys to scroll by one item width
|
|
112
|
+
* - Press Space bar to scroll right
|
|
113
|
+
* - When tabbing through items, the scroll follows focus to keep the active item visible
|
|
114
|
+
* - Screen readers announce the component as a region with a descriptive label
|
|
115
|
+
* - Touch and swipe gestures are supported on mobile and mobile web
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
export const Scroll = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
|
|
119
|
+
let {
|
|
120
|
+
children,
|
|
121
|
+
tokens,
|
|
122
|
+
variant,
|
|
123
|
+
copy,
|
|
124
|
+
dictionary: customDictionary = dictionary,
|
|
125
|
+
accessibilityLabel,
|
|
126
|
+
...rest
|
|
127
|
+
} = _ref6;
|
|
128
|
+
const getCopy = useCopy({
|
|
129
|
+
dictionary: customDictionary,
|
|
130
|
+
copy
|
|
131
|
+
});
|
|
132
|
+
const resolvedAccessibilityLabel = accessibilityLabel || getCopy('accessibilityLabel');
|
|
133
|
+
const getTokens = useThemeTokensCallback(COMPONENT_NAME, tokens, variant);
|
|
134
|
+
const defaultTokens = React.useMemo(() => getTokens(), [getTokens]);
|
|
135
|
+
const descriptionId = useUniqueId(SCROLL_DESCRIPTION_PREFIX);
|
|
136
|
+
const scrollRef = React.useRef(null);
|
|
137
|
+
const pressableRef = React.useRef(null);
|
|
138
|
+
const scrollContainerRef = React.useRef(null);
|
|
139
|
+
const scrollMaxRef = React.useRef(INITIAL_DIMENSION);
|
|
140
|
+
const scrollOffsetRef = React.useRef(INITIAL_DIMENSION);
|
|
141
|
+
const thumbWidthPercentRef = React.useRef(MAX_PERCENTAGE);
|
|
142
|
+
const thumbRef = React.useRef(null);
|
|
143
|
+
const isDraggingRef = React.useRef(false);
|
|
144
|
+
const dragStartRef = React.useRef({
|
|
145
|
+
mouseX: INITIAL_DIMENSION,
|
|
146
|
+
scrollOffset: INITIAL_DIMENSION
|
|
147
|
+
});
|
|
148
|
+
const [scrollOffset, setScrollOffset] = React.useState(INITIAL_DIMENSION);
|
|
149
|
+
const [containerWidth, setContainerWidth] = React.useState(INITIAL_DIMENSION);
|
|
150
|
+
const [contentWidth, setContentWidth] = React.useState(INITIAL_DIMENSION);
|
|
151
|
+
const [itemWidth, setItemWidth] = React.useState(INITIAL_DIMENSION);
|
|
152
|
+
const [itemHeight, setItemHeight] = React.useState(INITIAL_DIMENSION);
|
|
153
|
+
const {
|
|
154
|
+
itemGap
|
|
155
|
+
} = defaultTokens;
|
|
156
|
+
const scrollMax = Math.max(INITIAL_DIMENSION, contentWidth - containerWidth);
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
scrollMaxRef.current = scrollMax;
|
|
159
|
+
}, [scrollMax]);
|
|
160
|
+
const thumbWidthPercent = calculateThumbWidth({
|
|
161
|
+
containerWidth,
|
|
162
|
+
contentWidth
|
|
163
|
+
});
|
|
164
|
+
thumbWidthPercentRef.current = thumbWidthPercent;
|
|
165
|
+
const thumbPositionPercent = calculateThumbPosition({
|
|
166
|
+
scrollOffset,
|
|
167
|
+
scrollMax,
|
|
168
|
+
thumbWidthPercent
|
|
169
|
+
});
|
|
170
|
+
const thumbWrapperStyle = React.useMemo(() => [staticStyles.thumbWrapper, selectThumbWrapperStyles({
|
|
171
|
+
thumbWidthPercent,
|
|
172
|
+
thumbPositionPercent
|
|
173
|
+
}), Platform.OS === 'web' ? staticStyles.thumbCursor : null], [thumbWidthPercent, thumbPositionPercent]);
|
|
174
|
+
const handleScroll = React.useCallback(_ref7 => {
|
|
175
|
+
let {
|
|
176
|
+
nativeEvent: {
|
|
177
|
+
contentOffset: {
|
|
178
|
+
x
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} = _ref7;
|
|
182
|
+
scrollOffsetRef.current = x;
|
|
183
|
+
setScrollOffset(x);
|
|
184
|
+
}, []);
|
|
185
|
+
const handleContainerLayout = React.useCallback(_ref8 => {
|
|
186
|
+
let {
|
|
187
|
+
nativeEvent: {
|
|
188
|
+
layout: {
|
|
189
|
+
width
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} = _ref8;
|
|
193
|
+
setContainerWidth(width);
|
|
194
|
+
}, []);
|
|
195
|
+
const handleContentWidth = React.useCallback(width => {
|
|
196
|
+
setContentWidth(width);
|
|
197
|
+
}, []);
|
|
198
|
+
const handleItemLayout = React.useCallback((width, height) => {
|
|
199
|
+
setItemWidth(prev => Math.max(prev, width));
|
|
200
|
+
setItemHeight(prev => Math.max(prev, height));
|
|
201
|
+
}, []);
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
if (Platform.OS !== 'web') return undefined;
|
|
204
|
+
const element = scrollContainerRef.current;
|
|
205
|
+
if (!element) return undefined;
|
|
206
|
+
const handleWheel = event => {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
const isHorizontal = Math.abs(event.deltaX) > Math.abs(event.deltaY);
|
|
210
|
+
const delta = isHorizontal ? event.deltaX : event.deltaY;
|
|
211
|
+
const scrollableNode = getScrollableNode(scrollRef);
|
|
212
|
+
if (scrollableNode) {
|
|
213
|
+
const maxScroll = scrollMaxRef.current;
|
|
214
|
+
const newScrollLeft = Math.max(INITIAL_DIMENSION, Math.min(maxScroll, scrollableNode.scrollLeft + delta));
|
|
215
|
+
scrollableNode.scrollLeft = newScrollLeft;
|
|
216
|
+
scrollOffsetRef.current = newScrollLeft;
|
|
217
|
+
setScrollOffset(newScrollLeft);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
element.addEventListener('wheel', handleWheel, {
|
|
221
|
+
passive: false
|
|
222
|
+
});
|
|
223
|
+
return () => element.removeEventListener('wheel', handleWheel);
|
|
224
|
+
}, []);
|
|
225
|
+
React.useEffect(() => {
|
|
226
|
+
if (Platform.OS !== 'web') return undefined;
|
|
227
|
+
const element = scrollContainerRef.current;
|
|
228
|
+
if (!element) return undefined;
|
|
229
|
+
const handleFocusIn = event => {
|
|
230
|
+
const scrollableNode = getScrollableNode(scrollRef);
|
|
231
|
+
if (!scrollableNode) return;
|
|
232
|
+
const focusedItem = event.target.closest(SCROLL_ITEM_SELECTOR);
|
|
233
|
+
if (!focusedItem) return;
|
|
234
|
+
const itemLeft = focusedItem.offsetLeft;
|
|
235
|
+
const itemRight = itemLeft + focusedItem.offsetWidth;
|
|
236
|
+
const visibleLeft = scrollableNode.scrollLeft;
|
|
237
|
+
const currentContainerWidth = containerWidth;
|
|
238
|
+
const visibleRight = visibleLeft + currentContainerWidth;
|
|
239
|
+
if (itemRight > visibleRight) {
|
|
240
|
+
scrollRef.current.scrollTo({
|
|
241
|
+
x: itemRight - currentContainerWidth,
|
|
242
|
+
animated: true
|
|
243
|
+
});
|
|
244
|
+
} else if (itemLeft < visibleLeft) {
|
|
245
|
+
scrollRef.current.scrollTo({
|
|
246
|
+
x: itemLeft,
|
|
247
|
+
animated: true
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
element.addEventListener('focusin', handleFocusIn);
|
|
252
|
+
return () => element.removeEventListener('focusin', handleFocusIn);
|
|
253
|
+
}, [containerWidth]);
|
|
254
|
+
const hasScrollbar = scrollMax > INITIAL_DIMENSION;
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
if (Platform.OS !== 'web' || !hasScrollbar) return undefined;
|
|
257
|
+
const thumbElement = thumbRef.current;
|
|
258
|
+
if (!thumbElement) return undefined;
|
|
259
|
+
const handleMouseDown = event => {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
event.stopPropagation();
|
|
262
|
+
isDraggingRef.current = true;
|
|
263
|
+
dragStartRef.current = {
|
|
264
|
+
mouseX: event.clientX,
|
|
265
|
+
scrollOffset: getScrollableNode(scrollRef)?.scrollLeft ?? INITIAL_DIMENSION
|
|
266
|
+
};
|
|
267
|
+
thumbElement.style.cursor = 'grabbing';
|
|
268
|
+
document.body.style.cursor = 'grabbing';
|
|
269
|
+
document.body.style.userSelect = 'none';
|
|
270
|
+
};
|
|
271
|
+
const handleMouseMove = event => {
|
|
272
|
+
if (!isDraggingRef.current) return;
|
|
273
|
+
const trackElement = pressableRef.current;
|
|
274
|
+
if (!trackElement) return;
|
|
275
|
+
const trackWidth = trackElement.offsetWidth;
|
|
276
|
+
const thumbPixelWidth = thumbWidthPercentRef.current / MAX_PERCENTAGE * trackWidth;
|
|
277
|
+
const availableTrackPixels = trackWidth - thumbPixelWidth;
|
|
278
|
+
if (availableTrackPixels <= INITIAL_DIMENSION) return;
|
|
279
|
+
const mouseDelta = event.clientX - dragStartRef.current.mouseX;
|
|
280
|
+
const scrollDelta = mouseDelta / availableTrackPixels * scrollMaxRef.current;
|
|
281
|
+
const targetScroll = Math.max(INITIAL_DIMENSION, Math.min(scrollMaxRef.current, dragStartRef.current.scrollOffset + scrollDelta));
|
|
282
|
+
const scrollableNode = getScrollableNode(scrollRef);
|
|
283
|
+
if (scrollableNode) {
|
|
284
|
+
scrollableNode.scrollLeft = targetScroll;
|
|
285
|
+
scrollOffsetRef.current = targetScroll;
|
|
286
|
+
setScrollOffset(targetScroll);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const handleMouseUp = () => {
|
|
290
|
+
if (!isDraggingRef.current) return;
|
|
291
|
+
isDraggingRef.current = false;
|
|
292
|
+
thumbElement.style.cursor = '';
|
|
293
|
+
document.body.style.cursor = '';
|
|
294
|
+
document.body.style.userSelect = '';
|
|
295
|
+
};
|
|
296
|
+
thumbElement.addEventListener('mousedown', handleMouseDown);
|
|
297
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
298
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
299
|
+
return () => {
|
|
300
|
+
thumbElement.removeEventListener('mousedown', handleMouseDown);
|
|
301
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
302
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
303
|
+
};
|
|
304
|
+
}, [hasScrollbar]);
|
|
305
|
+
const handleKeyDown = React.useCallback(event => {
|
|
306
|
+
if (event.key === KEY_ARROW_RIGHT || event.key === KEY_SPACE) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
if (scrollRef.current) {
|
|
309
|
+
scrollRef.current.scrollTo({
|
|
310
|
+
x: scrollOffsetRef.current + itemWidth,
|
|
311
|
+
animated: true
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} else if (event.key === KEY_ARROW_LEFT) {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
if (scrollRef.current) {
|
|
317
|
+
scrollRef.current.scrollTo({
|
|
318
|
+
x: Math.max(INITIAL_DIMENSION, scrollOffsetRef.current - itemWidth),
|
|
319
|
+
animated: true
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}, [itemWidth]);
|
|
324
|
+
const handleProgressPress = React.useCallback(event => {
|
|
325
|
+
if (scrollMax <= INITIAL_DIMENSION || isDraggingRef.current) return;
|
|
326
|
+
if (Platform.OS === 'web' && pressableRef.current) {
|
|
327
|
+
const {
|
|
328
|
+
offsetX
|
|
329
|
+
} = event.nativeEvent;
|
|
330
|
+
const {
|
|
331
|
+
offsetWidth
|
|
332
|
+
} = pressableRef.current;
|
|
333
|
+
const thumbPixelWidth = thumbWidthPercent / MAX_PERCENTAGE * offsetWidth;
|
|
334
|
+
const availableTrack = offsetWidth - thumbPixelWidth;
|
|
335
|
+
if (availableTrack <= INITIAL_DIMENSION) return;
|
|
336
|
+
const clickPosition = Math.max(INITIAL_DIMENSION, Math.min(availableTrack, offsetX - thumbPixelWidth / HALF_DIVISOR));
|
|
337
|
+
const targetX = clickPosition / availableTrack * scrollMax;
|
|
338
|
+
if (scrollRef.current) {
|
|
339
|
+
scrollRef.current.scrollTo({
|
|
340
|
+
x: targetX,
|
|
341
|
+
animated: true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} else if (pressableRef.current) {
|
|
345
|
+
const {
|
|
346
|
+
locationX
|
|
347
|
+
} = event.nativeEvent;
|
|
348
|
+
pressableRef.current.measure((_x, _y, width) => {
|
|
349
|
+
const thumbPixelWidth = thumbWidthPercent / MAX_PERCENTAGE * width;
|
|
350
|
+
const availableTrack = width - thumbPixelWidth;
|
|
351
|
+
if (availableTrack <= INITIAL_DIMENSION) return;
|
|
352
|
+
const clickPosition = Math.max(INITIAL_DIMENSION, Math.min(availableTrack, locationX - thumbPixelWidth / HALF_DIVISOR));
|
|
353
|
+
const targetX = clickPosition / availableTrack * scrollMax;
|
|
354
|
+
if (scrollRef.current) {
|
|
355
|
+
scrollRef.current.scrollTo({
|
|
356
|
+
x: targetX,
|
|
357
|
+
animated: true
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}, [scrollMax, thumbWidthPercent]);
|
|
363
|
+
const handleContainerRef = React.useCallback(node => {
|
|
364
|
+
scrollContainerRef.current = node;
|
|
365
|
+
if (typeof ref === 'function') ref(node);else if (ref) Object.assign(ref, {
|
|
366
|
+
current: node
|
|
367
|
+
});
|
|
368
|
+
}, [ref]);
|
|
369
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
370
|
+
ref: handleContainerRef,
|
|
371
|
+
style: staticStyles.container,
|
|
372
|
+
...selectContainerA11yProps({
|
|
373
|
+
accessibilityLabel: resolvedAccessibilityLabel,
|
|
374
|
+
descriptionId,
|
|
375
|
+
handleKeyDown
|
|
376
|
+
}),
|
|
377
|
+
...selectProps(rest),
|
|
378
|
+
children: [/*#__PURE__*/_jsx(A11yText, {
|
|
379
|
+
text: getCopy('instructionText'),
|
|
380
|
+
nativeID: descriptionId
|
|
381
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
382
|
+
style: staticStyles.scrollContainer,
|
|
383
|
+
children: /*#__PURE__*/_jsx(ScrollViewEnd, {
|
|
384
|
+
ref: scrollRef,
|
|
385
|
+
horizontal: true,
|
|
386
|
+
onScrollEnd: handleScroll,
|
|
387
|
+
onScroll: handleScroll,
|
|
388
|
+
onContentSizeChange: handleContentWidth,
|
|
389
|
+
onLayout: handleContainerLayout,
|
|
390
|
+
showsHorizontalScrollIndicator: false,
|
|
391
|
+
contentContainerStyle: [staticStyles.scrollContent, selectScrollContentStyle(itemGap)],
|
|
392
|
+
children: /*#__PURE__*/_jsx(ScrollProvider, {
|
|
393
|
+
itemWidth: itemWidth,
|
|
394
|
+
itemHeight: itemHeight,
|
|
395
|
+
onItemLayout: handleItemLayout,
|
|
396
|
+
children: children
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
}), scrollMax > INITIAL_DIMENSION && /*#__PURE__*/_jsx(Pressable, {
|
|
400
|
+
ref: pressableRef,
|
|
401
|
+
style: [staticStyles.pressable, selectPressableStyles(defaultTokens)],
|
|
402
|
+
onPress: handleProgressPress,
|
|
403
|
+
children: pressableState => /*#__PURE__*/_jsx(ScrollProgress, {
|
|
404
|
+
ref: thumbRef,
|
|
405
|
+
pressableState: pressableState,
|
|
406
|
+
getTokens: getTokens,
|
|
407
|
+
thumbWrapperStyle: thumbWrapperStyle,
|
|
408
|
+
getCopy: getCopy
|
|
409
|
+
})
|
|
410
|
+
})]
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
Scroll.displayName = 'Scroll';
|
|
414
|
+
Scroll.propTypes = {
|
|
415
|
+
...selectedSystemPropTypes,
|
|
416
|
+
/**
|
|
417
|
+
* Scroll items to display. Should be `ScrollItem` components.
|
|
418
|
+
*/
|
|
419
|
+
children: PropTypes.node.isRequired,
|
|
420
|
+
/**
|
|
421
|
+
* Theme tokens for customizing the Scroll appearance
|
|
422
|
+
*/
|
|
423
|
+
tokens: getTokensPropType('Scroll'),
|
|
424
|
+
/**
|
|
425
|
+
* Variant for the Scroll component
|
|
426
|
+
*/
|
|
427
|
+
variant: variantProp.propType,
|
|
428
|
+
/**
|
|
429
|
+
* Select English or French copy for accessible labels.
|
|
430
|
+
*/
|
|
431
|
+
copy: copyPropTypes,
|
|
432
|
+
/**
|
|
433
|
+
* Override the default dictionary by passing the complete dictionary object for `en` and `fr`.
|
|
434
|
+
*/
|
|
435
|
+
dictionary: PropTypes.shape({
|
|
436
|
+
en: PropTypes.object,
|
|
437
|
+
fr: PropTypes.object
|
|
438
|
+
}),
|
|
439
|
+
/**
|
|
440
|
+
* Accessibility label for the scrollable region
|
|
441
|
+
*/
|
|
442
|
+
accessibilityLabel: PropTypes.string
|
|
443
|
+
};
|
|
444
|
+
const staticStyles = StyleSheet.create({
|
|
445
|
+
container: {
|
|
446
|
+
width: '100%'
|
|
447
|
+
},
|
|
448
|
+
scrollContainer: {
|
|
449
|
+
width: '100%'
|
|
450
|
+
},
|
|
451
|
+
pressable: {
|
|
452
|
+
justifyContent: 'center',
|
|
453
|
+
width: '100%'
|
|
454
|
+
},
|
|
455
|
+
scrollContent: {
|
|
456
|
+
alignSelf: 'flex-start',
|
|
457
|
+
flexGrow: 1
|
|
458
|
+
},
|
|
459
|
+
thumbWrapper: {
|
|
460
|
+
height: '100%'
|
|
461
|
+
},
|
|
462
|
+
thumbCursor: {
|
|
463
|
+
cursor: 'grab'
|
|
464
|
+
}
|
|
465
|
+
});
|