@telus-uds/components-base 4.0.0-alpha.1 → 4.0.0-beta.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 (129) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/lib/cjs/Autocomplete/Autocomplete.js +13 -1
  3. package/lib/cjs/Autocomplete/constants.js +1 -1
  4. package/lib/cjs/Footnote/FootnoteLink.js +18 -17
  5. package/lib/cjs/Listbox/ListboxOverlay.js +7 -1
  6. package/lib/cjs/MultiSelectFilter/ModalOverlay.js +7 -1
  7. package/lib/cjs/MultiSelectFilter/MultiSelectFilter.js +2 -28
  8. package/lib/cjs/Progress/Progress.js +30 -5
  9. package/lib/cjs/Scroll/Scroll.js +472 -0
  10. package/lib/cjs/Scroll/ScrollContext.js +55 -0
  11. package/lib/cjs/Scroll/ScrollItem.js +106 -0
  12. package/lib/cjs/Scroll/ScrollProgress.js +100 -0
  13. package/lib/cjs/Scroll/dictionary.js +18 -0
  14. package/lib/cjs/TextInput/TextInputBase.js +2 -1
  15. package/lib/cjs/index.js +14 -0
  16. package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
  17. package/lib/cjs/utils/children.js +2 -1
  18. package/lib/cjs/utils/useOverlaidPosition.js +83 -42
  19. package/lib/esm/Autocomplete/Autocomplete.js +13 -1
  20. package/lib/esm/Autocomplete/constants.js +1 -1
  21. package/lib/esm/Footnote/FootnoteLink.js +18 -17
  22. package/lib/esm/Listbox/ListboxOverlay.js +7 -1
  23. package/lib/esm/MultiSelectFilter/ModalOverlay.js +8 -2
  24. package/lib/esm/MultiSelectFilter/MultiSelectFilter.js +2 -28
  25. package/lib/esm/Progress/Progress.js +30 -5
  26. package/lib/esm/Scroll/Scroll.js +465 -0
  27. package/lib/esm/Scroll/ScrollContext.js +46 -0
  28. package/lib/esm/Scroll/ScrollItem.js +100 -0
  29. package/lib/esm/Scroll/ScrollProgress.js +93 -0
  30. package/lib/esm/Scroll/dictionary.js +12 -0
  31. package/lib/esm/TextInput/TextInputBase.js +2 -1
  32. package/lib/esm/index.js +2 -0
  33. package/lib/esm/utils/animation/useAnimatedValue.js +38 -0
  34. package/lib/esm/utils/children.js +2 -1
  35. package/lib/esm/utils/useOverlaidPosition.js +83 -42
  36. package/lib/package.json +2 -2
  37. package/package.json +2 -2
  38. package/src/Autocomplete/Autocomplete.jsx +11 -2
  39. package/src/Autocomplete/constants.js +1 -1
  40. package/src/Footnote/FootnoteLink.jsx +17 -12
  41. package/src/Listbox/ListboxOverlay.jsx +6 -2
  42. package/src/MultiSelectFilter/ModalOverlay.jsx +9 -3
  43. package/src/MultiSelectFilter/MultiSelectFilter.jsx +1 -31
  44. package/src/Progress/Progress.jsx +22 -3
  45. package/src/Scroll/Scroll.jsx +483 -0
  46. package/src/Scroll/ScrollContext.jsx +39 -0
  47. package/src/Scroll/ScrollItem.jsx +87 -0
  48. package/src/Scroll/ScrollProgress.jsx +74 -0
  49. package/src/Scroll/dictionary.js +12 -0
  50. package/src/TextInput/TextInputBase.jsx +2 -1
  51. package/src/index.js +2 -0
  52. package/src/utils/animation/useAnimatedValue.js +35 -0
  53. package/src/utils/children.jsx +4 -1
  54. package/src/utils/useOverlaidPosition.js +84 -34
  55. package/types/A11yInfoProvider.d.ts +9 -0
  56. package/types/A11yText.d.ts +13 -0
  57. package/types/ActionCard.d.ts +19 -0
  58. package/types/ActivityIndicator.d.ts +13 -0
  59. package/types/Autocomplete.d.ts +27 -0
  60. package/types/Badge.d.ts +15 -4
  61. package/types/BaseProvider.d.ts +26 -0
  62. package/types/Box.d.ts +13 -3
  63. package/types/Button.d.ts +35 -0
  64. package/types/Card.d.ts +19 -0
  65. package/types/CardGroup.d.ts +23 -0
  66. package/types/Carousel.d.ts +25 -0
  67. package/types/Checkbox.d.ts +15 -2
  68. package/types/CheckboxCard.d.ts +20 -0
  69. package/types/CheckboxCardGroup.d.ts +10 -4
  70. package/types/ChevronLink.d.ts +9 -2
  71. package/types/ColourToggle.d.ts +16 -0
  72. package/types/Common.d.ts +7 -3
  73. package/types/Divider.d.ts +13 -1
  74. package/types/DownloadApp.d.ts +19 -0
  75. package/types/ExpandCollapse.d.ts +16 -9
  76. package/types/ExpandCollapseMini.d.ts +16 -0
  77. package/types/Feedback.d.ts +16 -0
  78. package/types/Fieldset.d.ts +17 -0
  79. package/types/FileUpload.d.ts +6 -0
  80. package/types/FlexGrid.d.ts +57 -0
  81. package/types/Footnote.d.ts +18 -0
  82. package/types/HorizontalScroll.d.ts +15 -0
  83. package/types/HorizontalScrollButton.d.ts +17 -1
  84. package/types/Icon.d.ts +13 -2
  85. package/types/IconButton.d.ts +19 -0
  86. package/types/InputLabel.d.ts +12 -0
  87. package/types/InputSupports.d.ts +19 -0
  88. package/types/Link.d.ts +20 -4
  89. package/types/List.d.ts +11 -4
  90. package/types/Listbox.d.ts +13 -3
  91. package/types/Modal.d.ts +25 -0
  92. package/types/MultiSelectFilter.d.ts +24 -0
  93. package/types/Notification.d.ts +19 -0
  94. package/types/OrderedList.d.ts +17 -0
  95. package/types/Pagination.d.ts +25 -0
  96. package/types/Portal.d.ts +5 -1
  97. package/types/PriceLockup.d.ts +28 -0
  98. package/types/ProductCard.d.ts +20 -0
  99. package/types/ProductCardGroup.d.ts +21 -0
  100. package/types/Progress.d.ts +22 -0
  101. package/types/QuickLinks.d.ts +27 -0
  102. package/types/QuickLinksFeature.d.ts +27 -0
  103. package/types/Radio.d.ts +22 -0
  104. package/types/RadioCard.d.ts +21 -0
  105. package/types/Responsive.d.ts +14 -0
  106. package/types/Scroll.d.ts +66 -0
  107. package/types/Search.d.ts +15 -2
  108. package/types/Select.d.ts +12 -4
  109. package/types/SideNav.d.ts +38 -0
  110. package/types/Skeleton.d.ts +17 -0
  111. package/types/SkipLink.d.ts +14 -0
  112. package/types/Spacer.d.ts +8 -1
  113. package/types/StackView.d.ts +8 -2
  114. package/types/Status.d.ts +2 -3
  115. package/types/StepTracker.d.ts +16 -0
  116. package/types/TabBar.d.ts +25 -0
  117. package/types/Tabs.d.ts +12 -3
  118. package/types/Tags.d.ts +24 -0
  119. package/types/TextButton.d.ts +11 -4
  120. package/types/TextInput.d.ts +38 -0
  121. package/types/ThemeProvider.d.ts +29 -0
  122. package/types/Timeline.d.ts +16 -0
  123. package/types/ToggleSwitch.d.ts +11 -2
  124. package/types/ToolTip.d.ts +7 -3
  125. package/types/TooltipButton.d.ts +20 -0
  126. package/types/Typography.d.ts +8 -2
  127. package/types/Validator.d.ts +16 -0
  128. package/types/ViewportProvider.d.ts +12 -0
  129. package/types/index.d.ts +205 -49
@@ -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 bars displayed within the progress bar container.
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((_ref2, ref) => {
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
- } = _ref2;
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: children
113
+ children: content
89
114
  })
90
115
  });
91
116
  });
@@ -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
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ const ScrollContext = /*#__PURE__*/React.createContext();
5
+ export const ScrollProvider = _ref => {
6
+ let {
7
+ children,
8
+ itemWidth,
9
+ itemHeight,
10
+ onItemLayout
11
+ } = _ref;
12
+ const value = React.useMemo(() => ({
13
+ itemWidth,
14
+ itemHeight,
15
+ onItemLayout
16
+ }), [itemWidth, itemHeight, onItemLayout]);
17
+ return /*#__PURE__*/_jsx(ScrollContext.Provider, {
18
+ value: value,
19
+ children: children
20
+ });
21
+ };
22
+ export function useScroll() {
23
+ const context = React.useContext(ScrollContext);
24
+ if (context === undefined) {
25
+ throw new Error(`'useScroll' must be used within a 'ScrollProvider'`);
26
+ }
27
+ return context;
28
+ }
29
+ ScrollProvider.propTypes = {
30
+ /**
31
+ * Content to be rendered within the scroll context provider.
32
+ */
33
+ children: PropTypes.node.isRequired,
34
+ /**
35
+ * Width of each scroll item in pixels.
36
+ */
37
+ itemWidth: PropTypes.number.isRequired,
38
+ /**
39
+ * Height of each scroll item in pixels.
40
+ */
41
+ itemHeight: PropTypes.number.isRequired,
42
+ /**
43
+ * Callback invoked when a scroll item layout is measured.
44
+ */
45
+ onItemLayout: PropTypes.func.isRequired
46
+ };
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import Platform from "react-native-web/dist/exports/Platform";
4
+ import View from "react-native-web/dist/exports/View";
5
+ import { a11yProps } from '../utils/props/a11yProps';
6
+ import { selectSystemProps } from '../utils/props/selectSystemProps';
7
+ import { viewProps } from '../utils/props/viewProps';
8
+ import { layoutTags, getA11yPropsFromHtmlTag } from '../utils/a11y/semantics';
9
+ import { useScroll } from './ScrollContext';
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
12
+ const DEFAULT_TAG = 'li';
13
+ const DATA_SCROLL_ITEM = 'scrollItem';
14
+ const INITIAL_DIMENSION = 0;
15
+ const TAB_INDEX = 0;
16
+ const selectContainerStyle = _ref => {
17
+ let {
18
+ itemWidth,
19
+ itemHeight
20
+ } = _ref;
21
+ const style = {};
22
+ if (itemWidth > INITIAL_DIMENSION) {
23
+ style.width = itemWidth;
24
+ }
25
+ if (itemHeight > INITIAL_DIMENSION) {
26
+ style.height = itemHeight;
27
+ }
28
+ return style;
29
+ };
30
+
31
+ /**
32
+ * The `ScrollItem` component wraps individual items within a `Scroll` container. Each item
33
+ * is individually focusable and reports its natural dimensions so the parent can apply uniform
34
+ * sizing across all items.
35
+ *
36
+ * ## Props
37
+ *
38
+ * - Use `children` to provide the content of the scroll item
39
+ * - Use `tag` to set the HTML tag of the outer container (defaults to `'li'`)
40
+ *
41
+ */
42
+ const ScrollItem = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
43
+ let {
44
+ children,
45
+ tag = DEFAULT_TAG,
46
+ ...rest
47
+ } = _ref2;
48
+ const {
49
+ itemWidth,
50
+ itemHeight,
51
+ onItemLayout
52
+ } = useScroll();
53
+ const handleLayout = React.useCallback(_ref3 => {
54
+ let {
55
+ nativeEvent: {
56
+ layout: {
57
+ width,
58
+ height
59
+ }
60
+ }
61
+ } = _ref3;
62
+ onItemLayout?.(width, height);
63
+ }, [onItemLayout]);
64
+ const selectedProps = selectProps({
65
+ ...rest,
66
+ ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
67
+ });
68
+ return /*#__PURE__*/_jsx(View, {
69
+ ref: ref,
70
+ style: selectContainerStyle({
71
+ itemWidth,
72
+ itemHeight
73
+ }),
74
+ onLayout: handleLayout,
75
+ dataSet: {
76
+ [DATA_SCROLL_ITEM]: true
77
+ },
78
+ ...(Platform.OS === 'web' ? {
79
+ tabIndex: TAB_INDEX
80
+ } : {
81
+ focusable: true
82
+ }),
83
+ ...selectedProps,
84
+ children: children
85
+ });
86
+ });
87
+ ScrollItem.displayName = 'ScrollItem';
88
+ ScrollItem.propTypes = {
89
+ ...selectedSystemPropTypes,
90
+ /**
91
+ * Content of the scroll item
92
+ */
93
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
94
+ /**
95
+ * Sets the HTML tag of the outer container. Defaults to `'li'`.
96
+ */
97
+ tag: PropTypes.oneOf(layoutTags)
98
+ };
99
+ const MemoizedScrollItem = /*#__PURE__*/React.memo(ScrollItem);
100
+ export { MemoizedScrollItem as ScrollItem };