@growth-angels/ds-core 1.13.0 → 1.14.0

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,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,159 @@
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 = { sm: 1, md: 2, lg: 3, xl: 4 }, 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({ sm: 768, md: 992, lg: 1200, xl: 1400 });
33
+ useEffect(() => {
34
+ setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1));
35
+ }, [breakpoint, slidesPerView]);
36
+ useEffect(() => {
37
+ const track = trackRef.current;
38
+ if (!track)
39
+ return;
40
+ const handleScroll = () => {
41
+ if (loop) {
42
+ const slideWidth = track.scrollWidth / (slides.length * 3);
43
+ const scrollLeft = track.scrollLeft;
44
+ // Repositionnement infini (uniquement si pas en navigation)
45
+ if (!isNavigatingRef.current) {
46
+ if (scrollLeft <= slideWidth * slides.length) {
47
+ track.scrollLeft = scrollLeft + slideWidth * slides.length;
48
+ }
49
+ else if (scrollLeft >= slideWidth * slides.length * 2) {
50
+ track.scrollLeft = scrollLeft - slideWidth * slides.length;
51
+ }
52
+ }
53
+ const scrollPosition = track.scrollLeft + slideWidth / 2;
54
+ const domIndex = Math.floor(scrollPosition / slideWidth);
55
+ const index = domIndex % slides.length;
56
+ setActiveDOMIndex(domIndex);
57
+ setActiveSlideIndex(index);
58
+ }
59
+ else {
60
+ const slideWidth = track.scrollWidth / slides.length;
61
+ const scrollPosition = track.scrollLeft + slideWidth / 2;
62
+ const index = Math.floor(scrollPosition / slideWidth);
63
+ setActiveSlideIndex(Math.min(index, slides.length - 1));
64
+ setActiveDOMIndex(index);
65
+ }
66
+ };
67
+ track.addEventListener("scroll", handleScroll);
68
+ // Position initiale au milieu en mode loop
69
+ if (loop) {
70
+ setTimeout(() => {
71
+ const slideWidth = track.scrollWidth / (slides.length * 3);
72
+ track.scrollLeft = slideWidth * slides.length;
73
+ }, 0);
74
+ }
75
+ return () => track.removeEventListener("scroll", handleScroll);
76
+ }, [slides.length, loop]);
77
+ // Check if the slides overflow the container
78
+ useEffect(() => {
79
+ const track = trackRef.current;
80
+ if (!track)
81
+ return;
82
+ const handleResize = () => {
83
+ setIsOverflowing(track.scrollWidth > track.clientWidth);
84
+ };
85
+ handleResize(); // Initial check
86
+ window.addEventListener("resize", handleResize);
87
+ return () => window.removeEventListener("resize", handleResize);
88
+ }, [loop]);
89
+ const style = Object.fromEntries(Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`]));
90
+ const goPrev = () => {
91
+ if (!trackRef.current)
92
+ return;
93
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
94
+ if (activeSlideIndex === 0 && !loop)
95
+ return;
96
+ isNavigatingRef.current = true;
97
+ if (loop) {
98
+ // En mode loop, on utilise l'index DOM actuel
99
+ allSlides[activeDOMIndex - 1]?.scrollIntoView({
100
+ behavior: "smooth",
101
+ block: "nearest",
102
+ inline: "start",
103
+ });
104
+ }
105
+ else {
106
+ const nextIndex = activeSlideIndex - 1;
107
+ allSlides[nextIndex]?.scrollIntoView({
108
+ behavior: "smooth",
109
+ block: "nearest",
110
+ inline: "start",
111
+ });
112
+ }
113
+ setTimeout(() => {
114
+ isNavigatingRef.current = false;
115
+ }, 600);
116
+ };
117
+ const goNext = () => {
118
+ if (!trackRef.current)
119
+ return;
120
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
121
+ isNavigatingRef.current = true;
122
+ if (loop) {
123
+ // En mode loop, on utilise l'index DOM actuel
124
+ allSlides[activeDOMIndex + 1]?.scrollIntoView({
125
+ behavior: "smooth",
126
+ block: "nearest",
127
+ inline: "start",
128
+ });
129
+ }
130
+ else {
131
+ const totalSlides = allSlides.length;
132
+ if (activeSlideIndex >= totalSlides - 1)
133
+ return;
134
+ if (isAtEnd)
135
+ return;
136
+ const nextIndex = activeSlideIndex + 1;
137
+ allSlides[nextIndex]?.scrollIntoView({
138
+ behavior: "smooth",
139
+ block: "nearest",
140
+ inline: "start",
141
+ });
142
+ }
143
+ setTimeout(() => {
144
+ isNavigatingRef.current = false;
145
+ }, 600);
146
+ };
147
+ const goTo = (nextIndex) => {
148
+ if (!trackRef.current)
149
+ return;
150
+ const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
151
+ slides[nextIndex]?.scrollIntoView({
152
+ behavior: "smooth",
153
+ block: "nearest",
154
+ inline: "start",
155
+ });
156
+ };
16
157
  const classes = ["ga-ds-carousel"];
17
158
  if (props.extraClassNames) {
18
159
  if (typeof props.extraClassNames === "string") {
@@ -22,31 +163,19 @@ export const Carousel = (props) => {
22
163
  classes.push(...props.extraClassNames);
23
164
  }
24
165
  }
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"
166
+ 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: {
167
+ "--ga-ds-space-between": `${spaceBetween / 10}rem`,
168
+ }, children: context === "wp-editor"
47
169
  ? 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, {})] })] }));
170
+ : slides.map((child, index) => {
171
+ return (_jsx("div", { className: `ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`, children: child }, index));
172
+ }) }), _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 }))] })] }));
173
+ };
174
+ const CarouselNavigation = ({ goPrev, goNext, isAtStart, isAtEnd, }) => {
175
+ 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
176
  };
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" })] }));
177
+ const CarouselPagination = ({ totalPages, activeSlideIndex, goTo, clickable, }) => {
178
+ 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: {
179
+ cursor: clickable ? "pointer" : "default",
180
+ } }, index))) }));
52
181
  };
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.0",
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"
@@ -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 = { sm: 1, md: 2, lg: 3, xl: 4 },
15
10
  spaceBetween = 20,
16
11
  navigation,
17
12
  pagination,
@@ -20,12 +15,173 @@ 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({ sm: 768, md: 992, lg: 1200, xl: 1400 })
48
+
49
+ useEffect(() => {
50
+ setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1))
51
+ }, [breakpoint, slidesPerView])
52
+
53
+ useEffect(() => {
54
+ const track = trackRef.current
55
+ if (!track) return
56
+
57
+ const handleScroll = () => {
58
+ if (loop) {
59
+ const slideWidth = track.scrollWidth / (slides.length * 3)
60
+ const scrollLeft = track.scrollLeft
61
+
62
+ // Repositionnement infini (uniquement si pas en navigation)
63
+ if (!isNavigatingRef.current) {
64
+ if (scrollLeft <= slideWidth * slides.length) {
65
+ track.scrollLeft = scrollLeft + slideWidth * slides.length
66
+ } else if (scrollLeft >= slideWidth * slides.length * 2) {
67
+ track.scrollLeft = scrollLeft - slideWidth * slides.length
68
+ }
69
+ }
70
+
71
+ const scrollPosition = track.scrollLeft + slideWidth / 2
72
+ const domIndex = Math.floor(scrollPosition / slideWidth)
73
+ const index = domIndex % slides.length
74
+ setActiveDOMIndex(domIndex)
75
+ setActiveSlideIndex(index)
76
+ } else {
77
+ const slideWidth = track.scrollWidth / slides.length
78
+ const scrollPosition = track.scrollLeft + slideWidth / 2
79
+ const index = Math.floor(scrollPosition / slideWidth)
80
+ setActiveSlideIndex(Math.min(index, slides.length - 1))
81
+ setActiveDOMIndex(index)
82
+ }
83
+ }
84
+
85
+ track.addEventListener("scroll", handleScroll)
86
+
87
+ // Position initiale au milieu en mode loop
88
+ if (loop) {
89
+ setTimeout(() => {
90
+ const slideWidth = track.scrollWidth / (slides.length * 3)
91
+ track.scrollLeft = slideWidth * slides.length
92
+ }, 0)
93
+ }
94
+
95
+ return () => track.removeEventListener("scroll", handleScroll)
96
+ }, [slides.length, loop])
97
+
98
+ // Check if the slides overflow the container
99
+ useEffect(() => {
100
+ const track = trackRef.current
101
+ if (!track) return
102
+
103
+ const handleResize = () => {
104
+ setIsOverflowing(track.scrollWidth > track.clientWidth)
105
+ }
106
+
107
+ handleResize() // Initial check
108
+
109
+ window.addEventListener("resize", handleResize)
110
+ return () => window.removeEventListener("resize", handleResize)
111
+ }, [loop])
112
+
113
+ const style = Object.fromEntries(
114
+ Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`])
115
+ )
116
+
117
+ const goPrev = () => {
118
+ if (!trackRef.current) return
119
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
120
+ if (activeSlideIndex === 0 && !loop) return
121
+
122
+ isNavigatingRef.current = true
123
+
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
+ } else {
132
+ const nextIndex = activeSlideIndex - 1
133
+ allSlides[nextIndex]?.scrollIntoView({
134
+ behavior: "smooth",
135
+ block: "nearest",
136
+ inline: "start",
137
+ })
138
+ }
139
+
140
+ setTimeout(() => {
141
+ isNavigatingRef.current = false
142
+ }, 600)
143
+ }
144
+
145
+ const goNext = () => {
146
+ if (!trackRef.current) return
147
+ const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
148
+
149
+ isNavigatingRef.current = true
150
+
151
+ if (loop) {
152
+ // En mode loop, on utilise l'index DOM actuel
153
+ allSlides[activeDOMIndex + 1]?.scrollIntoView({
154
+ behavior: "smooth",
155
+ block: "nearest",
156
+ inline: "start",
157
+ })
158
+ } else {
159
+ const totalSlides = allSlides.length
160
+ if (activeSlideIndex >= totalSlides - 1) return
161
+ if (isAtEnd) return
162
+
163
+ const nextIndex = activeSlideIndex + 1
164
+ allSlides[nextIndex]?.scrollIntoView({
165
+ behavior: "smooth",
166
+ block: "nearest",
167
+ inline: "start",
168
+ })
169
+ }
170
+
171
+ setTimeout(() => {
172
+ isNavigatingRef.current = false
173
+ }, 600)
174
+ }
175
+
176
+ const goTo = (nextIndex: number) => {
177
+ if (!trackRef.current) return
178
+ const slides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
179
+ slides[nextIndex]?.scrollIntoView({
180
+ behavior: "smooth",
181
+ block: "nearest",
182
+ inline: "start",
183
+ })
184
+ }
29
185
 
30
186
  const classes = ["ga-ds-carousel"]
31
187
 
@@ -37,68 +193,103 @@ export const Carousel = (props: CarouselProps) => {
37
193
  }
38
194
  }
39
195
 
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
196
  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
- }}
197
+ <div className={classes.join(" ")} style={style}>
198
+ {navigation?.positionY === "top" && displayNavigation && (
199
+ <CarouselNavigation goPrev={goPrev} goNext={goNext} isAtStart={isAtStart} isAtEnd={isAtEnd} />
200
+ )}
201
+ <div
78
202
  className="ga-ds-carousel__track"
203
+ ref={trackRef}
204
+ style={
205
+ {
206
+ "--ga-ds-space-between": `${spaceBetween / 10}rem`,
207
+ } as React.CSSProperties
208
+ }
79
209
  >
80
210
  {context === "wp-editor"
81
211
  ? children
82
- : slides.map((child, index) => (
83
- <SwiperSlide key={index} className="ga-ds-carousel__slide">
84
- {child}
85
- </SwiperSlide>
86
- ))}
87
- </Swiper>
212
+ : slides.map((child, index) => {
213
+ return (
214
+ <div
215
+ key={index}
216
+ className={`ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`}
217
+ >
218
+ {child}
219
+ </div>
220
+ )
221
+ })}
222
+ </div>
88
223
 
89
224
  <div className="ga-ds-carousel__navigation">
90
- {displayPagination && <div className="ga-ds-carousel__dots"></div>}
91
- {displayNavigation && navigation?.positionY === "bottom" && <CarouselNavigation />}
225
+ {pagination && displayPagination && (
226
+ <CarouselPagination
227
+ totalPages={totalPages}
228
+ activeSlideIndex={activeSlideIndex}
229
+ goTo={goTo}
230
+ clickable={pagination.clickable}
231
+ />
232
+ )}
233
+ {displayNavigation && navigation?.positionY === "bottom" && (
234
+ <CarouselNavigation goPrev={goPrev} goNext={goNext} isAtStart={isAtStart} isAtEnd={isAtEnd} />
235
+ )}
92
236
  </div>
93
237
  </div>
94
238
  )
95
239
  }
96
240
 
97
- const CarouselNavigation = () => {
241
+ const CarouselNavigation = ({
242
+ goPrev,
243
+ goNext,
244
+ isAtStart,
245
+ isAtEnd,
246
+ }: {
247
+ goPrev: () => void
248
+ goNext: () => void
249
+ isAtStart: boolean
250
+ isAtEnd: boolean
251
+ }) => {
98
252
  return (
99
253
  <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" />
254
+ <Button
255
+ extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--prev"]}
256
+ icon="chevron-left"
257
+ onClick={goPrev}
258
+ disabled={isAtStart}
259
+ />
260
+ <Button
261
+ extraClassNames={["ga-ds-carousel__button", "ga-ds-carousel__button--next"]}
262
+ icon="chevron-right"
263
+ onClick={goNext}
264
+ disabled={isAtEnd}
265
+ />
266
+ </div>
267
+ )
268
+ }
269
+
270
+ const CarouselPagination = ({
271
+ totalPages,
272
+ activeSlideIndex,
273
+ goTo,
274
+ clickable,
275
+ }: {
276
+ totalPages: number
277
+ activeSlideIndex: number
278
+ goTo: (nextIndex: number) => void
279
+ clickable: boolean
280
+ }) => {
281
+ return (
282
+ <div className="ga-ds-carousel__dots">
283
+ {Array.from({ length: totalPages }, (_, index) => index).map((_, index) => (
284
+ <span
285
+ key={index}
286
+ className={`ga-ds-carousel__dot ${index === activeSlideIndex ? "ga-ds-carousel__dot--active" : ""}`}
287
+ onClick={() => clickable && goTo(index)}
288
+ style={{
289
+ cursor: clickable ? "pointer" : "default",
290
+ }}
291
+ ></span>
292
+ ))}
102
293
  </div>
103
294
  )
104
295
  }