@growth-angels/ds-core 1.13.0 → 1.14.1
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/dist/hooks/useBreakPointObserver.d.ts +5 -1
- package/dist/hooks/useBreakPointObserver.js +7 -3
- package/dist/organisms/Carousel/Carousel.d.ts +0 -3
- package/dist/organisms/Carousel/Carousel.js +166 -35
- package/package.json +2 -3
- package/src/hooks/useBreakPointObserver.ts +10 -5
- package/src/organisms/Carousel/Carousel.scss +0 -3
- package/src/organisms/Carousel/Carousel.tsx +254 -59
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
1
2
|
export declare function useBreakpointObserver(breakpoints?: {
|
|
3
|
+
xs: number;
|
|
2
4
|
sm: number;
|
|
3
5
|
md: number;
|
|
4
6
|
lg: number;
|
|
5
7
|
xl: number;
|
|
6
|
-
|
|
8
|
+
xxl: number;
|
|
9
|
+
}): Breakpoint;
|
|
10
|
+
export {};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useReactAdapter } from './useReactAdaptater';
|
|
2
|
-
export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
|
|
2
|
+
export function useBreakpointObserver(breakpoints = { xs: 375, sm: 640, md: 1024, lg: 1280, xl: 1400, xxl: 1600 }) {
|
|
3
3
|
const { useEffect, useState } = useReactAdapter();
|
|
4
4
|
const [current, setCurrent] = useState('sm');
|
|
5
5
|
useEffect(() => {
|
|
6
6
|
let frameId;
|
|
7
7
|
const check = () => {
|
|
8
8
|
const width = window.innerWidth;
|
|
9
|
-
let bp = '
|
|
10
|
-
if (width >= breakpoints.
|
|
9
|
+
let bp = 'xs';
|
|
10
|
+
if (width >= breakpoints.xxl)
|
|
11
|
+
bp = 'xxl';
|
|
12
|
+
else if (width >= breakpoints.xl)
|
|
11
13
|
bp = 'xl';
|
|
12
14
|
else if (width >= breakpoints.lg)
|
|
13
15
|
bp = 'lg';
|
|
@@ -15,6 +17,8 @@ export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 128
|
|
|
15
17
|
bp = 'md';
|
|
16
18
|
else if (width >= breakpoints.sm)
|
|
17
19
|
bp = 'sm';
|
|
20
|
+
else if (width < breakpoints.sm)
|
|
21
|
+
bp = 'xs';
|
|
18
22
|
setCurrent((prev) => (prev !== bp ? bp : prev));
|
|
19
23
|
frameId = requestAnimationFrame(check);
|
|
20
24
|
};
|
|
@@ -1,18 +1,161 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button } from "../../atoms/atoms";
|
|
3
|
+
import { useBreakpointObserver } from "../../hooks/useBreakPointObserver";
|
|
3
4
|
import { useReactAdapter } from "../../hooks/useReactAdaptater";
|
|
4
|
-
import { Swiper, SwiperSlide } from "swiper/react";
|
|
5
|
-
import { Navigation, Pagination, A11y } from "swiper/modules";
|
|
6
|
-
import "swiper/css";
|
|
7
|
-
import "swiper/css/navigation";
|
|
8
|
-
import "swiper/css/pagination";
|
|
9
5
|
export const Carousel = (props) => {
|
|
10
|
-
const { children, slidesPerView = { xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, loop = false, } = props;
|
|
11
|
-
const { useRef, Children } = useReactAdapter();
|
|
12
|
-
const
|
|
6
|
+
const { children, slidesPerView = { xs: 1, sm: 1, md: 2, lg: 3, xl: 4, xxl: 5 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, loop = false, } = props;
|
|
7
|
+
const { useEffect, useState, useRef, Children } = useReactAdapter();
|
|
8
|
+
const trackRef = useRef(null);
|
|
9
|
+
const isNavigatingRef = useRef(false);
|
|
13
10
|
const slides = Children.toArray(children);
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
|
|
12
|
+
return Math.max(1, Math.ceil((totalSlides - slidesPerView) / step) + 1);
|
|
13
|
+
};
|
|
14
|
+
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
|
|
15
|
+
const [activeDOMIndex, setActiveDOMIndex] = useState(0);
|
|
16
|
+
const [isAtEnd, setIsAtEnd] = useState(false);
|
|
17
|
+
const [isAtStart, setIsAtStart] = useState(true);
|
|
18
|
+
const [totalPages, setTotalPages] = useState(0);
|
|
19
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
20
|
+
const displayNavigation = hasNavigation && (loop || isOverflowing);
|
|
21
|
+
const displayPagination = hasPagination && (loop || isOverflowing);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (loop) {
|
|
24
|
+
setIsAtStart(false);
|
|
25
|
+
setIsAtEnd(false);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
setIsAtStart(activeSlideIndex === 0);
|
|
29
|
+
setIsAtEnd(activeSlideIndex === totalPages - 1);
|
|
30
|
+
}
|
|
31
|
+
}, [activeSlideIndex, totalPages, loop]);
|
|
32
|
+
const breakpoint = useBreakpointObserver({ xs: 375, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1440 });
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const currentSlidesPerView = slidesPerView[breakpoint] ??
|
|
35
|
+
(typeof slidesPerView === "number" ? slidesPerView : slidesPerView.xl ?? 1);
|
|
36
|
+
setTotalPages(calculatePages(slides.length, currentSlidesPerView, 1));
|
|
37
|
+
}, [breakpoint, slidesPerView]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const track = trackRef.current;
|
|
40
|
+
if (!track)
|
|
41
|
+
return;
|
|
42
|
+
const handleScroll = () => {
|
|
43
|
+
if (loop) {
|
|
44
|
+
const slideWidth = track.scrollWidth / (slides.length * 3);
|
|
45
|
+
const scrollLeft = track.scrollLeft;
|
|
46
|
+
// Repositionnement infini (uniquement si pas en navigation)
|
|
47
|
+
if (!isNavigatingRef.current) {
|
|
48
|
+
if (scrollLeft <= slideWidth * slides.length) {
|
|
49
|
+
track.scrollLeft = scrollLeft + slideWidth * slides.length;
|
|
50
|
+
}
|
|
51
|
+
else if (scrollLeft >= slideWidth * slides.length * 2) {
|
|
52
|
+
track.scrollLeft = scrollLeft - slideWidth * slides.length;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2;
|
|
56
|
+
const domIndex = Math.floor(scrollPosition / slideWidth);
|
|
57
|
+
const index = domIndex % slides.length;
|
|
58
|
+
setActiveDOMIndex(domIndex);
|
|
59
|
+
setActiveSlideIndex(index);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const slideWidth = track.scrollWidth / slides.length;
|
|
63
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2;
|
|
64
|
+
const index = Math.floor(scrollPosition / slideWidth);
|
|
65
|
+
setActiveSlideIndex(Math.min(index, slides.length - 1));
|
|
66
|
+
setActiveDOMIndex(index);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
track.addEventListener("scroll", handleScroll);
|
|
70
|
+
// Position initiale au milieu en mode loop
|
|
71
|
+
if (loop) {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
const slideWidth = track.scrollWidth / (slides.length * 3);
|
|
74
|
+
track.scrollLeft = slideWidth * slides.length;
|
|
75
|
+
}, 0);
|
|
76
|
+
}
|
|
77
|
+
return () => track.removeEventListener("scroll", handleScroll);
|
|
78
|
+
}, [slides.length, loop]);
|
|
79
|
+
// Check if the slides overflow the container
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const track = trackRef.current;
|
|
82
|
+
if (!track)
|
|
83
|
+
return;
|
|
84
|
+
const handleResize = () => {
|
|
85
|
+
setIsOverflowing(track.scrollWidth > track.clientWidth);
|
|
86
|
+
};
|
|
87
|
+
handleResize(); // Initial check
|
|
88
|
+
window.addEventListener("resize", handleResize);
|
|
89
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
90
|
+
}, [loop]);
|
|
91
|
+
const style = Object.fromEntries(Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`]));
|
|
92
|
+
const goPrev = () => {
|
|
93
|
+
if (!trackRef.current)
|
|
94
|
+
return;
|
|
95
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
|
|
96
|
+
if (activeSlideIndex === 0 && !loop)
|
|
97
|
+
return;
|
|
98
|
+
isNavigatingRef.current = true;
|
|
99
|
+
if (loop) {
|
|
100
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
101
|
+
allSlides[activeDOMIndex - 1]?.scrollIntoView({
|
|
102
|
+
behavior: "smooth",
|
|
103
|
+
block: "nearest",
|
|
104
|
+
inline: "start",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const nextIndex = activeSlideIndex - 1;
|
|
109
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
110
|
+
behavior: "smooth",
|
|
111
|
+
block: "nearest",
|
|
112
|
+
inline: "start",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
isNavigatingRef.current = false;
|
|
117
|
+
}, 600);
|
|
118
|
+
};
|
|
119
|
+
const goNext = () => {
|
|
120
|
+
if (!trackRef.current)
|
|
121
|
+
return;
|
|
122
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
|
|
123
|
+
isNavigatingRef.current = true;
|
|
124
|
+
if (loop) {
|
|
125
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
126
|
+
allSlides[activeDOMIndex + 1]?.scrollIntoView({
|
|
127
|
+
behavior: "smooth",
|
|
128
|
+
block: "nearest",
|
|
129
|
+
inline: "start",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const totalSlides = allSlides.length;
|
|
134
|
+
if (activeSlideIndex >= totalSlides - 1)
|
|
135
|
+
return;
|
|
136
|
+
if (isAtEnd)
|
|
137
|
+
return;
|
|
138
|
+
const nextIndex = activeSlideIndex + 1;
|
|
139
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
140
|
+
behavior: "smooth",
|
|
141
|
+
block: "nearest",
|
|
142
|
+
inline: "start",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
isNavigatingRef.current = false;
|
|
147
|
+
}, 600);
|
|
148
|
+
};
|
|
149
|
+
const goTo = (nextIndex) => {
|
|
150
|
+
if (!trackRef.current)
|
|
151
|
+
return;
|
|
152
|
+
const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
|
|
153
|
+
slides[nextIndex]?.scrollIntoView({
|
|
154
|
+
behavior: "smooth",
|
|
155
|
+
block: "nearest",
|
|
156
|
+
inline: "start",
|
|
157
|
+
});
|
|
158
|
+
};
|
|
16
159
|
const classes = ["ga-ds-carousel"];
|
|
17
160
|
if (props.extraClassNames) {
|
|
18
161
|
if (typeof props.extraClassNames === "string") {
|
|
@@ -22,31 +165,19 @@ export const Carousel = (props) => {
|
|
|
22
165
|
classes.push(...props.extraClassNames);
|
|
23
166
|
}
|
|
24
167
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
992: { slidesPerView: slidesPerView.md || 2 },
|
|
29
|
-
1200: { slidesPerView: slidesPerView.lg || 3 },
|
|
30
|
-
1440: { slidesPerView: slidesPerView.xl || 4 },
|
|
31
|
-
};
|
|
32
|
-
return (_jsxs("div", { className: classes.join(" "), children: [navigation?.positionY === "top" && displayNavigation && _jsx(CarouselNavigation, {}), _jsx(Swiper, { modules: [Navigation, Pagination, A11y], spaceBetween: spaceBetween, breakpoints: breakpoints, loop: loop, navigation: displayNavigation
|
|
33
|
-
? {
|
|
34
|
-
prevEl: ".ga-ds-carousel__button--prev",
|
|
35
|
-
nextEl: ".ga-ds-carousel__button--next",
|
|
36
|
-
}
|
|
37
|
-
: false, pagination: displayPagination && pagination
|
|
38
|
-
? {
|
|
39
|
-
clickable: pagination.clickable,
|
|
40
|
-
el: ".ga-ds-carousel__dots",
|
|
41
|
-
bulletClass: "ga-ds-carousel__dot",
|
|
42
|
-
bulletActiveClass: "ga-ds-carousel__dot--active",
|
|
43
|
-
}
|
|
44
|
-
: false, onSwiper: (swiper) => {
|
|
45
|
-
swiperRef.current = swiper;
|
|
46
|
-
}, className: "ga-ds-carousel__track", children: context === "wp-editor"
|
|
168
|
+
return (_jsxs("div", { className: classes.join(" "), style: style, children: [navigation?.positionY === "top" && displayNavigation && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd })), _jsx("div", { className: "ga-ds-carousel__track", ref: trackRef, style: {
|
|
169
|
+
"--ga-ds-space-between": `${spaceBetween / 10}rem`,
|
|
170
|
+
}, children: context === "wp-editor"
|
|
47
171
|
? children
|
|
48
|
-
: slides.map((child, index) =>
|
|
172
|
+
: slides.map((child, index) => {
|
|
173
|
+
return (_jsx("div", { className: `ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`, children: child }, index));
|
|
174
|
+
}) }), _jsxs("div", { className: "ga-ds-carousel__navigation", children: [pagination && displayPagination && (_jsx(CarouselPagination, { totalPages: totalPages, activeSlideIndex: activeSlideIndex, goTo: goTo, clickable: pagination.clickable })), displayNavigation && navigation?.positionY === "bottom" && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd }))] })] }));
|
|
175
|
+
};
|
|
176
|
+
const CarouselNavigation = ({ goPrev, goNext, isAtStart, isAtEnd, }) => {
|
|
177
|
+
return (_jsxs("div", { className: `ga-ds-carousel__arrows`, children: [_jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--prev"], icon: "chevron-left", onClick: goPrev, disabled: isAtStart }), _jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--next"], icon: "chevron-right", onClick: goNext, disabled: isAtEnd })] }));
|
|
49
178
|
};
|
|
50
|
-
const
|
|
51
|
-
return (
|
|
179
|
+
const CarouselPagination = ({ totalPages, activeSlideIndex, goTo, clickable, }) => {
|
|
180
|
+
return (_jsx("div", { className: "ga-ds-carousel__dots", children: Array.from({ length: totalPages }, (_, index) => index).map((_, index) => (_jsx("span", { className: `ga-ds-carousel__dot ${index === activeSlideIndex ? "ga-ds-carousel__dot--active" : ""}`, onClick: () => clickable && goTo(index), style: {
|
|
181
|
+
cursor: clickable ? "pointer" : "default",
|
|
182
|
+
} }, index))) }));
|
|
52
183
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-angels/ds-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
4
4
|
"description": "Design system by Growth Angels",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
"build-storybook": "storybook build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@growth-angels/foundation": "^1.4.1"
|
|
41
|
-
"swiper": "^11.1.15"
|
|
40
|
+
"@growth-angels/foundation": "^1.4.1"
|
|
42
41
|
},
|
|
43
42
|
"peerDependencies": {
|
|
44
43
|
"react": ">=18"
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { useReactAdapter } from './useReactAdaptater'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
|
|
5
|
+
|
|
6
|
+
export function useBreakpointObserver(breakpoints = { xs: 375, sm: 640, md: 1024, lg: 1280, xl: 1400, xxl: 1600 }): Breakpoint {
|
|
4
7
|
const { useEffect, useState } = useReactAdapter()
|
|
5
|
-
const [current, setCurrent] = useState<
|
|
8
|
+
const [current, setCurrent] = useState<Breakpoint>('sm')
|
|
6
9
|
useEffect(() => {
|
|
7
10
|
let frameId: number
|
|
8
11
|
|
|
9
12
|
const check = () => {
|
|
10
13
|
const width = window.innerWidth
|
|
11
|
-
let bp:
|
|
12
|
-
|
|
13
|
-
if (width >= breakpoints.
|
|
14
|
+
let bp: Breakpoint = 'xs'
|
|
15
|
+
|
|
16
|
+
if (width >= breakpoints.xxl) bp = 'xxl'
|
|
17
|
+
else if (width >= breakpoints.xl) bp = 'xl'
|
|
14
18
|
else if (width >= breakpoints.lg) bp = 'lg'
|
|
15
19
|
else if (width >= breakpoints.md) bp = 'md'
|
|
16
20
|
else if (width >= breakpoints.sm) bp = 'sm'
|
|
21
|
+
else if (width < breakpoints.sm) bp = 'xs'
|
|
17
22
|
|
|
18
23
|
setCurrent((prev) => (prev !== bp ? bp : prev))
|
|
19
24
|
frameId = requestAnimationFrame(check)
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { Button } from "../../atoms/atoms"
|
|
2
|
+
import { useBreakpointObserver } from "../../hooks/useBreakPointObserver"
|
|
2
3
|
import { useReactAdapter } from "../../hooks/useReactAdaptater"
|
|
3
4
|
import { CarouselProps } from "./Carousel.types"
|
|
4
|
-
import { Swiper, SwiperSlide } from "swiper/react"
|
|
5
|
-
import { Navigation, Pagination, A11y } from "swiper/modules"
|
|
6
|
-
import type { Swiper as SwiperType } from "swiper"
|
|
7
|
-
import "swiper/css"
|
|
8
|
-
import "swiper/css/navigation"
|
|
9
|
-
import "swiper/css/pagination"
|
|
10
5
|
|
|
11
6
|
export const Carousel = (props: CarouselProps) => {
|
|
12
7
|
const {
|
|
13
8
|
children,
|
|
14
|
-
slidesPerView = { xs: 1, sm: 1, md: 2, lg: 3, xl: 4 },
|
|
9
|
+
slidesPerView = { xs: 1, sm: 1, md: 2, lg: 3, xl: 4, xxl: 5 },
|
|
15
10
|
spaceBetween = 20,
|
|
16
11
|
navigation,
|
|
17
12
|
pagination,
|
|
@@ -20,12 +15,177 @@ export const Carousel = (props: CarouselProps) => {
|
|
|
20
15
|
hasNavigation,
|
|
21
16
|
loop = false,
|
|
22
17
|
} = props
|
|
23
|
-
const { useRef, Children } = useReactAdapter()
|
|
24
|
-
const
|
|
18
|
+
const { useEffect, useState, useRef, Children } = useReactAdapter()
|
|
19
|
+
const trackRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
const isNavigatingRef = useRef(false)
|
|
25
21
|
const slides = Children.toArray(children)
|
|
26
22
|
|
|
27
|
-
const
|
|
28
|
-
|
|
23
|
+
const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
|
|
24
|
+
return Math.max(1, Math.ceil((totalSlides - slidesPerView) / step) + 1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [activeSlideIndex, setActiveSlideIndex] = useState(0)
|
|
28
|
+
const [activeDOMIndex, setActiveDOMIndex] = useState(0)
|
|
29
|
+
const [isAtEnd, setIsAtEnd] = useState(false)
|
|
30
|
+
const [isAtStart, setIsAtStart] = useState(true)
|
|
31
|
+
const [totalPages, setTotalPages] = useState(0)
|
|
32
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
33
|
+
|
|
34
|
+
const displayNavigation = hasNavigation && (loop || isOverflowing)
|
|
35
|
+
const displayPagination = hasPagination && (loop || isOverflowing)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (loop) {
|
|
39
|
+
setIsAtStart(false)
|
|
40
|
+
setIsAtEnd(false)
|
|
41
|
+
} else {
|
|
42
|
+
setIsAtStart(activeSlideIndex === 0)
|
|
43
|
+
setIsAtEnd(activeSlideIndex === totalPages - 1)
|
|
44
|
+
}
|
|
45
|
+
}, [activeSlideIndex, totalPages, loop])
|
|
46
|
+
|
|
47
|
+
const breakpoint = useBreakpointObserver({ xs: 375, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1440 })
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const currentSlidesPerView =
|
|
51
|
+
(slidesPerView as Record<string, number>)[breakpoint] ??
|
|
52
|
+
(typeof slidesPerView === "number" ? slidesPerView : (slidesPerView as any).xl ?? 1)
|
|
53
|
+
|
|
54
|
+
setTotalPages(calculatePages(slides.length, currentSlidesPerView, 1))
|
|
55
|
+
}, [breakpoint, slidesPerView])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const track = trackRef.current
|
|
59
|
+
if (!track) return
|
|
60
|
+
|
|
61
|
+
const handleScroll = () => {
|
|
62
|
+
if (loop) {
|
|
63
|
+
const slideWidth = track.scrollWidth / (slides.length * 3)
|
|
64
|
+
const scrollLeft = track.scrollLeft
|
|
65
|
+
|
|
66
|
+
// Repositionnement infini (uniquement si pas en navigation)
|
|
67
|
+
if (!isNavigatingRef.current) {
|
|
68
|
+
if (scrollLeft <= slideWidth * slides.length) {
|
|
69
|
+
track.scrollLeft = scrollLeft + slideWidth * slides.length
|
|
70
|
+
} else if (scrollLeft >= slideWidth * slides.length * 2) {
|
|
71
|
+
track.scrollLeft = scrollLeft - slideWidth * slides.length
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2
|
|
76
|
+
const domIndex = Math.floor(scrollPosition / slideWidth)
|
|
77
|
+
const index = domIndex % slides.length
|
|
78
|
+
setActiveDOMIndex(domIndex)
|
|
79
|
+
setActiveSlideIndex(index)
|
|
80
|
+
} else {
|
|
81
|
+
const slideWidth = track.scrollWidth / slides.length
|
|
82
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2
|
|
83
|
+
const index = Math.floor(scrollPosition / slideWidth)
|
|
84
|
+
setActiveSlideIndex(Math.min(index, slides.length - 1))
|
|
85
|
+
setActiveDOMIndex(index)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
track.addEventListener("scroll", handleScroll)
|
|
90
|
+
|
|
91
|
+
// Position initiale au milieu en mode loop
|
|
92
|
+
if (loop) {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
const slideWidth = track.scrollWidth / (slides.length * 3)
|
|
95
|
+
track.scrollLeft = slideWidth * slides.length
|
|
96
|
+
}, 0)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return () => track.removeEventListener("scroll", handleScroll)
|
|
100
|
+
}, [slides.length, loop])
|
|
101
|
+
|
|
102
|
+
// Check if the slides overflow the container
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const track = trackRef.current
|
|
105
|
+
if (!track) return
|
|
106
|
+
|
|
107
|
+
const handleResize = () => {
|
|
108
|
+
setIsOverflowing(track.scrollWidth > track.clientWidth)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
handleResize() // Initial check
|
|
112
|
+
|
|
113
|
+
window.addEventListener("resize", handleResize)
|
|
114
|
+
return () => window.removeEventListener("resize", handleResize)
|
|
115
|
+
}, [loop])
|
|
116
|
+
|
|
117
|
+
const style = Object.fromEntries(
|
|
118
|
+
Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`])
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const goPrev = () => {
|
|
122
|
+
if (!trackRef.current) return
|
|
123
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
|
|
124
|
+
if (activeSlideIndex === 0 && !loop) return
|
|
125
|
+
|
|
126
|
+
isNavigatingRef.current = true
|
|
127
|
+
|
|
128
|
+
if (loop) {
|
|
129
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
130
|
+
allSlides[activeDOMIndex - 1]?.scrollIntoView({
|
|
131
|
+
behavior: "smooth",
|
|
132
|
+
block: "nearest",
|
|
133
|
+
inline: "start",
|
|
134
|
+
})
|
|
135
|
+
} else {
|
|
136
|
+
const nextIndex = activeSlideIndex - 1
|
|
137
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
138
|
+
behavior: "smooth",
|
|
139
|
+
block: "nearest",
|
|
140
|
+
inline: "start",
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
isNavigatingRef.current = false
|
|
146
|
+
}, 600)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const goNext = () => {
|
|
150
|
+
if (!trackRef.current) return
|
|
151
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
|
|
152
|
+
|
|
153
|
+
isNavigatingRef.current = true
|
|
154
|
+
|
|
155
|
+
if (loop) {
|
|
156
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
157
|
+
allSlides[activeDOMIndex + 1]?.scrollIntoView({
|
|
158
|
+
behavior: "smooth",
|
|
159
|
+
block: "nearest",
|
|
160
|
+
inline: "start",
|
|
161
|
+
})
|
|
162
|
+
} else {
|
|
163
|
+
const totalSlides = allSlides.length
|
|
164
|
+
if (activeSlideIndex >= totalSlides - 1) return
|
|
165
|
+
if (isAtEnd) return
|
|
166
|
+
|
|
167
|
+
const nextIndex = activeSlideIndex + 1
|
|
168
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
169
|
+
behavior: "smooth",
|
|
170
|
+
block: "nearest",
|
|
171
|
+
inline: "start",
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
isNavigatingRef.current = false
|
|
177
|
+
}, 600)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const goTo = (nextIndex: number) => {
|
|
181
|
+
if (!trackRef.current) return
|
|
182
|
+
const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
|
|
183
|
+
slides[nextIndex]?.scrollIntoView({
|
|
184
|
+
behavior: "smooth",
|
|
185
|
+
block: "nearest",
|
|
186
|
+
inline: "start",
|
|
187
|
+
})
|
|
188
|
+
}
|
|
29
189
|
|
|
30
190
|
const classes = ["ga-ds-carousel"]
|
|
31
191
|
|
|
@@ -37,68 +197,103 @@ export const Carousel = (props: CarouselProps) => {
|
|
|
37
197
|
}
|
|
38
198
|
}
|
|
39
199
|
|
|
40
|
-
const breakpoints = {
|
|
41
|
-
0: { slidesPerView: slidesPerView.xs || slidesPerView.sm || 1 },
|
|
42
|
-
576: { slidesPerView: slidesPerView.sm || 1 },
|
|
43
|
-
992: { slidesPerView: slidesPerView.md || 2 },
|
|
44
|
-
1200: { slidesPerView: slidesPerView.lg || 3 },
|
|
45
|
-
1440: { slidesPerView: slidesPerView.xl || 4 },
|
|
46
|
-
}
|
|
47
|
-
|
|
48
200
|
return (
|
|
49
|
-
<div className={classes.join(" ")}>
|
|
50
|
-
{navigation?.positionY === "top" && displayNavigation &&
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
spaceBetween={spaceBetween}
|
|
55
|
-
breakpoints={breakpoints}
|
|
56
|
-
loop={loop}
|
|
57
|
-
navigation={
|
|
58
|
-
displayNavigation
|
|
59
|
-
? {
|
|
60
|
-
prevEl: ".ga-ds-carousel__button--prev",
|
|
61
|
-
nextEl: ".ga-ds-carousel__button--next",
|
|
62
|
-
}
|
|
63
|
-
: false
|
|
64
|
-
}
|
|
65
|
-
pagination={
|
|
66
|
-
displayPagination && pagination
|
|
67
|
-
? {
|
|
68
|
-
clickable: pagination.clickable,
|
|
69
|
-
el: ".ga-ds-carousel__dots",
|
|
70
|
-
bulletClass: "ga-ds-carousel__dot",
|
|
71
|
-
bulletActiveClass: "ga-ds-carousel__dot--active",
|
|
72
|
-
}
|
|
73
|
-
: false
|
|
74
|
-
}
|
|
75
|
-
onSwiper={(swiper: SwiperType) => {
|
|
76
|
-
swiperRef.current = swiper
|
|
77
|
-
}}
|
|
201
|
+
<div className={classes.join(" ")} style={style}>
|
|
202
|
+
{navigation?.positionY === "top" && displayNavigation && (
|
|
203
|
+
<CarouselNavigation goPrev={goPrev} goNext={goNext} isAtStart={isAtStart} isAtEnd={isAtEnd} />
|
|
204
|
+
)}
|
|
205
|
+
<div
|
|
78
206
|
className="ga-ds-carousel__track"
|
|
207
|
+
ref={trackRef}
|
|
208
|
+
style={
|
|
209
|
+
{
|
|
210
|
+
"--ga-ds-space-between": `${spaceBetween / 10}rem`,
|
|
211
|
+
} as React.CSSProperties
|
|
212
|
+
}
|
|
79
213
|
>
|
|
80
214
|
{context === "wp-editor"
|
|
81
215
|
? children
|
|
82
|
-
: slides.map((child, index) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
216
|
+
: slides.map((child, index) => {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
key={index}
|
|
220
|
+
className={`ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`}
|
|
221
|
+
>
|
|
222
|
+
{child}
|
|
223
|
+
</div>
|
|
224
|
+
)
|
|
225
|
+
})}
|
|
226
|
+
</div>
|
|
88
227
|
|
|
89
228
|
<div className="ga-ds-carousel__navigation">
|
|
90
|
-
{displayPagination &&
|
|
91
|
-
|
|
229
|
+
{pagination && displayPagination && (
|
|
230
|
+
<CarouselPagination
|
|
231
|
+
totalPages={totalPages}
|
|
232
|
+
activeSlideIndex={activeSlideIndex}
|
|
233
|
+
goTo={goTo}
|
|
234
|
+
clickable={pagination.clickable}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
{displayNavigation && navigation?.positionY === "bottom" && (
|
|
238
|
+
<CarouselNavigation goPrev={goPrev} goNext={goNext} isAtStart={isAtStart} isAtEnd={isAtEnd} />
|
|
239
|
+
)}
|
|
92
240
|
</div>
|
|
93
241
|
</div>
|
|
94
242
|
)
|
|
95
243
|
}
|
|
96
244
|
|
|
97
|
-
const CarouselNavigation = (
|
|
245
|
+
const CarouselNavigation = ({
|
|
246
|
+
goPrev,
|
|
247
|
+
goNext,
|
|
248
|
+
isAtStart,
|
|
249
|
+
isAtEnd,
|
|
250
|
+
}: {
|
|
251
|
+
goPrev: () => void
|
|
252
|
+
goNext: () => void
|
|
253
|
+
isAtStart: boolean
|
|
254
|
+
isAtEnd: boolean
|
|
255
|
+
}) => {
|
|
98
256
|
return (
|
|
99
257
|
<div className={`ga-ds-carousel__arrows`}>
|
|
100
|
-
<Button
|
|
101
|
-
|
|
258
|
+
<Button
|
|
259
|
+
extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--prev"]}
|
|
260
|
+
icon="chevron-left"
|
|
261
|
+
onClick={goPrev}
|
|
262
|
+
disabled={isAtStart}
|
|
263
|
+
/>
|
|
264
|
+
<Button
|
|
265
|
+
extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--next"]}
|
|
266
|
+
icon="chevron-right"
|
|
267
|
+
onClick={goNext}
|
|
268
|
+
disabled={isAtEnd}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const CarouselPagination = ({
|
|
275
|
+
totalPages,
|
|
276
|
+
activeSlideIndex,
|
|
277
|
+
goTo,
|
|
278
|
+
clickable,
|
|
279
|
+
}: {
|
|
280
|
+
totalPages: number
|
|
281
|
+
activeSlideIndex: number
|
|
282
|
+
goTo: (nextIndex: number) => void
|
|
283
|
+
clickable: boolean
|
|
284
|
+
}) => {
|
|
285
|
+
return (
|
|
286
|
+
<div className="ga-ds-carousel__dots">
|
|
287
|
+
{Array.from({ length: totalPages }, (_, index) => index).map((_, index) => (
|
|
288
|
+
<span
|
|
289
|
+
key={index}
|
|
290
|
+
className={`ga-ds-carousel__dot ${index === activeSlideIndex ? "ga-ds-carousel__dot--active" : ""}`}
|
|
291
|
+
onClick={() => clickable && goTo(index)}
|
|
292
|
+
style={{
|
|
293
|
+
cursor: clickable ? "pointer" : "default",
|
|
294
|
+
}}
|
|
295
|
+
></span>
|
|
296
|
+
))}
|
|
102
297
|
</div>
|
|
103
298
|
)
|
|
104
299
|
}
|