@utilitywarehouse/hearth-react-native 0.4.2 → 0.5.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 (180) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +44 -0
  4. package/build/components/Alert/AlertTitle.js +6 -6
  5. package/build/components/Badge/Badge.js +3 -3
  6. package/build/components/Badge/Badge.props.d.ts +1 -0
  7. package/build/components/Button/ButtonRoot.js +4 -0
  8. package/build/components/Button/ButtonText.js +2 -2
  9. package/build/components/Card/CardRoot.js +1 -1
  10. package/build/components/Carousel/Carousel.context.d.ts +4 -0
  11. package/build/components/Carousel/Carousel.context.js +4 -0
  12. package/build/components/Carousel/Carousel.d.ts +6 -0
  13. package/build/components/Carousel/Carousel.js +278 -0
  14. package/build/components/Carousel/Carousel.props.d.ts +65 -0
  15. package/build/components/Carousel/Carousel.props.js +1 -0
  16. package/build/components/Carousel/CarouselControlItem.d.ts +24 -0
  17. package/build/components/Carousel/CarouselControlItem.js +64 -0
  18. package/build/components/Carousel/CarouselControls.d.ts +4 -0
  19. package/build/components/Carousel/CarouselControls.js +74 -0
  20. package/build/components/Carousel/CarouselItem.d.ts +6 -0
  21. package/build/components/Carousel/CarouselItem.js +38 -0
  22. package/build/components/Carousel/index.d.ts +5 -0
  23. package/build/components/Carousel/index.js +5 -0
  24. package/build/components/DescriptionList/DescriptionList.d.ts +1 -1
  25. package/build/components/DescriptionList/DescriptionList.js +2 -2
  26. package/build/components/DescriptionList/DescriptionList.props.d.ts +1 -8
  27. package/build/components/DescriptionList/DescriptionListItem.d.ts +1 -1
  28. package/build/components/DescriptionList/DescriptionListItem.js +4 -3
  29. package/build/components/DescriptionList/DescriptionListItem.props.d.ts +3 -8
  30. package/build/components/IndicatorIconButton/IndicatorIconButton.d.ts +6 -0
  31. package/build/components/IndicatorIconButton/IndicatorIconButton.js +26 -0
  32. package/build/components/IndicatorIconButton/IndicatorIconButton.props.d.ts +8 -0
  33. package/build/components/IndicatorIconButton/IndicatorIconButton.props.js +1 -0
  34. package/build/components/IndicatorIconButton/index.d.ts +2 -0
  35. package/build/components/IndicatorIconButton/index.js +1 -0
  36. package/build/components/Link/LinkText.js +3 -3
  37. package/build/components/List/List.context.d.ts +0 -2
  38. package/build/components/List/List.d.ts +1 -1
  39. package/build/components/List/List.js +5 -5
  40. package/build/components/List/List.props.d.ts +1 -9
  41. package/build/components/List/ListAction/ListAction.d.ts +18 -0
  42. package/build/components/List/ListAction/ListAction.js +103 -0
  43. package/build/components/List/ListAction/ListAction.props.d.ts +8 -0
  44. package/build/components/List/ListAction/ListAction.props.js +1 -0
  45. package/build/components/List/ListAction/ListActionContent.d.ts +6 -0
  46. package/build/components/List/ListAction/ListActionContent.js +14 -0
  47. package/build/components/List/ListAction/ListActionText.d.ts +6 -0
  48. package/build/components/List/ListAction/ListActionText.js +7 -0
  49. package/build/components/List/ListAction/ListActionTrailingContent.d.ts +6 -0
  50. package/build/components/List/ListAction/ListActionTrailingContent.js +5 -0
  51. package/build/components/List/ListAction/ListActionTrailingIcon.d.ts +9 -0
  52. package/build/components/List/ListAction/ListActionTrailingIcon.js +18 -0
  53. package/build/components/List/ListAction/index.d.ts +6 -0
  54. package/build/components/List/ListAction/index.js +5 -0
  55. package/build/components/List/ListItem/ListItem.context.d.ts +1 -1
  56. package/build/components/List/ListItem/ListItem.props.d.ts +9 -5
  57. package/build/components/List/ListItem/ListItemRoot.d.ts +1 -1
  58. package/build/components/List/ListItem/ListItemRoot.js +10 -12
  59. package/build/components/List/ListItem/index.d.ts +4 -4
  60. package/build/components/List/ListItem/index.js +3 -3
  61. package/build/components/List/index.d.ts +1 -0
  62. package/build/components/List/index.js +1 -0
  63. package/build/components/ProgressStepper/ProgressStep.d.ts +10 -0
  64. package/build/components/ProgressStepper/ProgressStep.js +100 -0
  65. package/build/components/ProgressStepper/ProgressStepper.d.ts +6 -0
  66. package/build/components/ProgressStepper/ProgressStepper.js +22 -0
  67. package/build/components/ProgressStepper/ProgressStepper.props.d.ts +22 -0
  68. package/build/components/ProgressStepper/ProgressStepper.props.js +1 -0
  69. package/build/components/ProgressStepper/ProgressStepperRoot.d.ts +6 -0
  70. package/build/components/ProgressStepper/ProgressStepperRoot.js +16 -0
  71. package/build/components/ProgressStepper/index.d.ts +3 -0
  72. package/build/components/ProgressStepper/index.js +2 -0
  73. package/build/components/SectionHeader/SectionHeader.d.ts +1 -1
  74. package/build/components/SectionHeader/SectionHeader.js +6 -3
  75. package/build/components/SectionHeader/SectionHeader.props.d.ts +9 -16
  76. package/build/components/SectionHeader/SectionHeaderTrailingContent.d.ts +6 -0
  77. package/build/components/SectionHeader/SectionHeaderTrailingContent.js +13 -0
  78. package/build/components/SectionHeader/index.d.ts +1 -0
  79. package/build/components/SectionHeader/index.js +1 -0
  80. package/build/components/Tabs/Tab.js +2 -2
  81. package/build/components/ToggleButton/ToggleButtonText.js +2 -2
  82. package/build/components/UnstyledIconButton/UnstyledIconButton.props.d.ts +4 -1
  83. package/build/components/index.d.ts +3 -0
  84. package/build/components/index.js +3 -0
  85. package/build/core/themes.d.ts +12 -24
  86. package/build/tokens/components/dark/button.d.ts +1 -1
  87. package/build/tokens/components/dark/button.js +1 -1
  88. package/build/tokens/components/dark/dialog.d.ts +1 -0
  89. package/build/tokens/components/dark/dialog.js +1 -0
  90. package/build/tokens/components/dark/illustrations.d.ts +1 -0
  91. package/build/tokens/components/dark/illustrations.js +1 -0
  92. package/build/tokens/components/dark/toast.d.ts +4 -1
  93. package/build/tokens/components/dark/toast.js +4 -1
  94. package/build/tokens/components/light/button.d.ts +1 -1
  95. package/build/tokens/components/light/button.js +1 -1
  96. package/build/tokens/components/light/dialog.d.ts +1 -0
  97. package/build/tokens/components/light/dialog.js +1 -0
  98. package/build/tokens/components/light/illustrations.d.ts +1 -0
  99. package/build/tokens/components/light/illustrations.js +1 -0
  100. package/build/tokens/components/light/toast.d.ts +4 -1
  101. package/build/tokens/components/light/toast.js +4 -1
  102. package/build/tokens/layout.d.ts +6 -12
  103. package/build/tokens/layout.js +3 -6
  104. package/docs/components/AllComponents.web.tsx +86 -4
  105. package/docs/components/BadgeList.tsx +20 -56
  106. package/docs/components/SwitchList.tsx +4 -8
  107. package/docs/getting-started.mdx +30 -14
  108. package/docs/introduction.mdx +1 -1
  109. package/package.json +4 -4
  110. package/src/components/Alert/AlertTitle.tsx +7 -7
  111. package/src/components/Badge/Badge.props.ts +1 -0
  112. package/src/components/Badge/Badge.tsx +3 -2
  113. package/src/components/Button/ButtonRoot.tsx +4 -0
  114. package/src/components/Button/ButtonText.tsx +3 -3
  115. package/src/components/Card/CardRoot.tsx +2 -0
  116. package/src/components/Carousel/Carousel.context.tsx +8 -0
  117. package/src/components/Carousel/Carousel.docs.mdx +389 -0
  118. package/src/components/Carousel/Carousel.props.ts +89 -0
  119. package/src/components/Carousel/Carousel.stories.tsx +317 -0
  120. package/src/components/Carousel/Carousel.tsx +444 -0
  121. package/src/components/Carousel/CarouselControlItem.tsx +87 -0
  122. package/src/components/Carousel/CarouselControls.tsx +150 -0
  123. package/src/components/Carousel/CarouselItem.tsx +68 -0
  124. package/src/components/Carousel/index.ts +6 -0
  125. package/src/components/DescriptionList/DescriptionList.docs.mdx +24 -27
  126. package/src/components/DescriptionList/DescriptionList.props.ts +1 -8
  127. package/src/components/DescriptionList/DescriptionList.stories.tsx +13 -19
  128. package/src/components/DescriptionList/DescriptionList.tsx +2 -14
  129. package/src/components/DescriptionList/DescriptionListItem.props.ts +3 -8
  130. package/src/components/DescriptionList/DescriptionListItem.tsx +13 -21
  131. package/src/components/IndicatorIconButton/IndicatorIconButton.docs.mdx +85 -0
  132. package/src/components/IndicatorIconButton/IndicatorIconButton.props.ts +12 -0
  133. package/src/components/IndicatorIconButton/IndicatorIconButton.stories.tsx +142 -0
  134. package/src/components/IndicatorIconButton/IndicatorIconButton.tsx +36 -0
  135. package/src/components/IndicatorIconButton/index.tsx +2 -0
  136. package/src/components/Link/LinkText.tsx +4 -4
  137. package/src/components/List/List.context.ts +0 -1
  138. package/src/components/List/List.docs.mdx +376 -179
  139. package/src/components/List/List.props.ts +1 -9
  140. package/src/components/List/List.stories.tsx +289 -38
  141. package/src/components/List/List.tsx +5 -26
  142. package/src/components/List/ListAction/ListAction.props.ts +10 -0
  143. package/src/components/List/ListAction/ListAction.tsx +133 -0
  144. package/src/components/List/ListAction/ListActionContent.tsx +21 -0
  145. package/src/components/List/ListAction/ListActionText.tsx +14 -0
  146. package/src/components/List/ListAction/ListActionTrailingContent.tsx +9 -0
  147. package/src/components/List/ListAction/ListActionTrailingIcon.tsx +32 -0
  148. package/src/components/List/ListAction/index.ts +6 -0
  149. package/src/components/List/ListItem/ListItem.context.ts +1 -1
  150. package/src/components/List/ListItem/ListItem.props.ts +9 -5
  151. package/src/components/List/ListItem/ListItemRoot.tsx +18 -14
  152. package/src/components/List/ListItem/index.ts +4 -4
  153. package/src/components/List/index.ts +1 -0
  154. package/src/components/ProgressStepper/ProgressStep.tsx +134 -0
  155. package/src/components/ProgressStepper/ProgressStepper.docs.mdx +87 -0
  156. package/src/components/ProgressStepper/ProgressStepper.props.ts +27 -0
  157. package/src/components/ProgressStepper/ProgressStepper.stories.tsx +108 -0
  158. package/src/components/ProgressStepper/ProgressStepper.tsx +26 -0
  159. package/src/components/ProgressStepper/ProgressStepperRoot.tsx +32 -0
  160. package/src/components/ProgressStepper/index.ts +3 -0
  161. package/src/components/SectionHeader/SectionHeader.props.ts +9 -16
  162. package/src/components/SectionHeader/SectionHeader.stories.tsx +28 -18
  163. package/src/components/SectionHeader/SectionHeader.tsx +18 -19
  164. package/src/components/SectionHeader/SectionHeaderTrailingContent.tsx +20 -0
  165. package/src/components/SectionHeader/Sectionheader.docs.mdx +9 -24
  166. package/src/components/SectionHeader/index.ts +1 -0
  167. package/src/components/Switch/Switch.docs.mdx +0 -4
  168. package/src/components/Tabs/Tab.tsx +4 -2
  169. package/src/components/ToggleButton/ToggleButtonText.tsx +3 -3
  170. package/src/components/UnstyledIconButton/UnstyledIconButton.props.ts +2 -1
  171. package/src/components/index.ts +3 -0
  172. package/src/tokens/components/dark/button.ts +1 -1
  173. package/src/tokens/components/dark/dialog.ts +1 -0
  174. package/src/tokens/components/dark/illustrations.ts +1 -0
  175. package/src/tokens/components/dark/toast.ts +4 -1
  176. package/src/tokens/components/light/button.ts +1 -1
  177. package/src/tokens/components/light/dialog.ts +1 -0
  178. package/src/tokens/components/light/illustrations.ts +1 -0
  179. package/src/tokens/components/light/toast.ts +4 -1
  180. package/src/tokens/layout.ts +3 -6
@@ -0,0 +1,444 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ ReactElement,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import {
13
+ FlatList,
14
+ NativeScrollEvent,
15
+ NativeSyntheticEvent,
16
+ Platform,
17
+ ScrollView,
18
+ View,
19
+ ViewStyle,
20
+ ViewToken,
21
+ } from 'react-native';
22
+ import { StyleSheet } from 'react-native-unistyles';
23
+
24
+ import CarouselContext from './Carousel.context';
25
+ import { CarouselItemProps, CarouselProps } from './Carousel.props';
26
+ import { CarouselControls } from './CarouselControls';
27
+ import { CarouselItem } from './CarouselItem';
28
+
29
+ const Carousel = ({
30
+ centered = false,
31
+ children,
32
+ disabled = false,
33
+ inactiveItemOpacity = 1,
34
+ itemWidth,
35
+ onSnapToItem,
36
+ showOverflow = false,
37
+ style,
38
+ width,
39
+ itemsStyle,
40
+ activeIndex: initialActiveIndex = 0,
41
+ showControls = true,
42
+ showNavigation = false,
43
+ controlsStyle,
44
+ controlsItemStyle,
45
+ controlsActiveItemStyle,
46
+ controlsAccessibilityHidden = true,
47
+ inverted = false,
48
+ ref,
49
+ ...props
50
+ }: CarouselProps) => {
51
+ const [activeIndex, setActiveIndex] = useState(initialActiveIndex);
52
+ const [numItems, setNumItems] = useState(0);
53
+
54
+ const scrollViewRef = useRef<ScrollView>(null);
55
+ const flatListRef = useRef<FlatList>(null);
56
+ const isWeb = Platform.OS === 'web';
57
+ const isProgrammaticScroll = useRef(false);
58
+
59
+ // Expose scroll methods through ref
60
+ useImperativeHandle(
61
+ ref,
62
+ () => ({
63
+ scrollToIndex: ({ index, animated = true }: { index: number; animated?: boolean | null }) => {
64
+ if (isWeb && scrollViewRef.current) {
65
+ isProgrammaticScroll.current = true;
66
+ const itemWidthValue = itemWidth || width;
67
+ const offset = centered ? (width - (itemWidth || width)) / 2 : 0;
68
+ const scrollX = index * itemWidthValue - offset;
69
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: animated ?? true });
70
+ setTimeout(() => {
71
+ isProgrammaticScroll.current = false;
72
+ }, 500);
73
+ } else if (flatListRef.current) {
74
+ flatListRef.current.scrollToIndex({ index, animated: animated ?? true });
75
+ }
76
+ setActiveIndex(index);
77
+ },
78
+ scrollToOffset: ({
79
+ offset,
80
+ animated = true,
81
+ }: {
82
+ offset: number;
83
+ animated?: boolean | null;
84
+ }) => {
85
+ if (isWeb && scrollViewRef.current) {
86
+ isProgrammaticScroll.current = true;
87
+ scrollViewRef.current.scrollTo({ x: offset, animated: animated ?? true });
88
+ setTimeout(() => {
89
+ isProgrammaticScroll.current = false;
90
+ }, 500);
91
+ } else if (flatListRef.current) {
92
+ flatListRef.current.scrollToOffset({ offset, animated: animated ?? true });
93
+ }
94
+ },
95
+ }),
96
+ [isWeb, itemWidth, width, centered]
97
+ );
98
+
99
+ // Recursively check if CarouselControls exists in the children tree
100
+ const hasCarouselControlsInTree = (node: React.ReactNode): boolean => {
101
+ if (!node) return false;
102
+
103
+ if (typeof node === 'object' && 'type' in node) {
104
+ if (
105
+ node.type === CarouselControls ||
106
+ (node.type as any)?.displayName === 'CarouselControls'
107
+ ) {
108
+ return true;
109
+ }
110
+
111
+ // Check children recursively
112
+ if ('props' in node && node.props && typeof node.props === 'object') {
113
+ const props = node.props as { children?: React.ReactNode };
114
+ if (props.children) {
115
+ const childArray = Children.toArray(props.children);
116
+ return childArray.some(child => hasCarouselControlsInTree(child));
117
+ }
118
+ }
119
+ }
120
+
121
+ return false;
122
+ };
123
+
124
+ // Filter only CarouselItem children for the carousel
125
+ const { carouselItems, otherChildren, hasCarouselControls } = useMemo(() => {
126
+ const childrenArray = Children.toArray(children);
127
+ const carouselItems: Array<ReactElement<CarouselItemProps>> = [];
128
+ const otherChildren: Array<React.ReactNode> = [];
129
+
130
+ childrenArray.forEach(child => {
131
+ if (
132
+ typeof child === 'object' &&
133
+ 'type' in child &&
134
+ (child.type === CarouselItem || (child.type as any)?.displayName === 'CarouselItem')
135
+ ) {
136
+ carouselItems.push(child as ReactElement<CarouselItemProps>);
137
+ } else {
138
+ otherChildren.push(child);
139
+ }
140
+ });
141
+
142
+ // Recursively check if any CarouselControls exist in the tree
143
+ const hasCarouselControls = childrenArray.some(child => hasCarouselControlsInTree(child));
144
+
145
+ return { carouselItems, otherChildren, hasCarouselControls };
146
+ }, [children]);
147
+
148
+ const innerMargin: number = width - (itemWidth || width);
149
+ const containerStyles: ViewStyle = {
150
+ marginHorizontal: centered ? innerMargin / 2 : 0,
151
+ overflow: showOverflow ? 'visible' : 'hidden',
152
+ };
153
+
154
+ const context = useMemo(
155
+ () => ({
156
+ activeIndex,
157
+ numItems: carouselItems.length,
158
+ setActiveIndex,
159
+ setNumItems,
160
+ controlsAccessibilityHidden,
161
+ inverted,
162
+ disabled,
163
+ }),
164
+ [
165
+ activeIndex,
166
+ carouselItems.length,
167
+ setActiveIndex,
168
+ setNumItems,
169
+ controlsAccessibilityHidden,
170
+ inverted,
171
+ disabled,
172
+ ]
173
+ );
174
+
175
+ // On web, we need to prevent overflow from expanding the container
176
+ const webContainerStyles: ViewStyle = isWeb
177
+ ? {
178
+ width,
179
+ maxWidth: width,
180
+ overflow: 'hidden', // Always clip on web to prevent expansion
181
+ }
182
+ : {};
183
+
184
+ // For web centered layouts, add padding to content container
185
+ const webContentContainerStyle: ViewStyle =
186
+ isWeb && centered
187
+ ? {
188
+ paddingHorizontal: innerMargin / 2,
189
+ }
190
+ : {};
191
+
192
+ useEffect(() => {
193
+ setNumItems((carouselItems || []).length);
194
+ }, [carouselItems, setNumItems]);
195
+
196
+ // Set initial scroll position on web
197
+ useEffect(() => {
198
+ if (isWeb && scrollViewRef.current && initialActiveIndex > 0) {
199
+ // Use setTimeout to ensure ScrollView is mounted
200
+ setTimeout(() => {
201
+ if (scrollViewRef.current) {
202
+ const itemWidthValue = itemWidth || width;
203
+ const offset = centered ? innerMargin / 2 : 0;
204
+ const scrollX = initialActiveIndex * itemWidthValue - offset;
205
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: false });
206
+ }
207
+ }, 0);
208
+ }
209
+ }, [isWeb, initialActiveIndex, itemWidth, width, centered, innerMargin]);
210
+
211
+ // Scroll to active index when it changes (for programmatic navigation)
212
+ useEffect(() => {
213
+ if (!isProgrammaticScroll.current) {
214
+ isProgrammaticScroll.current = true;
215
+
216
+ if (isWeb && scrollViewRef.current) {
217
+ const itemWidthValue = itemWidth || width;
218
+ const offset = centered ? innerMargin / 2 : 0;
219
+ const scrollX = activeIndex * itemWidthValue - offset;
220
+
221
+ scrollViewRef.current.scrollTo({ x: scrollX, animated: true });
222
+ } else if (
223
+ !isWeb &&
224
+ flatListRef.current &&
225
+ activeIndex >= 0 &&
226
+ activeIndex < carouselItems.length
227
+ ) {
228
+ flatListRef.current.scrollToIndex({ index: activeIndex, animated: true });
229
+ }
230
+
231
+ setTimeout(() => {
232
+ isProgrammaticScroll.current = false;
233
+ }, 500);
234
+ }
235
+ }, [activeIndex, isWeb, itemWidth, width, centered, innerMargin, carouselItems.length]);
236
+
237
+ // Web scroll handler - track scroll position
238
+ const handleWebScroll = useCallback(
239
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
240
+ if (!isWeb || isProgrammaticScroll.current) return;
241
+
242
+ const scrollX = event.nativeEvent.contentOffset.x;
243
+ const itemWidthValue = itemWidth || width;
244
+
245
+ // For centered layouts, account for the padding offset
246
+ const offset = centered ? innerMargin / 2 : 0;
247
+ const index = Math.round((scrollX + offset) / itemWidthValue);
248
+
249
+ if (index >= 0 && index < carouselItems.length && index !== activeIndex) {
250
+ setActiveIndex?.(index);
251
+ }
252
+ },
253
+ [
254
+ activeIndex,
255
+ centered,
256
+ innerMargin,
257
+ isWeb,
258
+ itemWidth,
259
+ carouselItems.length,
260
+ setActiveIndex,
261
+ width,
262
+ ]
263
+ );
264
+
265
+ // Web scroll end handler - update when scroll completes
266
+ const handleWebScrollEnd = useCallback(
267
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
268
+ if (!isWeb || isProgrammaticScroll.current) return;
269
+
270
+ const scrollX = event.nativeEvent.contentOffset.x;
271
+ const itemWidthValue = itemWidth || width;
272
+
273
+ // For centered layouts, account for the padding offset
274
+ const offset = centered ? innerMargin / 2 : 0;
275
+ const index = Math.round((scrollX + offset) / itemWidthValue);
276
+
277
+ if (index >= 0 && index < carouselItems.length) {
278
+ setActiveIndex?.(index);
279
+ onSnapToItem?.(index);
280
+ }
281
+ },
282
+ [
283
+ centered,
284
+ innerMargin,
285
+ isWeb,
286
+ itemWidth,
287
+ carouselItems.length,
288
+ onSnapToItem,
289
+ setActiveIndex,
290
+ width,
291
+ ]
292
+ );
293
+
294
+ // Native viewable items handler
295
+ const handleViewableItemsChanged = useCallback(
296
+ ({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
297
+ if (!viewableItems.length) {
298
+ return;
299
+ }
300
+
301
+ const index = viewableItems[viewableItems.length - 1].index || 0;
302
+
303
+ setActiveIndex?.(index);
304
+ onSnapToItem?.(index);
305
+ },
306
+ [onSnapToItem, setActiveIndex]
307
+ );
308
+
309
+ const controls = (
310
+ <CarouselControls
311
+ style={[styles.controls, controlsStyle]}
312
+ itemStyle={controlsItemStyle}
313
+ activeItemStyle={controlsActiveItemStyle}
314
+ showNavigation={showNavigation}
315
+ accessibilityHidden={controlsAccessibilityHidden}
316
+ />
317
+ );
318
+
319
+ // Render for web using ScrollView with scroll snap
320
+ if (isWeb) {
321
+ return (
322
+ <CarouselContext.Provider value={context}>
323
+ <View style={style}>
324
+ <ScrollView
325
+ horizontal
326
+ onScroll={handleWebScroll}
327
+ onMomentumScrollEnd={handleWebScrollEnd}
328
+ onScrollEndDrag={handleWebScrollEnd}
329
+ ref={scrollViewRef as any}
330
+ scrollEnabled={!disabled}
331
+ pointerEvents={disabled ? 'none' : 'auto'}
332
+ scrollEventThrottle={16}
333
+ showsHorizontalScrollIndicator={false}
334
+ snapToInterval={itemWidth || width}
335
+ snapToAlignment={centered ? 'center' : 'start'}
336
+ decelerationRate="fast"
337
+ style={[styles.webContainer, webContainerStyles, itemsStyle]}
338
+ contentContainerStyle={[styles.webContentContainer, webContentContainerStyle]}
339
+ {...props}
340
+ >
341
+ {carouselItems.map((item, index) =>
342
+ cloneElement(item, {
343
+ active: index === activeIndex,
344
+ inactiveOpacity: inactiveItemOpacity,
345
+ key: item?.key || item.props?.id || index,
346
+ width: itemWidth || width,
347
+ style: [
348
+ item.props?.style,
349
+ styles.webItem,
350
+ centered ? styles.webItemCentered : styles.webItemStart,
351
+ ],
352
+ })
353
+ )}
354
+ </ScrollView>
355
+ {showControls && !hasCarouselControls && controls}
356
+ {otherChildren}
357
+ </View>
358
+ </CarouselContext.Provider>
359
+ );
360
+ }
361
+
362
+ // Render for native using FlatList
363
+ return (
364
+ <CarouselContext.Provider value={context}>
365
+ <View style={style}>
366
+ <FlatList<ReactElement<CarouselItemProps>>
367
+ ref={flatListRef as any}
368
+ bounces={false} // Prevents bouncing at the start and end of carousel scrolling (iOS only)
369
+ data={carouselItems}
370
+ decelerationRate="fast"
371
+ disableIntervalMomentum // Prevents scrolling more than one item at a time
372
+ getItemLayout={(_, index) => ({
373
+ length: itemWidth || width,
374
+ offset: (itemWidth || width) * index,
375
+ index,
376
+ })}
377
+ horizontal
378
+ initialScrollIndex={activeIndex}
379
+ pagingEnabled
380
+ onViewableItemsChanged={handleViewableItemsChanged}
381
+ overScrollMode="never" // Prevents stretching of first and last items when reaching each end of the carousel (Android only)
382
+ removeClippedSubviews={!showOverflow}
383
+ renderItem={({ index, item }) =>
384
+ cloneElement(item, {
385
+ active: index === activeIndex,
386
+ inactiveOpacity: inactiveItemOpacity,
387
+ key: item?.key || item.props?.id || index,
388
+ width: itemWidth || width,
389
+ })
390
+ }
391
+ scrollEnabled={!disabled}
392
+ showsHorizontalScrollIndicator={false}
393
+ snapToInterval={itemWidth || width}
394
+ snapToAlignment="center"
395
+ style={[itemsStyle, containerStyles]}
396
+ viewabilityConfig={{ itemVisiblePercentThreshold: 51 }}
397
+ {...props}
398
+ />
399
+ {showControls && !hasCarouselControls && controls}
400
+ {otherChildren}
401
+ </View>
402
+ </CarouselContext.Provider>
403
+ );
404
+ };
405
+
406
+ Carousel.displayName = 'Carousel';
407
+
408
+ const styles = StyleSheet.create(theme => ({
409
+ controls: {
410
+ marginTop: theme.space['200'],
411
+ },
412
+ webContainer: {
413
+ _web: {
414
+ scrollSnapType: 'x mandatory',
415
+ WebkitOverflowScrolling: 'touch',
416
+ overflowX: 'scroll',
417
+ overflowY: 'hidden',
418
+ },
419
+ },
420
+ webContentContainer: {
421
+ _web: {
422
+ display: 'flex',
423
+ flexDirection: 'row',
424
+ },
425
+ },
426
+ webItem: {
427
+ _web: {
428
+ scrollSnapStop: 'always',
429
+ flexShrink: 0,
430
+ },
431
+ },
432
+ webItemCentered: {
433
+ _web: {
434
+ scrollSnapAlign: 'center',
435
+ },
436
+ },
437
+ webItemStart: {
438
+ _web: {
439
+ scrollSnapAlign: 'start',
440
+ },
441
+ },
442
+ }));
443
+
444
+ export default Carousel;
@@ -0,0 +1,87 @@
1
+ import { createPressable } from '@gluestack-ui/pressable';
2
+ import { useContext } from 'react';
3
+ import { Pressable, View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import CarouselContext from './Carousel.context';
6
+ import { CarouselControlsItemProps } from './Carousel.props';
7
+
8
+ export const CarouselControlItemRoot = ({
9
+ active,
10
+ style,
11
+ index,
12
+ activeStyle,
13
+ onPress,
14
+ disabled,
15
+ ...props
16
+ }: CarouselControlsItemProps & { states?: { active?: boolean; disabled?: boolean } }) => {
17
+ const { inverted, numItems } = useContext(CarouselContext);
18
+ styles.useVariants({ active, inverted });
19
+ return (
20
+ <Pressable
21
+ style={[styles.item, active && activeStyle]}
22
+ onPress={onPress}
23
+ disabled={disabled}
24
+ accessibilityLabel={`Page ${index + 1} of ${numItems}`}
25
+ {...props}
26
+ >
27
+ <View style={[styles.circle, style]} />
28
+ </Pressable>
29
+ );
30
+ };
31
+
32
+ const CarouselControlItem = createPressable({
33
+ Root: CarouselControlItemRoot,
34
+ });
35
+
36
+ CarouselControlItem.displayName = 'CarouselControlItem';
37
+
38
+ const styles = StyleSheet.create(theme => ({
39
+ item: {
40
+ width: 24,
41
+ height: 24,
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ },
45
+ circle: {
46
+ width: theme.components.carouselControl.size,
47
+ height: theme.components.carouselControl.size,
48
+ borderRadius: theme.borderRadius.full,
49
+ borderColor: theme.color.interactive.functional.border.subtle,
50
+ borderWidth: theme.borderWidth[2],
51
+ backgroundColor: 'transparent',
52
+ paddingTop: theme.space['100'],
53
+ overflow: 'hidden',
54
+ _hover: {
55
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
56
+ },
57
+ variants: {
58
+ active: {
59
+ true: {
60
+ backgroundColor: theme.color.interactive.functional.foreground.subtle,
61
+ _hover: {
62
+ backgroundColor: theme.color.interactive.functional.foreground.subtle,
63
+ },
64
+ },
65
+ },
66
+ inverted: {
67
+ true: {
68
+ borderColor: theme.color.interactive.functional.border.inverted,
69
+ _hover: {
70
+ backgroundColor: theme.color.interactive.functional.surface.subtle.inverted.hover,
71
+ },
72
+ },
73
+ },
74
+ },
75
+ compoundVariants: [
76
+ {
77
+ active: true,
78
+ inverted: true,
79
+ styles: {
80
+ backgroundColor: theme.color.interactive.functional.foreground.inverted,
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ }));
86
+
87
+ export default CarouselControlItem;
@@ -0,0 +1,150 @@
1
+ import { nanoid } from 'nanoid/non-secure';
2
+ import { FC, useContext, useEffect, useMemo } from 'react';
3
+ import { StyleSheet } from 'react-native-unistyles';
4
+ import { View } from 'react-native';
5
+ import {
6
+ ChevronLeftSmallIcon,
7
+ ChevronRightSmallIcon,
8
+ } from '@utilitywarehouse/hearth-react-native-icons';
9
+ import { UnstyledIconButton } from '../UnstyledIconButton';
10
+ import CarouselContext from './Carousel.context';
11
+ import { CarouselControlsProps } from './Carousel.props';
12
+ import CarouselControlItem from './CarouselControlItem';
13
+
14
+ export const CarouselControls: FC<CarouselControlsProps> = ({
15
+ testID = 'pagination',
16
+ style,
17
+ itemStyle,
18
+ activeItemStyle,
19
+ showNavigation = false,
20
+ onPressPrev,
21
+ onPressNext,
22
+ accessibilityHidden,
23
+ ...props
24
+ }) => {
25
+ const context = useContext(CarouselContext);
26
+ const {
27
+ activeIndex = 0,
28
+ numItems = 0,
29
+ setActiveIndex,
30
+ controlsAccessibilityHidden,
31
+ disabled = false,
32
+ } = context;
33
+
34
+ const isAccessibilityHidden = accessibilityHidden ?? controlsAccessibilityHidden ?? true;
35
+
36
+ styles.useVariants({ showNavigation });
37
+
38
+ useEffect(() => {
39
+ if (!Object.keys(context).length) {
40
+ console.warn(
41
+ 'CarouselControls must be a child of Carousel. Pagination will not be displayed.'
42
+ );
43
+ }
44
+ }, [context]);
45
+
46
+ const keys = useMemo(() => {
47
+ return Array(numItems)
48
+ .fill(null)
49
+ .map(() => nanoid());
50
+ }, [numItems]);
51
+
52
+ const handlePrev = () => {
53
+ if (activeIndex > 0) {
54
+ setActiveIndex(activeIndex - 1);
55
+ onPressPrev?.();
56
+ }
57
+ };
58
+
59
+ const handleNext = () => {
60
+ if (activeIndex < numItems - 1) {
61
+ setActiveIndex(activeIndex + 1);
62
+ onPressNext?.();
63
+ }
64
+ };
65
+
66
+ const handleItemPress = (index: number) => {
67
+ setActiveIndex(index);
68
+ };
69
+
70
+ if (!Object.keys(context).length) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <View
76
+ style={[styles.root, style]}
77
+ testID={testID}
78
+ importantForAccessibility={isAccessibilityHidden ? 'no-hide-descendants' : 'auto'}
79
+ accessibilityElementsHidden={isAccessibilityHidden}
80
+ {...props}
81
+ >
82
+ {showNavigation && (
83
+ <UnstyledIconButton
84
+ icon={ChevronLeftSmallIcon}
85
+ onPress={handlePrev}
86
+ disabled={disabled || activeIndex === 0}
87
+ accessibilityLabel="Previous"
88
+ style={styles.button}
89
+ inverted={context.inverted}
90
+ />
91
+ )}
92
+ <View style={styles.dotsContainer}>
93
+ {keys.map((_, index) => (
94
+ <CarouselControlItem
95
+ active={index === activeIndex}
96
+ index={index}
97
+ key={keys[index]}
98
+ style={itemStyle}
99
+ activeStyle={activeItemStyle}
100
+ onPress={() => handleItemPress(index)}
101
+ disabled={disabled}
102
+ />
103
+ ))}
104
+ </View>
105
+ {showNavigation && (
106
+ <UnstyledIconButton
107
+ icon={ChevronRightSmallIcon}
108
+ onPress={handleNext}
109
+ disabled={disabled || activeIndex === numItems - 1}
110
+ accessibilityLabel="Next"
111
+ style={styles.button}
112
+ inverted={context.inverted}
113
+ />
114
+ )}
115
+ </View>
116
+ );
117
+ };
118
+
119
+ const styles = StyleSheet.create(theme => ({
120
+ root: {
121
+ width: '100%',
122
+ alignSelf: 'center',
123
+ flexDirection: 'row',
124
+ alignItems: 'center',
125
+ gap: theme.components.carouselControl.gap,
126
+ justifyContent: 'space-between',
127
+ variants: {
128
+ showNavigation: {
129
+ true: {
130
+ justifyContent: 'space-between',
131
+ },
132
+ false: {
133
+ justifyContent: 'center',
134
+ },
135
+ },
136
+ },
137
+ },
138
+ dotsContainer: {
139
+ flexDirection: 'row',
140
+ gap: theme.components.carouselControl.gap,
141
+ },
142
+ button: {
143
+ width: 24,
144
+ height: 24,
145
+ },
146
+ }));
147
+
148
+ CarouselControls.displayName = 'CarouselControls';
149
+
150
+ export default CarouselControls;