@windstream/react-shared-components 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +635 -635
- package/dist/contentful/index.d.ts +2 -2
- package/dist/contentful/index.esm.js +2 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +5 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +5 -7
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +1 -3
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -3
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +2 -3
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +185 -185
- package/src/components/accordion/Accordion.stories.tsx +230 -230
- package/src/components/accordion/index.tsx +70 -70
- package/src/components/accordion/types.ts +12 -12
- package/src/components/alert-card/AlertCard.stories.tsx +171 -171
- package/src/components/alert-card/index.tsx +41 -41
- package/src/components/alert-card/types.ts +13 -13
- package/src/components/brand-button/BrandButton.stories.tsx +223 -223
- package/src/components/brand-button/helpers.ts +35 -35
- package/src/components/brand-button/index.tsx +120 -120
- package/src/components/brand-button/types.ts +38 -38
- package/src/components/button/Button.stories.tsx +108 -108
- package/src/components/button/index.tsx +27 -27
- package/src/components/button/types.ts +14 -14
- package/src/components/call-button/CallButton.stories.tsx +324 -324
- package/src/components/call-button/index.tsx +86 -86
- package/src/components/call-button/types.ts +11 -11
- package/src/components/checkbox/Checkbox.stories.tsx +247 -247
- package/src/components/checkbox/index.tsx +197 -197
- package/src/components/checkbox/types.ts +27 -27
- package/src/components/checklist/Checklist.stories.tsx +150 -150
- package/src/components/checklist/index.tsx +61 -61
- package/src/components/checklist/types.ts +17 -17
- package/src/components/collapse/Collapse.stories.tsx +255 -255
- package/src/components/collapse/index.tsx +46 -46
- package/src/components/collapse/types.ts +6 -6
- package/src/components/divider/Divider.stories.tsx +205 -205
- package/src/components/divider/index.tsx +22 -22
- package/src/components/divider/type.ts +3 -3
- package/src/components/image/Image.stories.tsx +113 -113
- package/src/components/image/index.tsx +25 -25
- package/src/components/image/types.ts +40 -40
- package/src/components/input/Input.stories.tsx +325 -325
- package/src/components/input/index.tsx +177 -177
- package/src/components/input/types.ts +37 -37
- package/src/components/link/Link.stories.tsx +163 -163
- package/src/components/link/index.tsx +109 -109
- package/src/components/link/types.ts +25 -25
- package/src/components/list/List.stories.tsx +272 -272
- package/src/components/list/index.tsx +88 -88
- package/src/components/list/list-item/index.tsx +38 -38
- package/src/components/list/list-item/types.ts +13 -13
- package/src/components/list/types.ts +29 -29
- package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
- package/src/components/material-icon/constants.ts +98 -98
- package/src/components/material-icon/index.tsx +47 -47
- package/src/components/material-icon/types.ts +31 -31
- package/src/components/modal/Modal.stories.tsx +171 -171
- package/src/components/modal/index.tsx +164 -164
- package/src/components/modal/types.ts +24 -24
- package/src/components/next-image/index.tsx +54 -54
- package/src/components/next-image/types.ts +1 -1
- package/src/components/pagination/index.tsx +100 -100
- package/src/components/pagination/types.ts +6 -6
- package/src/components/radio-button/RadioButton.stories.tsx +307 -307
- package/src/components/radio-button/index.tsx +75 -75
- package/src/components/radio-button/types.ts +21 -21
- package/src/components/see-more/SeeMore.stories.tsx +181 -181
- package/src/components/see-more/index.tsx +44 -44
- package/src/components/see-more/types.ts +4 -4
- package/src/components/select/Select.stories.tsx +411 -411
- package/src/components/select/index.tsx +155 -155
- package/src/components/select/types.ts +36 -36
- package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
- package/src/components/select-plan-button/index.tsx +63 -63
- package/src/components/select-plan-button/types.ts +17 -17
- package/src/components/skeleton/Skeleton.stories.tsx +179 -179
- package/src/components/skeleton/index.tsx +61 -61
- package/src/components/skeleton/types.ts +4 -4
- package/src/components/spinner/Spinner.stories.tsx +335 -335
- package/src/components/spinner/index.tsx +44 -44
- package/src/components/spinner/types.ts +5 -5
- package/src/components/text/Text.stories.tsx +321 -321
- package/src/components/text/index.tsx +25 -25
- package/src/components/text/types.ts +45 -45
- package/src/components/tooltip/Tooltip.stories.tsx +219 -219
- package/src/components/tooltip/index.tsx +74 -74
- package/src/components/tooltip/types.ts +7 -7
- package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
- package/src/components/view-cart-button/index.tsx +42 -42
- package/src/components/view-cart-button/types.ts +5 -5
- package/src/contentful/blocks/accordion/Accordion.stories.tsx +34 -34
- package/src/contentful/blocks/accordion/index.tsx +112 -112
- package/src/contentful/blocks/accordion/types.ts +34 -34
- package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
- package/src/contentful/blocks/address-input-banner/types.ts +14 -14
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +70 -70
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +10 -10
- package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
- package/src/contentful/blocks/blogs-grid/types.ts +26 -26
- package/src/contentful/blocks/breadcrumbs/index.tsx +51 -51
- package/src/contentful/blocks/breadcrumbs/types.ts +5 -5
- package/src/contentful/blocks/button/Button.stories.tsx +40 -40
- package/src/contentful/blocks/button/index.tsx +130 -130
- package/src/contentful/blocks/button/types.ts +39 -39
- package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
- package/src/contentful/blocks/callout/index.tsx +88 -88
- package/src/contentful/blocks/callout/types.ts +15 -15
- package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
- package/src/contentful/blocks/cards/blog-card/index.tsx +110 -110
- package/src/contentful/blocks/cards/blog-card/types.ts +18 -18
- package/src/contentful/blocks/cards/index.tsx +13 -13
- package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
- package/src/contentful/blocks/cards/product-card/types.ts +28 -28
- package/src/contentful/blocks/cards/simple-card/index.tsx +89 -89
- package/src/contentful/blocks/cards/simple-card/types.ts +28 -28
- package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
- package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
- package/src/contentful/blocks/cards/types.ts +1 -1
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
- package/src/contentful/blocks/carousel/helper.tsx +440 -440
- package/src/contentful/blocks/carousel/index.tsx +85 -85
- package/src/contentful/blocks/carousel/types.ts +144 -144
- package/src/contentful/blocks/cartretentionbanner/CartRetentionBanner.tsx +99 -0
- package/src/contentful/blocks/cartretentionbanner/types.ts +11 -0
- package/src/contentful/blocks/comparison-table/index.tsx +27 -27
- package/src/contentful/blocks/comparison-table/types.ts +6 -6
- package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
- package/src/contentful/blocks/cookiebanner/type.ts +7 -7
- package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
- package/src/contentful/blocks/cta-callout/index.tsx +71 -71
- package/src/contentful/blocks/cta-callout/types.ts +26 -26
- package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
- package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
- package/src/contentful/blocks/email-input-block/index.tsx +117 -117
- package/src/contentful/blocks/email-input-block/types.ts +16 -16
- package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
- package/src/contentful/blocks/find-kinetic/index.tsx +130 -130
- package/src/contentful/blocks/find-kinetic/types.ts +19 -19
- package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
- package/src/contentful/blocks/floating-banner/index.tsx +97 -97
- package/src/contentful/blocks/floating-banner/types.ts +22 -22
- package/src/contentful/blocks/footer/Footer.stories.tsx +30 -30
- package/src/contentful/blocks/footer/index.tsx +91 -91
- package/src/contentful/blocks/footer/types.ts +13 -13
- package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
- package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
- package/src/contentful/blocks/image-promo-bar/index.tsx +246 -246
- package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
- package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
- package/src/contentful/blocks/modal/constants.ts +53 -53
- package/src/contentful/blocks/modal/index.tsx +107 -107
- package/src/contentful/blocks/modal/types.ts +12 -12
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +113 -113
- package/src/contentful/blocks/navigation/index.tsx +394 -394
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
- package/src/contentful/blocks/navigation/types.ts +41 -41
- package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
- package/src/contentful/blocks/primary-hero/index.tsx +236 -236
- package/src/contentful/blocks/primary-hero/types.ts +37 -37
- package/src/contentful/blocks/search-block/index.tsx +90 -90
- package/src/contentful/blocks/search-block/types.ts +15 -15
- package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
- package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
- package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
- package/src/contentful/blocks/text/Text.stories.tsx +23 -23
- package/src/contentful/blocks/text/index.tsx +12 -12
- package/src/contentful/blocks/text/types.ts +1 -1
- package/src/contentful/index.ts +99 -99
- package/src/hooks/contentful/use-contentful-rich-text.tsx +310 -310
- package/src/hooks/contentful/use-processed-check-list.ts +63 -63
- package/src/hooks/use-body-scroll-lock.ts +34 -34
- package/src/hooks/use-carousel-swipe.ts +264 -264
- package/src/hooks/use-outside-click.ts +17 -17
- package/src/index.ts +101 -101
- package/src/next/index.ts +5 -5
- package/src/setupTests.ts +46 -46
- package/src/stories/DocsTemplate.tsx +24 -24
- package/src/styles/globals.css +343 -343
- package/src/types/global.d.ts +9 -9
- package/src/types/micro-components.ts +99 -99
- package/src/types/utm.ts +49 -49
- package/src/utils/contentful/to-document.ts +24 -24
- package/src/utils/cookie.ts +84 -84
- package/src/utils/cx.ts +49 -49
- package/src/utils/index.ts +38 -38
- 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
|
+
};
|