@telus-uds/components-base 3.29.1 → 3.30.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 (50) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/lib/cjs/Carousel/Carousel.js +6 -1
  3. package/lib/cjs/Carousel/CarouselThumbnail.js +123 -31
  4. package/lib/cjs/Carousel/CarouselThumbnailNavigation.js +8 -1
  5. package/lib/cjs/Footnote/FootnoteLink.js +18 -17
  6. package/lib/cjs/Progress/Progress.js +30 -5
  7. package/lib/cjs/Scroll/Scroll.js +466 -0
  8. package/lib/cjs/Scroll/ScrollContext.js +55 -0
  9. package/lib/cjs/Scroll/ScrollItem.js +103 -0
  10. package/lib/cjs/Scroll/ScrollProgress.js +99 -0
  11. package/lib/cjs/Scroll/dictionary.js +18 -0
  12. package/lib/cjs/Scroll/index.js +29 -0
  13. package/lib/cjs/index.js +15 -0
  14. package/lib/cjs/utils/animation/index.js +7 -0
  15. package/lib/cjs/utils/animation/useAnimatedValue.js +46 -0
  16. package/lib/cjs/utils/children.js +2 -1
  17. package/lib/esm/Carousel/Carousel.js +6 -1
  18. package/lib/esm/Carousel/CarouselThumbnail.js +124 -32
  19. package/lib/esm/Carousel/CarouselThumbnailNavigation.js +8 -1
  20. package/lib/esm/Footnote/FootnoteLink.js +18 -17
  21. package/lib/esm/Progress/Progress.js +30 -5
  22. package/lib/esm/Scroll/Scroll.js +459 -0
  23. package/lib/esm/Scroll/ScrollContext.js +47 -0
  24. package/lib/esm/Scroll/ScrollItem.js +96 -0
  25. package/lib/esm/Scroll/ScrollProgress.js +92 -0
  26. package/lib/esm/Scroll/dictionary.js +12 -0
  27. package/lib/esm/Scroll/index.js +4 -0
  28. package/lib/esm/index.js +1 -0
  29. package/lib/esm/utils/animation/index.js +1 -1
  30. package/lib/esm/utils/animation/useAnimatedValue.js +39 -0
  31. package/lib/esm/utils/children.js +2 -1
  32. package/lib/package.json +2 -2
  33. package/package.json +2 -2
  34. package/src/Carousel/Carousel.jsx +6 -1
  35. package/src/Carousel/CarouselThumbnail.jsx +153 -64
  36. package/src/Carousel/CarouselThumbnailNavigation.jsx +8 -2
  37. package/src/Footnote/FootnoteLink.jsx +17 -12
  38. package/src/Progress/Progress.jsx +22 -3
  39. package/src/Scroll/Scroll.jsx +488 -0
  40. package/src/Scroll/ScrollContext.jsx +41 -0
  41. package/src/Scroll/ScrollItem.jsx +89 -0
  42. package/src/Scroll/ScrollProgress.jsx +75 -0
  43. package/src/Scroll/dictionary.js +12 -0
  44. package/src/Scroll/index.js +5 -0
  45. package/src/index.js +1 -0
  46. package/src/utils/animation/index.js +1 -1
  47. package/src/utils/animation/useAnimatedValue.js +37 -0
  48. package/src/utils/children.jsx +4 -1
  49. package/types/Scroll.d.ts +66 -0
  50. package/types/index.d.ts +9 -0
@@ -0,0 +1,459 @@
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';
8
+ import { a11yProps, copyPropTypes, getTokensPropType, selectSystemProps, useCopy, useUniqueId, variantProp, viewProps } from '../utils';
9
+ import A11yText from '../A11yText';
10
+ import ScrollViewEnd from '../HorizontalScroll/ScrollViewEnd';
11
+ import dictionary from './dictionary';
12
+ import ScrollProgress from './ScrollProgress';
13
+ import { ScrollProvider } from './ScrollContext';
14
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
16
+ const SCROLL_DESCRIPTION_PREFIX = 'scroll-description';
17
+ const KEY_ARROW_RIGHT = 'ArrowRight';
18
+ const KEY_ARROW_LEFT = 'ArrowLeft';
19
+ const KEY_SPACE = ' ';
20
+ const MAX_PERCENTAGE = 100;
21
+ const MIN_BAR_PERCENTAGE = 1;
22
+ const ROLE_REGION = 'region';
23
+ const ROLE_NONE = 'none';
24
+ const COMPONENT_NAME = 'Scroll';
25
+ const SCROLL_ITEM_SELECTOR = '[data-scroll-item]';
26
+ const TAB_INDEX = 0;
27
+ const INITIAL_DIMENSION = 0;
28
+ const HALF_DIVISOR = 2;
29
+ const selectPressableStyles = _ref => {
30
+ let {
31
+ progressHitAreaHeight,
32
+ progressSpacing
33
+ } = _ref;
34
+ return {
35
+ height: progressHitAreaHeight,
36
+ marginTop: progressSpacing
37
+ };
38
+ };
39
+ const selectThumbWrapperStyles = _ref2 => {
40
+ let {
41
+ thumbWidthPercent,
42
+ thumbPositionPercent
43
+ } = _ref2;
44
+ return {
45
+ width: `${thumbWidthPercent}%`,
46
+ marginLeft: `${thumbPositionPercent}%`
47
+ };
48
+ };
49
+ const selectContainerA11yProps = _ref3 => {
50
+ let {
51
+ accessibilityLabel,
52
+ descriptionId,
53
+ handleKeyDown
54
+ } = _ref3;
55
+ return Platform.OS === 'web' ? {
56
+ role: ROLE_REGION,
57
+ 'aria-label': accessibilityLabel,
58
+ 'aria-describedby': descriptionId,
59
+ tabIndex: TAB_INDEX,
60
+ onKeyDown: handleKeyDown
61
+ } : {
62
+ accessibilityRole: ROLE_NONE,
63
+ accessibilityLabel
64
+ };
65
+ };
66
+ const selectScrollContentStyle = itemGap => ({
67
+ columnGap: itemGap
68
+ });
69
+ const calculateThumbWidth = _ref4 => {
70
+ let {
71
+ containerWidth,
72
+ contentWidth
73
+ } = _ref4;
74
+ if (contentWidth <= INITIAL_DIMENSION) return MAX_PERCENTAGE;
75
+ return Math.max(MIN_BAR_PERCENTAGE, Math.min(MAX_PERCENTAGE, containerWidth / contentWidth * MAX_PERCENTAGE));
76
+ };
77
+ const calculateThumbPosition = _ref5 => {
78
+ let {
79
+ scrollOffset,
80
+ scrollMax,
81
+ thumbWidthPercent
82
+ } = _ref5;
83
+ if (scrollMax <= INITIAL_DIMENSION) return INITIAL_DIMENSION;
84
+ return scrollOffset / scrollMax * (MAX_PERCENTAGE - thumbWidthPercent);
85
+ };
86
+ const getScrollableNode = ref => ref.current?.getScrollableNode?.() ?? null;
87
+
88
+ /**
89
+ * The `Scroll` component provides a continuous horizontal scrollable container with a progress
90
+ * bar indicator. Items are displayed in a flow layout where all items share the width of the
91
+ * widest item and the height of the tallest item.
92
+ *
93
+ * ## Props
94
+ *
95
+ * - Use `children` to pass `ScrollItem` components
96
+ * - Use `accessibilityLabel` to provide a meaningful label for the scrollable region
97
+ * - Use `copy` to select English or French for accessible labels
98
+ * - Use `dictionary` to override the default translations
99
+ *
100
+ * ## Usability and A11y guidelines
101
+ *
102
+ * - The scroll container is keyboard-focusable with `tabIndex="0"`
103
+ * - Each `ScrollItem` is individually focusable via Tab navigation
104
+ * - Use left and right arrow keys to scroll by one item width
105
+ * - Press Space bar to scroll right
106
+ * - When tabbing through items, the scroll follows focus to keep the active item visible
107
+ * - Screen readers announce the component as a region with a descriptive label
108
+ * - Touch and swipe gestures are supported on mobile and mobile web
109
+ *
110
+ */
111
+ const Scroll = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
112
+ let {
113
+ children,
114
+ tokens,
115
+ variant,
116
+ copy,
117
+ dictionary: customDictionary = dictionary,
118
+ accessibilityLabel,
119
+ ...rest
120
+ } = _ref6;
121
+ const getCopy = useCopy({
122
+ dictionary: customDictionary,
123
+ copy
124
+ });
125
+ const resolvedAccessibilityLabel = accessibilityLabel || getCopy('accessibilityLabel');
126
+ const getTokens = useThemeTokensCallback(COMPONENT_NAME, tokens, variant);
127
+ const defaultTokens = React.useMemo(() => getTokens(), [getTokens]);
128
+ const descriptionId = useUniqueId(SCROLL_DESCRIPTION_PREFIX);
129
+ const scrollRef = React.useRef(null);
130
+ const pressableRef = React.useRef(null);
131
+ const scrollContainerRef = React.useRef(null);
132
+ const scrollMaxRef = React.useRef(INITIAL_DIMENSION);
133
+ const scrollOffsetRef = React.useRef(INITIAL_DIMENSION);
134
+ const thumbWidthPercentRef = React.useRef(MAX_PERCENTAGE);
135
+ const thumbRef = React.useRef(null);
136
+ const isDraggingRef = React.useRef(false);
137
+ const dragStartRef = React.useRef({
138
+ mouseX: INITIAL_DIMENSION,
139
+ scrollOffset: INITIAL_DIMENSION
140
+ });
141
+ const [scrollOffset, setScrollOffset] = React.useState(INITIAL_DIMENSION);
142
+ const [containerWidth, setContainerWidth] = React.useState(INITIAL_DIMENSION);
143
+ const [contentWidth, setContentWidth] = React.useState(INITIAL_DIMENSION);
144
+ const [itemWidth, setItemWidth] = React.useState(INITIAL_DIMENSION);
145
+ const [itemHeight, setItemHeight] = React.useState(INITIAL_DIMENSION);
146
+ const {
147
+ itemGap
148
+ } = defaultTokens;
149
+ const scrollMax = Math.max(INITIAL_DIMENSION, contentWidth - containerWidth);
150
+ React.useEffect(() => {
151
+ scrollMaxRef.current = scrollMax;
152
+ }, [scrollMax]);
153
+ const thumbWidthPercent = calculateThumbWidth({
154
+ containerWidth,
155
+ contentWidth
156
+ });
157
+ thumbWidthPercentRef.current = thumbWidthPercent;
158
+ const thumbPositionPercent = calculateThumbPosition({
159
+ scrollOffset,
160
+ scrollMax,
161
+ thumbWidthPercent
162
+ });
163
+ const thumbWrapperStyle = React.useMemo(() => [staticStyles.thumbWrapper, selectThumbWrapperStyles({
164
+ thumbWidthPercent,
165
+ thumbPositionPercent
166
+ }), Platform.OS === 'web' ? staticStyles.thumbCursor : null], [thumbWidthPercent, thumbPositionPercent]);
167
+ const handleScroll = React.useCallback(_ref7 => {
168
+ let {
169
+ nativeEvent: {
170
+ contentOffset: {
171
+ x
172
+ }
173
+ }
174
+ } = _ref7;
175
+ scrollOffsetRef.current = x;
176
+ setScrollOffset(x);
177
+ }, []);
178
+ const handleContainerLayout = React.useCallback(_ref8 => {
179
+ let {
180
+ nativeEvent: {
181
+ layout: {
182
+ width
183
+ }
184
+ }
185
+ } = _ref8;
186
+ setContainerWidth(width);
187
+ }, []);
188
+ const handleContentWidth = React.useCallback(width => {
189
+ setContentWidth(width);
190
+ }, []);
191
+ const handleItemLayout = React.useCallback((width, height) => {
192
+ setItemWidth(prev => Math.max(prev, width));
193
+ setItemHeight(prev => Math.max(prev, height));
194
+ }, []);
195
+ React.useEffect(() => {
196
+ if (Platform.OS !== 'web') return undefined;
197
+ const element = scrollContainerRef.current;
198
+ if (!element) return undefined;
199
+ const handleWheel = event => {
200
+ event.preventDefault();
201
+ event.stopPropagation();
202
+ const isHorizontal = Math.abs(event.deltaX) > Math.abs(event.deltaY);
203
+ const delta = isHorizontal ? event.deltaX : event.deltaY;
204
+ const scrollableNode = getScrollableNode(scrollRef);
205
+ if (scrollableNode) {
206
+ const maxScroll = scrollMaxRef.current;
207
+ const newScrollLeft = Math.max(INITIAL_DIMENSION, Math.min(maxScroll, scrollableNode.scrollLeft + delta));
208
+ scrollableNode.scrollLeft = newScrollLeft;
209
+ scrollOffsetRef.current = newScrollLeft;
210
+ setScrollOffset(newScrollLeft);
211
+ }
212
+ };
213
+ element.addEventListener('wheel', handleWheel, {
214
+ passive: false
215
+ });
216
+ return () => element.removeEventListener('wheel', handleWheel);
217
+ }, []);
218
+ React.useEffect(() => {
219
+ if (Platform.OS !== 'web') return undefined;
220
+ const element = scrollContainerRef.current;
221
+ if (!element) return undefined;
222
+ const handleFocusIn = event => {
223
+ const scrollableNode = getScrollableNode(scrollRef);
224
+ if (!scrollableNode) return;
225
+ const focusedItem = event.target.closest(SCROLL_ITEM_SELECTOR);
226
+ if (!focusedItem) return;
227
+ const itemLeft = focusedItem.offsetLeft;
228
+ const itemRight = itemLeft + focusedItem.offsetWidth;
229
+ const visibleLeft = scrollableNode.scrollLeft;
230
+ const currentContainerWidth = containerWidth;
231
+ const visibleRight = visibleLeft + currentContainerWidth;
232
+ if (itemRight > visibleRight) {
233
+ scrollRef.current.scrollTo({
234
+ x: itemRight - currentContainerWidth,
235
+ animated: true
236
+ });
237
+ } else if (itemLeft < visibleLeft) {
238
+ scrollRef.current.scrollTo({
239
+ x: itemLeft,
240
+ animated: true
241
+ });
242
+ }
243
+ };
244
+ element.addEventListener('focusin', handleFocusIn);
245
+ return () => element.removeEventListener('focusin', handleFocusIn);
246
+ }, [containerWidth]);
247
+ const hasScrollbar = scrollMax > INITIAL_DIMENSION;
248
+ React.useEffect(() => {
249
+ if (Platform.OS !== 'web' || !hasScrollbar) return undefined;
250
+ const thumbElement = thumbRef.current;
251
+ if (!thumbElement) return undefined;
252
+ const handleMouseDown = event => {
253
+ event.preventDefault();
254
+ event.stopPropagation();
255
+ isDraggingRef.current = true;
256
+ dragStartRef.current = {
257
+ mouseX: event.clientX,
258
+ scrollOffset: getScrollableNode(scrollRef)?.scrollLeft ?? INITIAL_DIMENSION
259
+ };
260
+ thumbElement.style.cursor = 'grabbing';
261
+ document.body.style.cursor = 'grabbing';
262
+ document.body.style.userSelect = 'none';
263
+ };
264
+ const handleMouseMove = event => {
265
+ if (!isDraggingRef.current) return;
266
+ const trackElement = pressableRef.current;
267
+ if (!trackElement) return;
268
+ const trackWidth = trackElement.offsetWidth;
269
+ const thumbPixelWidth = thumbWidthPercentRef.current / MAX_PERCENTAGE * trackWidth;
270
+ const availableTrackPixels = trackWidth - thumbPixelWidth;
271
+ if (availableTrackPixels <= INITIAL_DIMENSION) return;
272
+ const mouseDelta = event.clientX - dragStartRef.current.mouseX;
273
+ const scrollDelta = mouseDelta / availableTrackPixels * scrollMaxRef.current;
274
+ const targetScroll = Math.max(INITIAL_DIMENSION, Math.min(scrollMaxRef.current, dragStartRef.current.scrollOffset + scrollDelta));
275
+ const scrollableNode = getScrollableNode(scrollRef);
276
+ if (scrollableNode) {
277
+ scrollableNode.scrollLeft = targetScroll;
278
+ scrollOffsetRef.current = targetScroll;
279
+ setScrollOffset(targetScroll);
280
+ }
281
+ };
282
+ const handleMouseUp = () => {
283
+ if (!isDraggingRef.current) return;
284
+ isDraggingRef.current = false;
285
+ thumbElement.style.cursor = '';
286
+ document.body.style.cursor = '';
287
+ document.body.style.userSelect = '';
288
+ };
289
+ thumbElement.addEventListener('mousedown', handleMouseDown);
290
+ document.addEventListener('mousemove', handleMouseMove);
291
+ document.addEventListener('mouseup', handleMouseUp);
292
+ return () => {
293
+ thumbElement.removeEventListener('mousedown', handleMouseDown);
294
+ document.removeEventListener('mousemove', handleMouseMove);
295
+ document.removeEventListener('mouseup', handleMouseUp);
296
+ };
297
+ }, [hasScrollbar]);
298
+ const handleKeyDown = React.useCallback(event => {
299
+ if (event.key === KEY_ARROW_RIGHT || event.key === KEY_SPACE) {
300
+ event.preventDefault();
301
+ if (scrollRef.current) {
302
+ scrollRef.current.scrollTo({
303
+ x: scrollOffsetRef.current + itemWidth,
304
+ animated: true
305
+ });
306
+ }
307
+ } else if (event.key === KEY_ARROW_LEFT) {
308
+ event.preventDefault();
309
+ if (scrollRef.current) {
310
+ scrollRef.current.scrollTo({
311
+ x: Math.max(INITIAL_DIMENSION, scrollOffsetRef.current - itemWidth),
312
+ animated: true
313
+ });
314
+ }
315
+ }
316
+ }, [itemWidth]);
317
+ const handleProgressPress = React.useCallback(event => {
318
+ if (scrollMax <= INITIAL_DIMENSION || isDraggingRef.current) return;
319
+ if (Platform.OS === 'web' && pressableRef.current) {
320
+ const {
321
+ offsetX
322
+ } = event.nativeEvent;
323
+ const {
324
+ offsetWidth
325
+ } = pressableRef.current;
326
+ const thumbPixelWidth = thumbWidthPercent / MAX_PERCENTAGE * offsetWidth;
327
+ const availableTrack = offsetWidth - thumbPixelWidth;
328
+ if (availableTrack <= INITIAL_DIMENSION) return;
329
+ const clickPosition = Math.max(INITIAL_DIMENSION, Math.min(availableTrack, offsetX - thumbPixelWidth / HALF_DIVISOR));
330
+ const targetX = clickPosition / availableTrack * scrollMax;
331
+ if (scrollRef.current) {
332
+ scrollRef.current.scrollTo({
333
+ x: targetX,
334
+ animated: true
335
+ });
336
+ }
337
+ } else if (pressableRef.current) {
338
+ const {
339
+ locationX
340
+ } = event.nativeEvent;
341
+ pressableRef.current.measure((_x, _y, width) => {
342
+ const thumbPixelWidth = thumbWidthPercent / MAX_PERCENTAGE * width;
343
+ const availableTrack = width - thumbPixelWidth;
344
+ if (availableTrack <= INITIAL_DIMENSION) return;
345
+ const clickPosition = Math.max(INITIAL_DIMENSION, Math.min(availableTrack, locationX - thumbPixelWidth / HALF_DIVISOR));
346
+ const targetX = clickPosition / availableTrack * scrollMax;
347
+ if (scrollRef.current) {
348
+ scrollRef.current.scrollTo({
349
+ x: targetX,
350
+ animated: true
351
+ });
352
+ }
353
+ });
354
+ }
355
+ }, [scrollMax, thumbWidthPercent]);
356
+ const handleContainerRef = React.useCallback(node => {
357
+ scrollContainerRef.current = node;
358
+ if (typeof ref === 'function') ref(node);else if (ref) Object.assign(ref, {
359
+ current: node
360
+ });
361
+ }, [ref]);
362
+ return /*#__PURE__*/_jsxs(View, {
363
+ ref: handleContainerRef,
364
+ style: staticStyles.container,
365
+ ...selectContainerA11yProps({
366
+ accessibilityLabel: resolvedAccessibilityLabel,
367
+ descriptionId,
368
+ handleKeyDown
369
+ }),
370
+ ...selectProps(rest),
371
+ children: [/*#__PURE__*/_jsx(A11yText, {
372
+ text: getCopy('instructionText'),
373
+ nativeID: descriptionId
374
+ }), /*#__PURE__*/_jsx(View, {
375
+ style: staticStyles.scrollContainer,
376
+ children: /*#__PURE__*/_jsx(ScrollViewEnd, {
377
+ ref: scrollRef,
378
+ horizontal: true,
379
+ onScrollEnd: handleScroll,
380
+ onScroll: handleScroll,
381
+ onContentSizeChange: handleContentWidth,
382
+ onLayout: handleContainerLayout,
383
+ showsHorizontalScrollIndicator: false,
384
+ contentContainerStyle: [staticStyles.scrollContent, selectScrollContentStyle(itemGap)],
385
+ children: /*#__PURE__*/_jsx(ScrollProvider, {
386
+ itemWidth: itemWidth,
387
+ itemHeight: itemHeight,
388
+ onItemLayout: handleItemLayout,
389
+ children: children
390
+ })
391
+ })
392
+ }), scrollMax > INITIAL_DIMENSION && /*#__PURE__*/_jsx(Pressable, {
393
+ ref: pressableRef,
394
+ style: [staticStyles.pressable, selectPressableStyles(defaultTokens)],
395
+ onPress: handleProgressPress,
396
+ children: pressableState => /*#__PURE__*/_jsx(ScrollProgress, {
397
+ ref: thumbRef,
398
+ pressableState: pressableState,
399
+ getTokens: getTokens,
400
+ thumbWrapperStyle: thumbWrapperStyle,
401
+ getCopy: getCopy
402
+ })
403
+ })]
404
+ });
405
+ });
406
+ Scroll.displayName = 'Scroll';
407
+ Scroll.propTypes = {
408
+ ...selectedSystemPropTypes,
409
+ /**
410
+ * Scroll items to display. Should be `ScrollItem` components.
411
+ */
412
+ children: PropTypes.node.isRequired,
413
+ /**
414
+ * Theme tokens for customizing the Scroll appearance
415
+ */
416
+ tokens: getTokensPropType('Scroll'),
417
+ /**
418
+ * Variant for the Scroll component
419
+ */
420
+ variant: variantProp.propType,
421
+ /**
422
+ * Select English or French copy for accessible labels.
423
+ */
424
+ copy: copyPropTypes,
425
+ /**
426
+ * Override the default dictionary by passing the complete dictionary object for `en` and `fr`.
427
+ */
428
+ dictionary: PropTypes.shape({
429
+ en: PropTypes.object,
430
+ fr: PropTypes.object
431
+ }),
432
+ /**
433
+ * Accessibility label for the scrollable region
434
+ */
435
+ accessibilityLabel: PropTypes.string
436
+ };
437
+ export default Scroll;
438
+ const staticStyles = StyleSheet.create({
439
+ container: {
440
+ width: '100%'
441
+ },
442
+ scrollContainer: {
443
+ width: '100%'
444
+ },
445
+ pressable: {
446
+ justifyContent: 'center',
447
+ width: '100%'
448
+ },
449
+ scrollContent: {
450
+ alignSelf: 'flex-start',
451
+ flexGrow: 1
452
+ },
453
+ thumbWrapper: {
454
+ height: '100%'
455
+ },
456
+ thumbCursor: {
457
+ cursor: 'grab'
458
+ }
459
+ });
@@ -0,0 +1,47 @@
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
+ 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
+ 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
+ };
47
+ export { ScrollProvider, useScroll };
@@ -0,0 +1,96 @@
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 { layoutTags, getA11yPropsFromHtmlTag, selectSystemProps, a11yProps, viewProps } from '../utils';
6
+ import { useScroll } from './ScrollContext';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, viewProps]);
9
+ const DEFAULT_TAG = 'li';
10
+ const DATA_SCROLL_ITEM = 'scrollItem';
11
+ const INITIAL_DIMENSION = 0;
12
+ const TAB_INDEX = 0;
13
+ const selectContainerStyle = _ref => {
14
+ let {
15
+ itemWidth,
16
+ itemHeight
17
+ } = _ref;
18
+ const style = {};
19
+ if (itemWidth > INITIAL_DIMENSION) {
20
+ style.width = itemWidth;
21
+ }
22
+ if (itemHeight > INITIAL_DIMENSION) {
23
+ style.height = itemHeight;
24
+ }
25
+ return style;
26
+ };
27
+
28
+ /**
29
+ * The `ScrollItem` component wraps individual items within a `Scroll` container. Each item
30
+ * is individually focusable and reports its natural dimensions so the parent can apply uniform
31
+ * sizing across all items.
32
+ *
33
+ * ## Props
34
+ *
35
+ * - Use `children` to provide the content of the scroll item
36
+ * - Use `tag` to set the HTML tag of the outer container (defaults to `'li'`)
37
+ *
38
+ */
39
+ const ScrollItem = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
40
+ let {
41
+ children,
42
+ tag = DEFAULT_TAG,
43
+ ...rest
44
+ } = _ref2;
45
+ const {
46
+ itemWidth,
47
+ itemHeight,
48
+ onItemLayout
49
+ } = useScroll();
50
+ const handleLayout = React.useCallback(_ref3 => {
51
+ let {
52
+ nativeEvent: {
53
+ layout: {
54
+ width,
55
+ height
56
+ }
57
+ }
58
+ } = _ref3;
59
+ onItemLayout?.(width, height);
60
+ }, [onItemLayout]);
61
+ const selectedProps = selectProps({
62
+ ...rest,
63
+ ...getA11yPropsFromHtmlTag(tag, rest.accessibilityRole)
64
+ });
65
+ return /*#__PURE__*/_jsx(View, {
66
+ ref: ref,
67
+ style: selectContainerStyle({
68
+ itemWidth,
69
+ itemHeight
70
+ }),
71
+ onLayout: handleLayout,
72
+ dataSet: {
73
+ [DATA_SCROLL_ITEM]: true
74
+ },
75
+ ...(Platform.OS === 'web' ? {
76
+ tabIndex: TAB_INDEX
77
+ } : {
78
+ focusable: true
79
+ }),
80
+ ...selectedProps,
81
+ children: children
82
+ });
83
+ });
84
+ ScrollItem.displayName = 'ScrollItem';
85
+ ScrollItem.propTypes = {
86
+ ...selectedSystemPropTypes,
87
+ /**
88
+ * Content of the scroll item
89
+ */
90
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
91
+ /**
92
+ * Sets the HTML tag of the outer container. Defaults to `'li'`.
93
+ */
94
+ tag: PropTypes.oneOf(layoutTags)
95
+ };
96
+ export default /*#__PURE__*/React.memo(ScrollItem);
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import View from "react-native-web/dist/exports/View";
4
+ import { resolvePressableState, useAnimatedValue } from '../utils';
5
+ import Progress from '../Progress';
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ const MAX_PERCENTAGE = 100;
8
+ const BORDER_RADIUS_DIVISOR = 2;
9
+ const staticProgressTokens = {
10
+ borderWidth: 0,
11
+ borderColor: 'transparent'
12
+ };
13
+ const staticProgressBarTokens = {
14
+ gradient: null,
15
+ outlineWidth: 0
16
+ };
17
+ const selectProgressTokens = _ref => {
18
+ let {
19
+ progressTrackHeight,
20
+ progressBarHeight,
21
+ progressTrackColor
22
+ } = _ref;
23
+ return {
24
+ height: progressTrackHeight,
25
+ barHeight: progressBarHeight,
26
+ backgroundColor: progressTrackColor,
27
+ borderRadius: progressTrackHeight / BORDER_RADIUS_DIVISOR
28
+ };
29
+ };
30
+ const selectBarTokens = _ref2 => {
31
+ let {
32
+ progressBarColor,
33
+ progressBarHeight
34
+ } = _ref2;
35
+ return {
36
+ backgroundColor: progressBarColor,
37
+ borderRadius: progressBarHeight / BORDER_RADIUS_DIVISOR
38
+ };
39
+ };
40
+ const ScrollProgress = /*#__PURE__*/React.forwardRef((_ref3, ref) => {
41
+ let {
42
+ pressableState,
43
+ getTokens,
44
+ thumbWrapperStyle,
45
+ getCopy
46
+ } = _ref3;
47
+ const state = resolvePressableState(pressableState);
48
+ const themeTokens = getTokens(state);
49
+ const animatedBarHeight = useAnimatedValue(themeTokens.progressBarHeight);
50
+ const animatedTokens = {
51
+ ...themeTokens,
52
+ progressBarHeight: animatedBarHeight
53
+ };
54
+ return /*#__PURE__*/_jsx(Progress, {
55
+ tokens: {
56
+ ...staticProgressTokens,
57
+ ...selectProgressTokens(animatedTokens)
58
+ },
59
+ children: /*#__PURE__*/_jsx(View, {
60
+ ref: ref,
61
+ style: thumbWrapperStyle,
62
+ children: /*#__PURE__*/_jsx(Progress.Bar, {
63
+ percentage: MAX_PERCENTAGE,
64
+ tokens: {
65
+ ...staticProgressBarTokens,
66
+ ...selectBarTokens(animatedTokens)
67
+ },
68
+ accessibilityLabel: getCopy('progressBarLabel')
69
+ })
70
+ })
71
+ });
72
+ });
73
+ ScrollProgress.displayName = 'ScrollProgress';
74
+ ScrollProgress.propTypes = {
75
+ /**
76
+ * State object from Pressable's render callback containing hovered, pressed, and focused states
77
+ */
78
+ pressableState: PropTypes.object.isRequired,
79
+ /**
80
+ * Theme token resolver callback from useThemeTokensCallback that accepts appearance state
81
+ */
82
+ getTokens: PropTypes.func.isRequired,
83
+ /**
84
+ * Style array for the thumb wrapper containing static and dynamic position styles
85
+ */
86
+ thumbWrapperStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
87
+ /**
88
+ * Copy resolver function from useCopy for retrieving localized strings
89
+ */
90
+ getCopy: PropTypes.func.isRequired
91
+ };
92
+ export default ScrollProgress;
@@ -0,0 +1,12 @@
1
+ export default {
2
+ en: {
3
+ accessibilityLabel: 'Scrollable content',
4
+ instructionText: 'Use Tab, arrow keys, or space bar to navigate',
5
+ progressBarLabel: 'Scroll position'
6
+ },
7
+ fr: {
8
+ accessibilityLabel: 'Contenu défilable',
9
+ instructionText: "Utilisez Tab, les touches fléchées ou la barre d'espace pour naviguer",
10
+ progressBarLabel: 'Position de défilement'
11
+ }
12
+ };