@windstream/react-shared-components 0.1.75 → 0.1.77

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 (204) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.esm.js +3 -3
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/index.esm.js +5 -13
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +5 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/next/index.esm.js +2 -2
  11. package/dist/next/index.esm.js.map +1 -1
  12. package/dist/next/index.js +2 -2
  13. package/dist/next/index.js.map +1 -1
  14. package/dist/styles.css +1 -1
  15. package/dist/utils/index.esm.js +1 -1
  16. package/dist/utils/index.esm.js.map +1 -1
  17. package/dist/utils/index.js +1 -1
  18. package/dist/utils/index.js.map +1 -1
  19. package/package.json +185 -185
  20. package/src/components/accordion/Accordion.stories.tsx +230 -230
  21. package/src/components/accordion/index.tsx +70 -70
  22. package/src/components/accordion/types.ts +12 -12
  23. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  24. package/src/components/alert-card/index.tsx +41 -41
  25. package/src/components/alert-card/types.ts +13 -13
  26. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  27. package/src/components/brand-button/helpers.ts +35 -35
  28. package/src/components/brand-button/index.tsx +120 -120
  29. package/src/components/brand-button/types.ts +38 -38
  30. package/src/components/button/Button.stories.tsx +108 -108
  31. package/src/components/button/index.tsx +27 -27
  32. package/src/components/button/types.ts +14 -14
  33. package/src/components/call-button/CallButton.stories.tsx +324 -324
  34. package/src/components/call-button/index.tsx +106 -106
  35. package/src/components/call-button/types.ts +16 -16
  36. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  37. package/src/components/checkbox/index.tsx +197 -197
  38. package/src/components/checkbox/types.ts +27 -27
  39. package/src/components/checklist/Checklist.stories.tsx +150 -150
  40. package/src/components/checklist/index.tsx +61 -61
  41. package/src/components/checklist/types.ts +17 -17
  42. package/src/components/collapse/Collapse.stories.tsx +255 -255
  43. package/src/components/collapse/index.tsx +46 -46
  44. package/src/components/collapse/types.ts +6 -6
  45. package/src/components/divider/Divider.stories.tsx +205 -205
  46. package/src/components/divider/index.tsx +22 -22
  47. package/src/components/divider/type.ts +3 -3
  48. package/src/components/image/Image.stories.tsx +113 -113
  49. package/src/components/image/index.tsx +25 -25
  50. package/src/components/image/types.ts +40 -40
  51. package/src/components/input/Input.stories.tsx +325 -325
  52. package/src/components/input/index.tsx +177 -177
  53. package/src/components/input/types.ts +37 -37
  54. package/src/components/link/Link.stories.tsx +163 -163
  55. package/src/components/link/index.tsx +116 -116
  56. package/src/components/link/types.ts +25 -25
  57. package/src/components/list/List.stories.tsx +272 -272
  58. package/src/components/list/index.tsx +88 -88
  59. package/src/components/list/list-item/index.tsx +38 -38
  60. package/src/components/list/list-item/types.ts +13 -13
  61. package/src/components/list/types.ts +29 -29
  62. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  63. package/src/components/material-icon/constants.ts +99 -99
  64. package/src/components/material-icon/index.tsx +47 -47
  65. package/src/components/material-icon/types.ts +31 -31
  66. package/src/components/modal/Modal.stories.tsx +171 -171
  67. package/src/components/modal/index.tsx +164 -164
  68. package/src/components/modal/types.ts +24 -24
  69. package/src/components/next-image/index.tsx +74 -74
  70. package/src/components/next-image/types.ts +1 -1
  71. package/src/components/pagination/index.tsx +91 -91
  72. package/src/components/pagination/types.ts +6 -6
  73. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  74. package/src/components/radio-button/index.tsx +75 -75
  75. package/src/components/radio-button/types.ts +21 -21
  76. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  77. package/src/components/see-more/index.tsx +44 -44
  78. package/src/components/see-more/types.ts +4 -4
  79. package/src/components/select/Select.stories.tsx +411 -411
  80. package/src/components/select/index.tsx +155 -155
  81. package/src/components/select/types.ts +36 -36
  82. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  83. package/src/components/select-plan-button/index.tsx +63 -63
  84. package/src/components/select-plan-button/types.ts +17 -17
  85. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  86. package/src/components/skeleton/index.tsx +61 -61
  87. package/src/components/skeleton/types.ts +4 -4
  88. package/src/components/spinner/Spinner.stories.tsx +335 -335
  89. package/src/components/spinner/index.tsx +44 -44
  90. package/src/components/spinner/types.ts +5 -5
  91. package/src/components/text/Text.stories.tsx +321 -321
  92. package/src/components/text/index.tsx +25 -25
  93. package/src/components/text/types.ts +45 -45
  94. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  95. package/src/components/tooltip/index.tsx +74 -74
  96. package/src/components/tooltip/types.ts +7 -7
  97. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  98. package/src/components/view-cart-button/index.tsx +42 -42
  99. package/src/components/view-cart-button/types.ts +5 -5
  100. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  101. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  102. package/src/contentful/blocks/accordion/index.tsx +112 -112
  103. package/src/contentful/blocks/accordion/types.ts +34 -34
  104. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  105. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  106. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  107. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  108. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  109. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  110. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  111. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  112. package/src/contentful/blocks/breadcrumbs/index.tsx +81 -81
  113. package/src/contentful/blocks/breadcrumbs/types.ts +6 -6
  114. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  115. package/src/contentful/blocks/button/index.tsx +131 -131
  116. package/src/contentful/blocks/button/types.ts +39 -39
  117. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  118. package/src/contentful/blocks/callout/index.tsx +252 -252
  119. package/src/contentful/blocks/callout/types.ts +68 -68
  120. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  121. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  122. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  123. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  124. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  125. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  126. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  127. package/src/contentful/blocks/cards/index.tsx +13 -13
  128. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  129. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  130. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  131. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  132. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  133. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  134. package/src/contentful/blocks/cards/types.ts +1 -1
  135. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  136. package/src/contentful/blocks/carousel/helper.tsx +440 -440
  137. package/src/contentful/blocks/carousel/index.tsx +85 -85
  138. package/src/contentful/blocks/carousel/types.ts +144 -144
  139. package/src/contentful/blocks/cart-retention-banner/index.tsx +105 -105
  140. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  141. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  142. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  143. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  144. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  145. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  146. package/src/contentful/blocks/cta-callout/index.tsx +71 -71
  147. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  148. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  149. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  150. package/src/contentful/blocks/email-input-block/index.tsx +116 -116
  151. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  152. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  153. package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
  154. package/src/contentful/blocks/find-kinetic/types.ts +19 -19
  155. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  156. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  157. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  158. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  159. package/src/contentful/blocks/footer/index.tsx +91 -91
  160. package/src/contentful/blocks/footer/types.ts +13 -13
  161. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  162. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  163. package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
  164. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  165. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  166. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  167. package/src/contentful/blocks/modal/constants.ts +53 -53
  168. package/src/contentful/blocks/modal/index.tsx +107 -107
  169. package/src/contentful/blocks/modal/types.ts +12 -12
  170. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +139 -139
  171. package/src/contentful/blocks/navigation/index.tsx +567 -512
  172. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  173. package/src/contentful/blocks/navigation/types.ts +71 -71
  174. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  175. package/src/contentful/blocks/primary-hero/index.tsx +236 -236
  176. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  177. package/src/contentful/blocks/search-block/index.tsx +90 -90
  178. package/src/contentful/blocks/search-block/types.ts +15 -15
  179. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  180. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  181. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  182. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  183. package/src/contentful/blocks/text/index.tsx +12 -12
  184. package/src/contentful/blocks/text/types.ts +1 -1
  185. package/src/contentful/index.ts +105 -105
  186. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  187. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  188. package/src/hooks/use-body-scroll-lock.ts +34 -34
  189. package/src/hooks/use-carousel-swipe.ts +264 -264
  190. package/src/hooks/use-outside-click.ts +17 -17
  191. package/src/index.ts +101 -101
  192. package/src/next/index.ts +5 -5
  193. package/src/setupTests.ts +46 -46
  194. package/src/stories/DocsTemplate.tsx +24 -24
  195. package/src/styles/globals.css +343 -343
  196. package/src/types/global.d.ts +9 -9
  197. package/src/types/micro-components.ts +99 -99
  198. package/src/types/utm.ts +49 -49
  199. package/src/utils/contentful/to-document.ts +24 -24
  200. package/src/utils/cookie.ts +84 -84
  201. package/src/utils/cx.ts +49 -49
  202. package/src/utils/index.ts +41 -41
  203. package/src/utils/speed-card-bg.ts +24 -24
  204. package/src/utils/utm.ts +221 -221
@@ -1,264 +1,264 @@
1
- import React, {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from "react";
8
-
9
- /**
10
- * Configuration options for the carousel swipe behavior
11
- */
12
- export interface CarouselSwipeConfig {
13
- /** Total number of items in the carousel */
14
- itemCount: number;
15
- /** Percentage of card width to offset each slide (default: 105) */
16
- cardOffsetPercentage?: number;
17
- /** Percentage of container width needed to trigger slide change (default: 0.15) */
18
- swipeThreshold?: number;
19
- /** Mobile breakpoint in pixels (default: 768) */
20
- mobileBreakpoint?: number;
21
- /** Auto-scroll interval in milliseconds (default: 8000). Set to 0 to disable. */
22
- autoScrollInterval?: number;
23
- /** Enable auto-scroll (default: true) */
24
- enableAutoScroll?: boolean;
25
- }
26
-
27
- /**
28
- * Return value from the useCarouselSwipe hook
29
- */
30
- export interface CarouselSwipeReturn {
31
- /** Current active slide index */
32
- currentIndex: number;
33
- /** Current swipe offset in pixels */
34
- swipeOffset: number;
35
- /** Whether user is currently swiping */
36
- isSwiping: boolean;
37
- /** Whether viewport is mobile size */
38
- isMobile: boolean;
39
- /** Memoized container width */
40
- containerWidth: number;
41
- /** Ref to attach to the carousel container element */
42
- containerRef: React.RefObject<HTMLDivElement>;
43
- /** Navigate to next slide */
44
- nextSlide: () => void;
45
- /** Navigate to previous slide */
46
- prevSlide: () => void;
47
- /** Navigate to specific slide index */
48
- goToSlide: (index: number) => void;
49
- /** Touch start handler */
50
- handleTouchStart: (e: React.TouchEvent) => void;
51
- /** Touch move handler */
52
- handleTouchMove: (e: React.TouchEvent) => void;
53
- /** Touch end handler */
54
- handleTouchEnd: () => void;
55
- /** Constants used for calculations */
56
- constants: {
57
- CARD_OFFSET_PERCENTAGE: number;
58
- SWIPE_THRESHOLD: number;
59
- MOBILE_BREAKPOINT: number;
60
- AUTO_SCROLL_INTERVAL: number;
61
- };
62
- }
63
-
64
- /**
65
- * Custom hook for implementing swipe/touch gestures in carousels
66
- *
67
- * Features:
68
- * - Touch/swipe support with smooth finger-following
69
- * - Auto-scroll with pause on interaction
70
- * - Responsive mobile detection with resize listener
71
- * - Performance optimized with memoization
72
- * - Configurable thresholds and behavior
73
- *
74
- * @example
75
- * ```tsx
76
- * const carousel = useCarouselSwipe({
77
- * itemCount: items.length,
78
- * autoScrollInterval: 5000,
79
- * });
80
- *
81
- * return (
82
- * <div
83
- * ref={carousel.containerRef}
84
- * onTouchStart={carousel.handleTouchStart}
85
- * onTouchMove={carousel.handleTouchMove}
86
- * onTouchEnd={carousel.handleTouchEnd}
87
- * >
88
- * {items.map((item, index) => (
89
- * <div
90
- * key={index}
91
- * style={{
92
- * transform: `translateX(${calculateTransform(index, carousel)})`,
93
- * }}
94
- * >
95
- * {item}
96
- * </div>
97
- * ))}
98
- * </div>
99
- * );
100
- * ```
101
- */
102
- export function useCarouselSwipe(
103
- config: CarouselSwipeConfig
104
- ): CarouselSwipeReturn {
105
- const {
106
- itemCount,
107
- cardOffsetPercentage = 105,
108
- swipeThreshold = 0.15,
109
- mobileBreakpoint = 768,
110
- autoScrollInterval = 8000,
111
- enableAutoScroll = true,
112
- } = config;
113
-
114
- // State
115
- const [currentIndex, setCurrentIndex] = useState(0);
116
- const [swipeOffset, setSwipeOffset] = useState(0);
117
- const [isSwiping, setIsSwiping] = useState(false);
118
- const [containerWidth, setContainerWidth] = useState(window.innerWidth);
119
- // Performance: Store mobile state to avoid repeated window.innerWidth checks on every render
120
- // This prevents expensive DOM queries during swipe operations
121
- const [isMobile, setIsMobile] = useState(false);
122
-
123
- // Refs
124
- const timeoutRef = useRef<ReturnType<typeof setInterval> | null>(null);
125
- const touchStartX = useRef<number>(0);
126
- const containerRef = useRef<HTMLDivElement>(null);
127
-
128
- // Constants
129
- const constants = {
130
- CARD_OFFSET_PERCENTAGE: cardOffsetPercentage,
131
- SWIPE_THRESHOLD: swipeThreshold,
132
- MOBILE_BREAKPOINT: mobileBreakpoint,
133
- AUTO_SCROLL_INTERVAL: autoScrollInterval,
134
- };
135
-
136
- // Performance: Memoize container width to prevent recalculation on every render
137
- // This is especially important during swipe operations where the component re-renders
138
- // frequently. Without memoization, it'd query the DOM on every frame while swiping.
139
- useEffect(() => {
140
- const updateWidth = () => {
141
- setContainerWidth(containerRef.current?.offsetWidth || window.innerWidth);
142
- };
143
- updateWidth(); // Initial
144
- window.addEventListener("resize", updateWidth);
145
- return () => window.removeEventListener("resize", updateWidth);
146
- }, []);
147
-
148
- // Navigation functions
149
- const nextSlide = useCallback(() => {
150
- if (itemCount === 0) return;
151
- setCurrentIndex(prev => (prev + 1) % itemCount);
152
- }, [itemCount]);
153
-
154
- const prevSlide = useCallback(() => {
155
- if (itemCount === 0) return;
156
- setCurrentIndex(prev => (prev === 0 ? itemCount - 1 : prev - 1));
157
- }, [itemCount]);
158
-
159
- const goToSlide = useCallback(
160
- (index: number) => {
161
- if (index < 0 || index >= itemCount) return;
162
- setCurrentIndex(index);
163
- },
164
- [itemCount]
165
- );
166
-
167
- // Touch handlers for mobile swipe
168
- const handleTouchStart = useCallback((e: React.TouchEvent) => {
169
- touchStartX.current = e.touches[0].clientX;
170
- setIsSwiping(true);
171
- // Pause auto-scroll during user interaction
172
- if (timeoutRef.current) {
173
- clearInterval(timeoutRef.current);
174
- }
175
- }, []);
176
-
177
- const handleTouchMove = useCallback(
178
- (e: React.TouchEvent) => {
179
- if (!isSwiping) return;
180
- const currentX = e.touches[0].clientX;
181
- const diff = currentX - touchStartX.current;
182
- setSwipeOffset(diff);
183
- },
184
- [isSwiping]
185
- );
186
-
187
- const handleTouchEnd = useCallback(() => {
188
- setIsSwiping(false);
189
- // Use memoized containerWidth and constant threshold for performance
190
- const threshold = containerWidth * swipeThreshold;
191
-
192
- // Determine if swipe was strong enough to change slides
193
- if (swipeOffset > threshold) {
194
- prevSlide();
195
- } else if (swipeOffset < -threshold) {
196
- nextSlide();
197
- }
198
-
199
- // Reset swipe offset to return card to snapped position
200
- setSwipeOffset(0);
201
-
202
- // Restart auto-scroll after user interaction completes
203
- if (enableAutoScroll && autoScrollInterval > 0) {
204
- timeoutRef.current = setInterval(() => {
205
- nextSlide();
206
- }, autoScrollInterval);
207
- }
208
- }, [
209
- swipeOffset,
210
- containerWidth,
211
- swipeThreshold,
212
- prevSlide,
213
- nextSlide,
214
- enableAutoScroll,
215
- autoScrollInterval,
216
- ]);
217
-
218
- // Performance: Detect mobile viewport and update on resize
219
- // This replaces inline window.innerWidth checks that were running on every render.
220
- // By using state and a resize listener, we only check when the viewport actually changes.
221
- useEffect(() => {
222
- const checkMobile = () => {
223
- setIsMobile(window.innerWidth < mobileBreakpoint);
224
- };
225
-
226
- // Check immediately on mount
227
- checkMobile();
228
-
229
- // Update when window is resized (e.g., device rotation, browser resize)
230
- window.addEventListener("resize", checkMobile);
231
- return () => window.removeEventListener("resize", checkMobile);
232
- }, [mobileBreakpoint]);
233
-
234
- // Auto-scroll logic
235
- useEffect(() => {
236
- if (!enableAutoScroll || itemCount === 0 || autoScrollInterval === 0) {
237
- return;
238
- }
239
-
240
- timeoutRef.current = setInterval(() => {
241
- nextSlide();
242
- }, autoScrollInterval);
243
-
244
- return () => {
245
- if (timeoutRef.current) clearInterval(timeoutRef.current);
246
- };
247
- }, [nextSlide, itemCount, enableAutoScroll, autoScrollInterval]);
248
-
249
- return {
250
- currentIndex,
251
- swipeOffset,
252
- isSwiping,
253
- isMobile,
254
- containerWidth,
255
- containerRef,
256
- nextSlide,
257
- prevSlide,
258
- goToSlide,
259
- handleTouchStart,
260
- handleTouchMove,
261
- handleTouchEnd,
262
- constants,
263
- };
264
- }
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+
9
+ /**
10
+ * Configuration options for the carousel swipe behavior
11
+ */
12
+ export interface CarouselSwipeConfig {
13
+ /** Total number of items in the carousel */
14
+ itemCount: number;
15
+ /** Percentage of card width to offset each slide (default: 105) */
16
+ cardOffsetPercentage?: number;
17
+ /** Percentage of container width needed to trigger slide change (default: 0.15) */
18
+ swipeThreshold?: number;
19
+ /** Mobile breakpoint in pixels (default: 768) */
20
+ mobileBreakpoint?: number;
21
+ /** Auto-scroll interval in milliseconds (default: 8000). Set to 0 to disable. */
22
+ autoScrollInterval?: number;
23
+ /** Enable auto-scroll (default: true) */
24
+ enableAutoScroll?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Return value from the useCarouselSwipe hook
29
+ */
30
+ export interface CarouselSwipeReturn {
31
+ /** Current active slide index */
32
+ currentIndex: number;
33
+ /** Current swipe offset in pixels */
34
+ swipeOffset: number;
35
+ /** Whether user is currently swiping */
36
+ isSwiping: boolean;
37
+ /** Whether viewport is mobile size */
38
+ isMobile: boolean;
39
+ /** Memoized container width */
40
+ containerWidth: number;
41
+ /** Ref to attach to the carousel container element */
42
+ containerRef: React.RefObject<HTMLDivElement>;
43
+ /** Navigate to next slide */
44
+ nextSlide: () => void;
45
+ /** Navigate to previous slide */
46
+ prevSlide: () => void;
47
+ /** Navigate to specific slide index */
48
+ goToSlide: (index: number) => void;
49
+ /** Touch start handler */
50
+ handleTouchStart: (e: React.TouchEvent) => void;
51
+ /** Touch move handler */
52
+ handleTouchMove: (e: React.TouchEvent) => void;
53
+ /** Touch end handler */
54
+ handleTouchEnd: () => void;
55
+ /** Constants used for calculations */
56
+ constants: {
57
+ CARD_OFFSET_PERCENTAGE: number;
58
+ SWIPE_THRESHOLD: number;
59
+ MOBILE_BREAKPOINT: number;
60
+ AUTO_SCROLL_INTERVAL: number;
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Custom hook for implementing swipe/touch gestures in carousels
66
+ *
67
+ * Features:
68
+ * - Touch/swipe support with smooth finger-following
69
+ * - Auto-scroll with pause on interaction
70
+ * - Responsive mobile detection with resize listener
71
+ * - Performance optimized with memoization
72
+ * - Configurable thresholds and behavior
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * const carousel = useCarouselSwipe({
77
+ * itemCount: items.length,
78
+ * autoScrollInterval: 5000,
79
+ * });
80
+ *
81
+ * return (
82
+ * <div
83
+ * ref={carousel.containerRef}
84
+ * onTouchStart={carousel.handleTouchStart}
85
+ * onTouchMove={carousel.handleTouchMove}
86
+ * onTouchEnd={carousel.handleTouchEnd}
87
+ * >
88
+ * {items.map((item, index) => (
89
+ * <div
90
+ * key={index}
91
+ * style={{
92
+ * transform: `translateX(${calculateTransform(index, carousel)})`,
93
+ * }}
94
+ * >
95
+ * {item}
96
+ * </div>
97
+ * ))}
98
+ * </div>
99
+ * );
100
+ * ```
101
+ */
102
+ export function useCarouselSwipe(
103
+ config: CarouselSwipeConfig
104
+ ): CarouselSwipeReturn {
105
+ const {
106
+ itemCount,
107
+ cardOffsetPercentage = 105,
108
+ swipeThreshold = 0.15,
109
+ mobileBreakpoint = 768,
110
+ autoScrollInterval = 8000,
111
+ enableAutoScroll = true,
112
+ } = config;
113
+
114
+ // State
115
+ const [currentIndex, setCurrentIndex] = useState(0);
116
+ const [swipeOffset, setSwipeOffset] = useState(0);
117
+ const [isSwiping, setIsSwiping] = useState(false);
118
+ const [containerWidth, setContainerWidth] = useState(window.innerWidth);
119
+ // Performance: Store mobile state to avoid repeated window.innerWidth checks on every render
120
+ // This prevents expensive DOM queries during swipe operations
121
+ const [isMobile, setIsMobile] = useState(false);
122
+
123
+ // Refs
124
+ const timeoutRef = useRef<ReturnType<typeof setInterval> | null>(null);
125
+ const touchStartX = useRef<number>(0);
126
+ const containerRef = useRef<HTMLDivElement>(null);
127
+
128
+ // Constants
129
+ const constants = {
130
+ CARD_OFFSET_PERCENTAGE: cardOffsetPercentage,
131
+ SWIPE_THRESHOLD: swipeThreshold,
132
+ MOBILE_BREAKPOINT: mobileBreakpoint,
133
+ AUTO_SCROLL_INTERVAL: autoScrollInterval,
134
+ };
135
+
136
+ // Performance: Memoize container width to prevent recalculation on every render
137
+ // This is especially important during swipe operations where the component re-renders
138
+ // frequently. Without memoization, it'd query the DOM on every frame while swiping.
139
+ useEffect(() => {
140
+ const updateWidth = () => {
141
+ setContainerWidth(containerRef.current?.offsetWidth || window.innerWidth);
142
+ };
143
+ updateWidth(); // Initial
144
+ window.addEventListener("resize", updateWidth);
145
+ return () => window.removeEventListener("resize", updateWidth);
146
+ }, []);
147
+
148
+ // Navigation functions
149
+ const nextSlide = useCallback(() => {
150
+ if (itemCount === 0) return;
151
+ setCurrentIndex(prev => (prev + 1) % itemCount);
152
+ }, [itemCount]);
153
+
154
+ const prevSlide = useCallback(() => {
155
+ if (itemCount === 0) return;
156
+ setCurrentIndex(prev => (prev === 0 ? itemCount - 1 : prev - 1));
157
+ }, [itemCount]);
158
+
159
+ const goToSlide = useCallback(
160
+ (index: number) => {
161
+ if (index < 0 || index >= itemCount) return;
162
+ setCurrentIndex(index);
163
+ },
164
+ [itemCount]
165
+ );
166
+
167
+ // Touch handlers for mobile swipe
168
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
169
+ touchStartX.current = e.touches[0].clientX;
170
+ setIsSwiping(true);
171
+ // Pause auto-scroll during user interaction
172
+ if (timeoutRef.current) {
173
+ clearInterval(timeoutRef.current);
174
+ }
175
+ }, []);
176
+
177
+ const handleTouchMove = useCallback(
178
+ (e: React.TouchEvent) => {
179
+ if (!isSwiping) return;
180
+ const currentX = e.touches[0].clientX;
181
+ const diff = currentX - touchStartX.current;
182
+ setSwipeOffset(diff);
183
+ },
184
+ [isSwiping]
185
+ );
186
+
187
+ const handleTouchEnd = useCallback(() => {
188
+ setIsSwiping(false);
189
+ // Use memoized containerWidth and constant threshold for performance
190
+ const threshold = containerWidth * swipeThreshold;
191
+
192
+ // Determine if swipe was strong enough to change slides
193
+ if (swipeOffset > threshold) {
194
+ prevSlide();
195
+ } else if (swipeOffset < -threshold) {
196
+ nextSlide();
197
+ }
198
+
199
+ // Reset swipe offset to return card to snapped position
200
+ setSwipeOffset(0);
201
+
202
+ // Restart auto-scroll after user interaction completes
203
+ if (enableAutoScroll && autoScrollInterval > 0) {
204
+ timeoutRef.current = setInterval(() => {
205
+ nextSlide();
206
+ }, autoScrollInterval);
207
+ }
208
+ }, [
209
+ swipeOffset,
210
+ containerWidth,
211
+ swipeThreshold,
212
+ prevSlide,
213
+ nextSlide,
214
+ enableAutoScroll,
215
+ autoScrollInterval,
216
+ ]);
217
+
218
+ // Performance: Detect mobile viewport and update on resize
219
+ // This replaces inline window.innerWidth checks that were running on every render.
220
+ // By using state and a resize listener, we only check when the viewport actually changes.
221
+ useEffect(() => {
222
+ const checkMobile = () => {
223
+ setIsMobile(window.innerWidth < mobileBreakpoint);
224
+ };
225
+
226
+ // Check immediately on mount
227
+ checkMobile();
228
+
229
+ // Update when window is resized (e.g., device rotation, browser resize)
230
+ window.addEventListener("resize", checkMobile);
231
+ return () => window.removeEventListener("resize", checkMobile);
232
+ }, [mobileBreakpoint]);
233
+
234
+ // Auto-scroll logic
235
+ useEffect(() => {
236
+ if (!enableAutoScroll || itemCount === 0 || autoScrollInterval === 0) {
237
+ return;
238
+ }
239
+
240
+ timeoutRef.current = setInterval(() => {
241
+ nextSlide();
242
+ }, autoScrollInterval);
243
+
244
+ return () => {
245
+ if (timeoutRef.current) clearInterval(timeoutRef.current);
246
+ };
247
+ }, [nextSlide, itemCount, enableAutoScroll, autoScrollInterval]);
248
+
249
+ return {
250
+ currentIndex,
251
+ swipeOffset,
252
+ isSwiping,
253
+ isMobile,
254
+ containerWidth,
255
+ containerRef,
256
+ nextSlide,
257
+ prevSlide,
258
+ goToSlide,
259
+ handleTouchStart,
260
+ handleTouchMove,
261
+ handleTouchEnd,
262
+ constants,
263
+ };
264
+ }
@@ -1,17 +1,17 @@
1
- import { RefObject, useEffect } from "react";
2
-
3
- export const useOutsideClick = (ref: RefObject<any>, callback: () => void) => {
4
- const handleClick = (e: MouseEvent) => {
5
- if (!ref?.current?.contains(e.target)) {
6
- callback();
7
- }
8
- };
9
-
10
- useEffect(() => {
11
- document.addEventListener("click", handleClick);
12
-
13
- return () => {
14
- document.removeEventListener("click", handleClick);
15
- };
16
- });
17
- };
1
+ import { RefObject, useEffect } from "react";
2
+
3
+ export const useOutsideClick = (ref: RefObject<any>, callback: () => void) => {
4
+ const handleClick = (e: MouseEvent) => {
5
+ if (!ref?.current?.contains(e.target)) {
6
+ callback();
7
+ }
8
+ };
9
+
10
+ useEffect(() => {
11
+ document.addEventListener("click", handleClick);
12
+
13
+ return () => {
14
+ document.removeEventListener("click", handleClick);
15
+ };
16
+ });
17
+ };