@windstream/react-shared-components 0.1.92 → 0.1.94
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 +1 -0
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +191 -185
- package/src/components/accordion/Accordion.stories.tsx +230 -230
- package/src/components/accordion/index.test.tsx +270 -0
- 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.test.tsx +152 -0
- package/src/components/alert-card/index.tsx +41 -41
- package/src/components/alert-card/types.ts +13 -13
- package/src/components/animation-wrapper/index.test.tsx +424 -0
- package/src/components/animation-wrapper/index.tsx +129 -129
- package/src/components/animation-wrapper/types.ts +11 -11
- 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.test.tsx +292 -0
- 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.test.tsx +91 -0
- 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.test.tsx +260 -0
- package/src/components/call-button/index.tsx +106 -106
- package/src/components/call-button/types.ts +16 -16
- package/src/components/checkbox/Checkbox.stories.tsx +247 -247
- package/src/components/checkbox/index.test.tsx +252 -0
- 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.test.tsx +231 -0
- package/src/components/checklist/index.tsx +96 -61
- package/src/components/checklist/types.ts +23 -17
- package/src/components/collapse/Collapse.stories.tsx +255 -255
- package/src/components/collapse/index.test.tsx +277 -0
- package/src/components/collapse/index.tsx +47 -46
- package/src/components/collapse/types.ts +6 -6
- package/src/components/divider/Divider.stories.tsx +205 -205
- package/src/components/divider/index.test.tsx +53 -0
- 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.test.tsx +174 -0
- 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.test.tsx +348 -0
- 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.test.tsx +199 -0
- package/src/components/link/index.tsx +116 -116
- package/src/components/link/types.ts +25 -25
- package/src/components/list/List.stories.tsx +272 -272
- package/src/components/list/index.test.tsx +166 -0
- 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 +99 -99
- package/src/components/material-icon/index.test.tsx +130 -0
- 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.test.tsx +310 -0
- package/src/components/modal/index.tsx +164 -164
- package/src/components/modal/types.ts +24 -24
- package/src/components/next-image/index.test.tsx +406 -0
- package/src/components/next-image/index.tsx +74 -74
- package/src/components/next-image/types.ts +1 -1
- package/src/components/pagination/index.test.tsx +521 -0
- package/src/components/pagination/index.tsx +91 -91
- package/src/components/pagination/types.ts +6 -6
- package/src/components/radio-button/RadioButton.stories.tsx +307 -307
- package/src/components/radio-button/index.test.tsx +151 -0
- 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.test.tsx +96 -0
- 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.test.tsx +256 -0
- 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.test.tsx +173 -0
- 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.test.tsx +74 -0
- 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.test.tsx +76 -0
- 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.test.tsx +65 -0
- 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.test.tsx +50 -0
- 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.test.tsx +57 -0
- 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.mocks.tsx +128 -128
- package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
- package/src/contentful/blocks/accordion/index.test.tsx +218 -0
- package/src/contentful/blocks/accordion/index.tsx +114 -112
- package/src/contentful/blocks/accordion/types.ts +34 -34
- package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
- 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.test.tsx +287 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -156
- package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
- package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
- package/src/contentful/blocks/blogs-grid/types.ts +26 -26
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
- package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
- package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
- package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
- package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
- package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
- package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
- package/src/contentful/blocks/button/Button.stories.tsx +40 -40
- package/src/contentful/blocks/button/index.test.tsx +339 -0
- package/src/contentful/blocks/button/index.tsx +131 -131
- 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.test.tsx +539 -0
- package/src/contentful/blocks/callout/index.tsx +277 -277
- package/src/contentful/blocks/callout/types.ts +78 -78
- package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
- package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
- package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
- package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
- package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
- package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
- package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
- package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
- package/src/contentful/blocks/cards/index.test.tsx +39 -0
- package/src/contentful/blocks/cards/index.tsx +13 -13
- package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
- 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.test.tsx +364 -0
- package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
- package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
- package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
- 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.test.tsx +539 -0
- package/src/contentful/blocks/carousel/helper.tsx +494 -494
- package/src/contentful/blocks/carousel/index.test.tsx +308 -0
- package/src/contentful/blocks/carousel/index.tsx +87 -87
- package/src/contentful/blocks/carousel/types.test.ts +16 -0
- package/src/contentful/blocks/carousel/types.ts +145 -145
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
- package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
- package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
- package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
- package/src/contentful/blocks/comparison-table/index.tsx +29 -29
- package/src/contentful/blocks/comparison-table/types.ts +6 -6
- package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
- 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.test.tsx +244 -0
- package/src/contentful/blocks/cta-callout/index.tsx +73 -73
- package/src/contentful/blocks/cta-callout/types.ts +26 -26
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
- 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.test.tsx +213 -0
- package/src/contentful/blocks/email-input-block/index.tsx +121 -116
- 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.test.tsx +269 -0
- package/src/contentful/blocks/find-kinetic/index.tsx +138 -130
- package/src/contentful/blocks/find-kinetic/types.ts +20 -19
- package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
- package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
- 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 +317 -317
- package/src/contentful/blocks/footer/index.test.tsx +302 -0
- 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.test.tsx +61 -0
- package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
- 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.test.tsx +142 -0
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
- package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
- 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.test.tsx +209 -0
- package/src/contentful/blocks/modal/index.tsx +108 -108
- package/src/contentful/blocks/modal/types.ts +12 -12
- package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -0
- package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -0
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -139
- package/src/contentful/blocks/navigation/index.test.tsx +924 -0
- package/src/contentful/blocks/navigation/index.tsx +569 -568
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
- package/src/contentful/blocks/navigation/types.ts +71 -71
- package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
- package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
- package/src/contentful/blocks/primary-hero/index.tsx +239 -236
- package/src/contentful/blocks/primary-hero/types.ts +37 -37
- package/src/contentful/blocks/search-block/index.test.tsx +268 -0
- 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.test.tsx +284 -0
- 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.test.tsx +36 -0
- package/src/contentful/blocks/text/index.tsx +12 -12
- package/src/contentful/blocks/text/types.ts +1 -1
- package/src/contentful/index.test.ts +45 -0
- package/src/contentful/index.ts +105 -105
- package/src/global-mocks/contentful/to-document.ts +25 -0
- package/src/global-mocks/cookie.ts +48 -0
- package/src/global-mocks/cx.ts +37 -0
- package/src/global-mocks/index.ts +89 -0
- package/src/global-mocks/speed-card-bg.ts +27 -0
- package/src/global-mocks/utm.ts +49 -0
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
- package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
- package/src/hooks/contentful/use-processed-check-list.ts +63 -63
- package/src/hooks/use-body-scroll-lock.test.ts +134 -0
- package/src/hooks/use-body-scroll-lock.ts +34 -34
- package/src/hooks/use-carousel-swipe.test.ts +393 -0
- package/src/hooks/use-carousel-swipe.ts +264 -264
- package/src/hooks/use-outside-click.test.ts +142 -0
- package/src/hooks/use-outside-click.ts +17 -17
- package/src/index.ts +107 -107
- package/src/next/index.test.ts +7 -0
- package/src/next/index.ts +5 -5
- package/src/setupTests.ts +52 -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.test.ts +85 -0
- package/src/utils/contentful/to-document.ts +24 -24
- package/src/utils/cookie.test.ts +180 -0
- package/src/utils/cookie.ts +84 -84
- package/src/utils/cx.test.ts +90 -0
- package/src/utils/cx.ts +49 -49
- package/src/utils/index.test.ts +115 -0
- package/src/utils/index.ts +41 -41
- package/src/utils/speed-card-bg.test.ts +46 -0
- package/src/utils/speed-card-bg.ts +24 -24
- package/src/utils/utm.test.ts +359 -0
- 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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useOutsideClick } from "./use-outside-click";
|
|
3
|
+
|
|
4
|
+
import { renderHook } from "@testing-library/react";
|
|
5
|
+
|
|
6
|
+
describe("useOutsideClick", () => {
|
|
7
|
+
let callback: jest.Mock;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
callback = jest.fn();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("Event listener lifecycle", () => {
|
|
14
|
+
it("adds click event listener on mount", () => {
|
|
15
|
+
const addSpy = jest.spyOn(document, "addEventListener");
|
|
16
|
+
const ref = { current: document.createElement("div") };
|
|
17
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
18
|
+
const clickCalls = addSpy.mock.calls.filter(c => c[0] === "click");
|
|
19
|
+
expect(clickCalls.length).toBeGreaterThanOrEqual(1);
|
|
20
|
+
addSpy.mockRestore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("removes click event listener on unmount", () => {
|
|
24
|
+
const removeSpy = jest.spyOn(document, "removeEventListener");
|
|
25
|
+
const ref = { current: document.createElement("div") };
|
|
26
|
+
const { unmount } = renderHook(() => useOutsideClick(ref, callback));
|
|
27
|
+
unmount();
|
|
28
|
+
const clickCalls = removeSpy.mock.calls.filter(c => c[0] === "click");
|
|
29
|
+
expect(clickCalls.length).toBeGreaterThanOrEqual(1);
|
|
30
|
+
removeSpy.mockRestore();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("Click detection", () => {
|
|
35
|
+
it("calls callback when clicking outside the ref element", () => {
|
|
36
|
+
const inside = document.createElement("div");
|
|
37
|
+
const outside = document.createElement("button");
|
|
38
|
+
document.body.appendChild(inside);
|
|
39
|
+
document.body.appendChild(outside);
|
|
40
|
+
|
|
41
|
+
const ref = { current: inside };
|
|
42
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
43
|
+
|
|
44
|
+
outside.click();
|
|
45
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
46
|
+
|
|
47
|
+
document.body.removeChild(inside);
|
|
48
|
+
document.body.removeChild(outside);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not call callback when clicking inside the ref element", () => {
|
|
52
|
+
const container = document.createElement("div");
|
|
53
|
+
const child = document.createElement("span");
|
|
54
|
+
container.appendChild(child);
|
|
55
|
+
document.body.appendChild(container);
|
|
56
|
+
|
|
57
|
+
const ref = { current: container };
|
|
58
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
59
|
+
|
|
60
|
+
child.click();
|
|
61
|
+
expect(callback).not.toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
document.body.removeChild(container);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not call callback when clicking on the ref element itself", () => {
|
|
67
|
+
const el = document.createElement("div");
|
|
68
|
+
document.body.appendChild(el);
|
|
69
|
+
|
|
70
|
+
const ref = { current: el };
|
|
71
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
72
|
+
|
|
73
|
+
el.click();
|
|
74
|
+
expect(callback).not.toHaveBeenCalled();
|
|
75
|
+
|
|
76
|
+
document.body.removeChild(el);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("calls callback when ref.current is null", () => {
|
|
80
|
+
const ref = { current: null } as React.RefObject<any>;
|
|
81
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
82
|
+
|
|
83
|
+
document.body.click();
|
|
84
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("calls callback when ref is undefined-like", () => {
|
|
88
|
+
const ref = {} as React.RefObject<any>;
|
|
89
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
90
|
+
|
|
91
|
+
document.body.click();
|
|
92
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("calls callback when ref is null", () => {
|
|
96
|
+
renderHook(() => useOutsideClick(null as any, callback));
|
|
97
|
+
document.body.click();
|
|
98
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Multiple clicks", () => {
|
|
103
|
+
it("calls callback for each outside click", () => {
|
|
104
|
+
const inside = document.createElement("div");
|
|
105
|
+
document.body.appendChild(inside);
|
|
106
|
+
|
|
107
|
+
const ref = { current: inside };
|
|
108
|
+
renderHook(() => useOutsideClick(ref, callback));
|
|
109
|
+
|
|
110
|
+
document.body.click();
|
|
111
|
+
document.body.click();
|
|
112
|
+
document.body.click();
|
|
113
|
+
expect(callback).toHaveBeenCalledTimes(3);
|
|
114
|
+
|
|
115
|
+
document.body.removeChild(inside);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("Re-render behavior", () => {
|
|
120
|
+
it("updates listener on re-render with new callback", () => {
|
|
121
|
+
const el = document.createElement("div");
|
|
122
|
+
document.body.appendChild(el);
|
|
123
|
+
|
|
124
|
+
const callback1 = jest.fn();
|
|
125
|
+
const callback2 = jest.fn();
|
|
126
|
+
const ref = { current: el };
|
|
127
|
+
|
|
128
|
+
const { rerender } = renderHook(({ cb }) => useOutsideClick(ref, cb), {
|
|
129
|
+
initialProps: { cb: callback1 },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
document.body.click();
|
|
133
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
134
|
+
|
|
135
|
+
rerender({ cb: callback2 });
|
|
136
|
+
document.body.click();
|
|
137
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
138
|
+
|
|
139
|
+
document.body.removeChild(el);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|