@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.
@@ -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
- }): "sm" | "md" | "lg" | "xl";
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 = 'sm';
10
- if (width >= breakpoints.xl)
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,5 +1,2 @@
1
1
  import { CarouselProps } from "./Carousel.types";
2
- import "swiper/css";
3
- import "swiper/css/navigation";
4
- import "swiper/css/pagination";
5
2
  export declare const Carousel: (props: CarouselProps) => import("react/jsx-runtime").JSX.Element;
@@ -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 swiperRef = useRef(null);
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 displayNavigation = hasNavigation;
15
- const displayPagination = hasPagination;
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
- const breakpoints = {
26
- 0: { slidesPerView: slidesPerView.xs || slidesPerView.sm || 1 },
27
- 576: { slidesPerView: slidesPerView.sm || 1 },
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) => (_jsx(SwiperSlide, { className: "ga-ds-carousel__slide", children: child }, index))) }), _jsxs("div", { className: "ga-ds-carousel__navigation", children: [displayPagination && _jsx("div", { className: "ga-ds-carousel__dots" }), displayNavigation && navigation?.positionY === "bottom" && _jsx(CarouselNavigation, {})] })] }));
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 CarouselNavigation = () => {
51
- return (_jsxs("div", { className: `ga-ds-carousel__arrows`, children: [_jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--prev"], icon: "chevron-left" }), _jsx(Button, { extraClassNames: ["ga-ds-carousel__button", "ga-ds-carousel__button--next"], icon: "chevron-right" })] }));
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.13.0",
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
- export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
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<'sm' | 'md' | 'lg' | 'xl'>('sm')
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: 'sm' | 'md' | 'lg' | 'xl' = 'sm'
12
-
13
- if (width >= breakpoints.xl) bp = 'xl'
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)
@@ -46,9 +46,6 @@
46
46
  flex: 0 0 auto;
47
47
  width: 100%;
48
48
  scroll-snap-align: start;
49
- width: 100%;
50
- max-width: 100%;
51
- min-width: 0;
52
49
  }
53
50
 
54
51
  &__arrows {
@@ -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 swiperRef = useRef<SwiperType | null>(null)
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 displayNavigation = hasNavigation
28
- const displayPagination = hasPagination
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 && <CarouselNavigation />}
51
-
52
- <Swiper
53
- modules={[Navigation, Pagination, A11y]}
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
- <SwiperSlide key={index} className="ga-ds-carousel__slide">
84
- {child}
85
- </SwiperSlide>
86
- ))}
87
- </Swiper>
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 && <div className="ga-ds-carousel__dots"></div>}
91
- {displayNavigation && navigation?.positionY === "bottom" && <CarouselNavigation />}
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 extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--prev"]} icon="chevron-left" />
101
- <Button extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--next"]} icon="chevron-right" />
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
  }