@growth-angels/ds-core 1.3.5 → 1.5.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.
- package/dist/hooks/useBreakPointObserver.d.ts +2 -1
- package/dist/hooks/useBreakPointObserver.js +7 -3
- package/dist/organisms/Carousel/Carousel.js +111 -30
- package/dist/organisms/Carousel/Carousel.stories.d.ts +1 -0
- package/dist/organisms/Carousel/Carousel.stories.js +17 -1
- package/dist/organisms/Carousel/Carousel.types.d.ts +4 -2
- package/dist/organisms/Tabs/Tabs.js +1 -6
- package/dist/organisms/organisms.d.ts +1 -0
- package/dist/organisms/organisms.js +1 -0
- package/package.json +3 -2
- package/src/atoms/Button/Button-mixins.scss +29 -0
- package/src/atoms/Button/Button.scss +12 -26
- package/src/atoms/atoms-mixins.scss +1 -0
- package/src/hooks/useBreakPointObserver.ts +7 -5
- package/src/mixins.scss +1 -0
- package/src/organisms/Carousel/Carousel.stories.tsx +18 -1
- package/src/organisms/Carousel/Carousel.tsx +121 -35
- package/src/organisms/Carousel/Carousel.types.ts +4 -2
- package/src/organisms/Tabs/Tabs.tsx +2 -7
- package/src/organisms/organisms.ts +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useReactAdapter } from './useReactAdaptater';
|
|
2
|
-
export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280 }) {
|
|
2
|
+
export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
|
|
3
3
|
const { useEffect, useState } = useReactAdapter();
|
|
4
4
|
const [current, setCurrent] = useState('sm');
|
|
5
5
|
useEffect(() => {
|
|
@@ -7,10 +7,14 @@ export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 128
|
|
|
7
7
|
const check = () => {
|
|
8
8
|
const width = window.innerWidth;
|
|
9
9
|
let bp = 'sm';
|
|
10
|
-
if (width >= breakpoints.
|
|
10
|
+
if (width >= breakpoints.xl)
|
|
11
|
+
bp = 'xl';
|
|
12
|
+
else if (width >= breakpoints.lg)
|
|
11
13
|
bp = 'lg';
|
|
12
|
-
else if (width >= breakpoints.
|
|
14
|
+
else if (width >= breakpoints.md)
|
|
13
15
|
bp = 'md';
|
|
16
|
+
else if (width >= breakpoints.sm)
|
|
17
|
+
bp = 'sm';
|
|
14
18
|
setCurrent((prev) => (prev !== bp ? bp : prev));
|
|
15
19
|
frameId = requestAnimationFrame(check);
|
|
16
20
|
};
|
|
@@ -3,56 +3,131 @@ import { Button } from "../../atoms/atoms";
|
|
|
3
3
|
import { useBreakpointObserver } from "../../hooks/useBreakPointObserver";
|
|
4
4
|
import { useReactAdapter } from "../../hooks/useReactAdaptater";
|
|
5
5
|
export const Carousel = (props) => {
|
|
6
|
-
const { children, slidesPerView = { sm: 1, md: 2, lg: 3 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, } = props;
|
|
6
|
+
const { children, slidesPerView = { sm: 1, md: 2, lg: 3, xl: 4 }, spaceBetween = 20, navigation, pagination, context, hasPagination, hasNavigation, loop = false, } = props;
|
|
7
7
|
const { useEffect, useState, useRef, Children } = useReactAdapter();
|
|
8
8
|
const trackRef = useRef(null);
|
|
9
|
+
const isNavigatingRef = useRef(false);
|
|
9
10
|
const slides = Children.toArray(children);
|
|
10
11
|
const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
|
|
11
12
|
return Math.max(1, Math.ceil((totalSlides - slidesPerView) / step) + 1);
|
|
12
13
|
};
|
|
13
14
|
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
|
|
15
|
+
const [activeDOMIndex, setActiveDOMIndex] = useState(0);
|
|
14
16
|
const [isAtEnd, setIsAtEnd] = useState(false);
|
|
15
17
|
const [isAtStart, setIsAtStart] = useState(true);
|
|
16
18
|
const [totalPages, setTotalPages] = useState(0);
|
|
17
19
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
if (loop) {
|
|
21
|
+
setIsAtStart(false);
|
|
22
|
+
setIsAtEnd(false);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
setIsAtStart(activeSlideIndex === 0);
|
|
26
|
+
setIsAtEnd(activeSlideIndex === totalPages - 1);
|
|
27
|
+
}
|
|
28
|
+
}, [activeSlideIndex, totalPages, loop]);
|
|
29
|
+
const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200, xl: 1400 });
|
|
22
30
|
useEffect(() => {
|
|
23
31
|
setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1));
|
|
24
32
|
}, [breakpoint, slidesPerView]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const track = trackRef.current;
|
|
35
|
+
if (!track)
|
|
36
|
+
return;
|
|
37
|
+
const handleScroll = () => {
|
|
38
|
+
if (loop) {
|
|
39
|
+
const slideWidth = track.scrollWidth / (slides.length * 3);
|
|
40
|
+
const scrollLeft = track.scrollLeft;
|
|
41
|
+
// Repositionnement infini (uniquement si pas en navigation)
|
|
42
|
+
if (!isNavigatingRef.current) {
|
|
43
|
+
if (scrollLeft <= slideWidth * slides.length) {
|
|
44
|
+
track.scrollLeft = scrollLeft + slideWidth * slides.length;
|
|
45
|
+
}
|
|
46
|
+
else if (scrollLeft >= slideWidth * slides.length * 2) {
|
|
47
|
+
track.scrollLeft = scrollLeft - slideWidth * slides.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2;
|
|
51
|
+
const domIndex = Math.floor(scrollPosition / slideWidth);
|
|
52
|
+
const index = domIndex % slides.length;
|
|
53
|
+
setActiveDOMIndex(domIndex);
|
|
54
|
+
setActiveSlideIndex(index);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const slideWidth = track.scrollWidth / slides.length;
|
|
58
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2;
|
|
59
|
+
const index = Math.floor(scrollPosition / slideWidth);
|
|
60
|
+
setActiveSlideIndex(Math.min(index, slides.length - 1));
|
|
61
|
+
setActiveDOMIndex(index);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
track.addEventListener("scroll", handleScroll);
|
|
65
|
+
// Position initiale au milieu en mode loop
|
|
66
|
+
if (loop) {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
const slideWidth = track.scrollWidth / (slides.length * 3);
|
|
69
|
+
track.scrollLeft = slideWidth * slides.length;
|
|
70
|
+
}, 0);
|
|
71
|
+
}
|
|
72
|
+
return () => track.removeEventListener("scroll", handleScroll);
|
|
73
|
+
}, [slides.length, loop]);
|
|
25
74
|
const style = Object.fromEntries(Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`]));
|
|
26
75
|
const goPrev = () => {
|
|
27
76
|
if (!trackRef.current)
|
|
28
77
|
return;
|
|
29
|
-
const
|
|
30
|
-
if (activeSlideIndex === 0)
|
|
78
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
|
|
79
|
+
if (activeSlideIndex === 0 && !loop)
|
|
31
80
|
return;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
isNavigatingRef.current = true;
|
|
82
|
+
if (loop) {
|
|
83
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
84
|
+
allSlides[activeDOMIndex - 1]?.scrollIntoView({
|
|
85
|
+
behavior: "smooth",
|
|
86
|
+
block: "nearest",
|
|
87
|
+
inline: "start",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const nextIndex = activeSlideIndex - 1;
|
|
92
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
93
|
+
behavior: "smooth",
|
|
94
|
+
block: "nearest",
|
|
95
|
+
inline: "start",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
isNavigatingRef.current = false;
|
|
100
|
+
}, 600);
|
|
39
101
|
};
|
|
40
102
|
const goNext = () => {
|
|
41
103
|
if (!trackRef.current)
|
|
42
104
|
return;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide");
|
|
106
|
+
isNavigatingRef.current = true;
|
|
107
|
+
if (loop) {
|
|
108
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
109
|
+
allSlides[activeDOMIndex + 1]?.scrollIntoView({
|
|
110
|
+
behavior: "smooth",
|
|
111
|
+
block: "nearest",
|
|
112
|
+
inline: "start",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const totalSlides = allSlides.length;
|
|
117
|
+
if (activeSlideIndex >= totalSlides - 1)
|
|
118
|
+
return;
|
|
119
|
+
if (isAtEnd)
|
|
120
|
+
return;
|
|
121
|
+
const nextIndex = activeSlideIndex + 1;
|
|
122
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
123
|
+
behavior: "smooth",
|
|
124
|
+
block: "nearest",
|
|
125
|
+
inline: "start",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
isNavigatingRef.current = false;
|
|
130
|
+
}, 600);
|
|
56
131
|
};
|
|
57
132
|
const goTo = (nextIndex) => {
|
|
58
133
|
if (!trackRef.current)
|
|
@@ -63,17 +138,23 @@ export const Carousel = (props) => {
|
|
|
63
138
|
block: "nearest",
|
|
64
139
|
inline: "start",
|
|
65
140
|
});
|
|
66
|
-
setActiveSlideIndex(nextIndex);
|
|
67
141
|
};
|
|
68
142
|
const classes = ["ga-ds-carousel"];
|
|
69
143
|
if (props.extraClassNames) {
|
|
70
|
-
|
|
144
|
+
if (typeof props.extraClassNames === "string") {
|
|
145
|
+
classes.push(props.extraClassNames);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
classes.push(...props.extraClassNames);
|
|
149
|
+
}
|
|
71
150
|
}
|
|
72
151
|
return (_jsxs("div", { className: classes.join(" "), style: style, children: [navigation?.positionY === "top" && hasNavigation && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd })), _jsx("div", { className: "ga-ds-carousel__track", ref: trackRef, style: {
|
|
73
152
|
"--ga-ds-space-between": `${spaceBetween / 10}rem`,
|
|
74
153
|
}, children: context === "wp-editor"
|
|
75
154
|
? children
|
|
76
|
-
: slides.map((child, index) =>
|
|
155
|
+
: slides.map((child, index) => {
|
|
156
|
+
return (_jsx("div", { className: `ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`, children: child }, index));
|
|
157
|
+
}) }), _jsxs("div", { className: "ga-ds-carousel__navigation", children: [pagination && hasPagination && (_jsx(CarouselPagination, { totalPages: totalPages, activeSlideIndex: activeSlideIndex, goTo: goTo, clickable: pagination.clickable })), hasNavigation && navigation?.positionY === "bottom" && (_jsx(CarouselNavigation, { goPrev: goPrev, goNext: goNext, isAtStart: isAtStart, isAtEnd: isAtEnd }))] })] }));
|
|
77
158
|
};
|
|
78
159
|
const CarouselNavigation = ({ goPrev, goNext, isAtStart, isAtEnd, }) => {
|
|
79
160
|
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 })] }));
|
|
@@ -17,7 +17,7 @@ export const Primary = {
|
|
|
17
17
|
_jsx("div", { children: "Slide 6" }),
|
|
18
18
|
_jsx("div", { children: "Slide 7" }),
|
|
19
19
|
],
|
|
20
|
-
slidesPerView: { sm: 1, md: 2, lg: 3 },
|
|
20
|
+
slidesPerView: { sm: 1, md: 2, lg: 3, xl: 4 },
|
|
21
21
|
spaceBetween: 16,
|
|
22
22
|
pagination: {
|
|
23
23
|
clickable: true,
|
|
@@ -29,3 +29,19 @@ export const Primary = {
|
|
|
29
29
|
hasPagination: true,
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
|
+
export const LoopingCarousel = {
|
|
33
|
+
args: {
|
|
34
|
+
children: [_jsx("div", { children: "Slide A" }), _jsx("div", { children: "Slide B" }), _jsx("div", { children: "Slide C" }), _jsx("div", { children: "Slide D" })],
|
|
35
|
+
slidesPerView: { sm: 1, md: 1, lg: 1, xl: 1 },
|
|
36
|
+
spaceBetween: 24,
|
|
37
|
+
loop: true,
|
|
38
|
+
pagination: {
|
|
39
|
+
clickable: true,
|
|
40
|
+
},
|
|
41
|
+
navigation: {
|
|
42
|
+
positionY: "top",
|
|
43
|
+
},
|
|
44
|
+
hasNavigation: true,
|
|
45
|
+
hasPagination: true,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { WordpressDefault } from "../../global.types";
|
|
2
|
-
export type
|
|
2
|
+
export type CarouselAttributes = {
|
|
3
3
|
context?: 'wp-editor';
|
|
4
4
|
slidesPerView?: {
|
|
5
5
|
sm: number;
|
|
6
6
|
md: number;
|
|
7
7
|
lg: number;
|
|
8
|
+
xl: number;
|
|
8
9
|
};
|
|
9
10
|
spaceBetween?: number;
|
|
10
11
|
hasPagination?: boolean;
|
|
11
12
|
hasNavigation?: boolean;
|
|
13
|
+
loop?: boolean;
|
|
12
14
|
pagination?: {
|
|
13
15
|
clickable: boolean;
|
|
14
16
|
};
|
|
@@ -16,6 +18,6 @@ export type Attributes = {
|
|
|
16
18
|
positionY: 'bottom' | 'top';
|
|
17
19
|
};
|
|
18
20
|
};
|
|
19
|
-
export interface CarouselProps extends
|
|
21
|
+
export interface CarouselProps extends CarouselAttributes, WordpressDefault {
|
|
20
22
|
children: React.ReactNode | React.ReactNode[];
|
|
21
23
|
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useReactAdapter } from "../../hooks/useReactAdaptater";
|
|
3
3
|
export const Tabs = ({ tabs, children, extraClassNames, edit }) => {
|
|
4
|
-
const {
|
|
5
|
-
console.log(edit);
|
|
4
|
+
const { useState, useRef, Children } = useReactAdapter();
|
|
6
5
|
const [activeTab, setActiveTab] = useState(tabs[0]?.id || "");
|
|
7
6
|
const tabsRef = useRef([]);
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);
|
|
10
|
-
tabsRef.current[activeIndex]?.focus();
|
|
11
|
-
}, [activeTab, tabs]);
|
|
12
7
|
const classes = ["ga-ds-tabs"];
|
|
13
8
|
if (extraClassNames) {
|
|
14
9
|
classes.push(...extraClassNames);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-angels/ds-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Design system by Growth Angels",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts"
|
|
18
18
|
},
|
|
19
19
|
"./styles": "./src/styles.ts",
|
|
20
|
-
"./styles/scss": "./src/index.scss"
|
|
20
|
+
"./styles/scss": "./src/index.scss",
|
|
21
|
+
"./mixins": "./src/mixins.scss"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@mixin button {
|
|
2
|
+
padding: var(--ga-button-padding);
|
|
3
|
+
border: none;
|
|
4
|
+
font-size: var(--ga-font-sizes-base);
|
|
5
|
+
font-weight: var(--ga-font-weight-base);
|
|
6
|
+
font-family: var(--ga-font-family-button);
|
|
7
|
+
border-radius: var(--ga-button-radius, 0);
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@mixin button--primary {
|
|
12
|
+
background-color: var(--ga-button-background-primary, #eee);
|
|
13
|
+
color: var(--ga-button-color-primary, #000);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@mixin button--secondary {
|
|
17
|
+
background-color: var(--ga-button-background-secondary, #424242);
|
|
18
|
+
color: var(--ga-button-color-secondary, #fff);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@mixin button--link {
|
|
22
|
+
color: var(--ga-button-color-link, purple);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@mixin button--icon {
|
|
26
|
+
--ga-button-padding: 0.8rem 1.2rem;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
}
|
|
@@ -1,35 +1,21 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
font-size: var(--ga-font-sizes-base);
|
|
6
|
-
font-weight: var(--ga-font-weight-base);
|
|
7
|
-
font-family: var(--ga-font-family-button);
|
|
8
|
-
border-radius: var(--ga-button-radius, 0);
|
|
9
|
-
cursor: pointer;
|
|
1
|
+
@use "./Button-mixins.scss" as *;
|
|
2
|
+
|
|
3
|
+
.ga-ds-button {
|
|
4
|
+
@include button;
|
|
10
5
|
}
|
|
11
6
|
|
|
12
|
-
.ga-ds-button--primary
|
|
13
|
-
button
|
|
14
|
-
background-color: var(--ga-button-background-primary, #eee);
|
|
15
|
-
color: var(--ga-button-color-primary, #000);
|
|
7
|
+
.ga-ds-button--primary {
|
|
8
|
+
@include button--primary;
|
|
16
9
|
}
|
|
17
10
|
|
|
18
|
-
.ga-ds-button--secondary
|
|
19
|
-
button
|
|
20
|
-
background-color: var(--ga-button-background-secondary, #424242);
|
|
21
|
-
color: var(--ga-button-color-secondary, #fff);
|
|
11
|
+
.ga-ds-button--secondary {
|
|
12
|
+
@include button--secondary;
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
.ga-ds-button--link
|
|
25
|
-
button
|
|
26
|
-
color: var(--ga-button-color-link, purple);
|
|
27
|
-
padding: 0;
|
|
15
|
+
.ga-ds-button--link {
|
|
16
|
+
@include button--link;
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
.ga-ds-button--icon
|
|
31
|
-
button
|
|
32
|
-
--ga-button-padding: 0.8rem 1.2rem;
|
|
33
|
-
justify-content: center;
|
|
34
|
-
align-items: center;
|
|
19
|
+
.ga-ds-button--icon {
|
|
20
|
+
@include button--icon;
|
|
35
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@forward "./Button/Button-mixins.scss";
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { useReactAdapter } from './useReactAdaptater'
|
|
2
2
|
|
|
3
|
-
export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280 }) {
|
|
3
|
+
export function useBreakpointObserver(breakpoints = { sm: 640, md: 1024, lg: 1280, xl: 1400 }) {
|
|
4
4
|
const { useEffect, useState } = useReactAdapter()
|
|
5
|
-
const [current, setCurrent] = useState<'sm' | 'md' | 'lg'>('sm')
|
|
5
|
+
const [current, setCurrent] = useState<'sm' | 'md' | 'lg' | 'xl'>('sm')
|
|
6
6
|
useEffect(() => {
|
|
7
7
|
let frameId: number
|
|
8
8
|
|
|
9
9
|
const check = () => {
|
|
10
10
|
const width = window.innerWidth
|
|
11
|
-
let bp: 'sm' | 'md' | 'lg' = 'sm'
|
|
11
|
+
let bp: 'sm' | 'md' | 'lg' | 'xl' = 'sm'
|
|
12
12
|
|
|
13
|
-
if (width >= breakpoints.
|
|
14
|
-
else if (width >= breakpoints.
|
|
13
|
+
if (width >= breakpoints.xl) bp = 'xl'
|
|
14
|
+
else if (width >= breakpoints.lg) bp = 'lg'
|
|
15
|
+
else if (width >= breakpoints.md) bp = 'md'
|
|
16
|
+
else if (width >= breakpoints.sm) bp = 'sm'
|
|
15
17
|
|
|
16
18
|
setCurrent((prev) => (prev !== bp ? bp : prev))
|
|
17
19
|
frameId = requestAnimationFrame(check)
|
package/src/mixins.scss
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@forward "./atoms/atoms-mixins.scss";
|
|
@@ -21,7 +21,7 @@ export const Primary: Story = {
|
|
|
21
21
|
<div>Slide 6</div>,
|
|
22
22
|
<div>Slide 7</div>,
|
|
23
23
|
],
|
|
24
|
-
slidesPerView: { sm: 1, md: 2, lg: 3 },
|
|
24
|
+
slidesPerView: { sm: 1, md: 2, lg: 3, xl: 4 },
|
|
25
25
|
spaceBetween: 16,
|
|
26
26
|
pagination: {
|
|
27
27
|
clickable: true,
|
|
@@ -33,3 +33,20 @@ export const Primary: Story = {
|
|
|
33
33
|
hasPagination: true,
|
|
34
34
|
},
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export const LoopingCarousel: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
children: [<div>Slide A</div>, <div>Slide B</div>, <div>Slide C</div>, <div>Slide D</div>],
|
|
40
|
+
slidesPerView: { sm: 1, md: 1, lg: 1, xl: 1 },
|
|
41
|
+
spaceBetween: 24,
|
|
42
|
+
loop: true,
|
|
43
|
+
pagination: {
|
|
44
|
+
clickable: true,
|
|
45
|
+
},
|
|
46
|
+
navigation: {
|
|
47
|
+
positionY: "top",
|
|
48
|
+
},
|
|
49
|
+
hasNavigation: true,
|
|
50
|
+
hasPagination: true,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -6,16 +6,18 @@ import { CarouselProps } from "./Carousel.types"
|
|
|
6
6
|
export const Carousel = (props: CarouselProps) => {
|
|
7
7
|
const {
|
|
8
8
|
children,
|
|
9
|
-
slidesPerView = { sm: 1, md: 2, lg: 3 },
|
|
9
|
+
slidesPerView = { sm: 1, md: 2, lg: 3, xl: 4 },
|
|
10
10
|
spaceBetween = 20,
|
|
11
11
|
navigation,
|
|
12
12
|
pagination,
|
|
13
13
|
context,
|
|
14
14
|
hasPagination,
|
|
15
15
|
hasNavigation,
|
|
16
|
+
loop = false,
|
|
16
17
|
} = props
|
|
17
18
|
const { useEffect, useState, useRef, Children } = useReactAdapter()
|
|
18
19
|
const trackRef = useRef<HTMLDivElement>(null)
|
|
20
|
+
const isNavigatingRef = useRef(false)
|
|
19
21
|
const slides = Children.toArray(children)
|
|
20
22
|
|
|
21
23
|
const calculatePages = (totalSlides = 0, slidesPerView = 0, step = 1) => {
|
|
@@ -23,54 +25,133 @@ export const Carousel = (props: CarouselProps) => {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const [activeSlideIndex, setActiveSlideIndex] = useState(0)
|
|
28
|
+
const [activeDOMIndex, setActiveDOMIndex] = useState(0)
|
|
26
29
|
const [isAtEnd, setIsAtEnd] = useState(false)
|
|
27
30
|
const [isAtStart, setIsAtStart] = useState(true)
|
|
28
31
|
const [totalPages, setTotalPages] = useState(0)
|
|
29
32
|
|
|
30
33
|
useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
if (loop) {
|
|
35
|
+
setIsAtStart(false)
|
|
36
|
+
setIsAtEnd(false)
|
|
37
|
+
} else {
|
|
38
|
+
setIsAtStart(activeSlideIndex === 0)
|
|
39
|
+
setIsAtEnd(activeSlideIndex === totalPages - 1)
|
|
40
|
+
}
|
|
41
|
+
}, [activeSlideIndex, totalPages, loop])
|
|
34
42
|
|
|
35
|
-
const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200 })
|
|
43
|
+
const breakpoint = useBreakpointObserver({ sm: 768, md: 992, lg: 1200, xl: 1400 })
|
|
36
44
|
|
|
37
45
|
useEffect(() => {
|
|
38
46
|
setTotalPages(calculatePages(slides.length, slidesPerView[breakpoint], 1))
|
|
39
47
|
}, [breakpoint, slidesPerView])
|
|
40
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const track = trackRef.current
|
|
51
|
+
if (!track) return
|
|
52
|
+
|
|
53
|
+
const handleScroll = () => {
|
|
54
|
+
if (loop) {
|
|
55
|
+
const slideWidth = track.scrollWidth / (slides.length * 3)
|
|
56
|
+
const scrollLeft = track.scrollLeft
|
|
57
|
+
|
|
58
|
+
// Repositionnement infini (uniquement si pas en navigation)
|
|
59
|
+
if (!isNavigatingRef.current) {
|
|
60
|
+
if (scrollLeft <= slideWidth * slides.length) {
|
|
61
|
+
track.scrollLeft = scrollLeft + slideWidth * slides.length
|
|
62
|
+
} else if (scrollLeft >= slideWidth * slides.length * 2) {
|
|
63
|
+
track.scrollLeft = scrollLeft - slideWidth * slides.length
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2
|
|
68
|
+
const domIndex = Math.floor(scrollPosition / slideWidth)
|
|
69
|
+
const index = domIndex % slides.length
|
|
70
|
+
setActiveDOMIndex(domIndex)
|
|
71
|
+
setActiveSlideIndex(index)
|
|
72
|
+
} else {
|
|
73
|
+
const slideWidth = track.scrollWidth / slides.length
|
|
74
|
+
const scrollPosition = track.scrollLeft + slideWidth / 2
|
|
75
|
+
const index = Math.floor(scrollPosition / slideWidth)
|
|
76
|
+
setActiveSlideIndex(Math.min(index, slides.length - 1))
|
|
77
|
+
setActiveDOMIndex(index)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
track.addEventListener("scroll", handleScroll)
|
|
82
|
+
|
|
83
|
+
// Position initiale au milieu en mode loop
|
|
84
|
+
if (loop) {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
const slideWidth = track.scrollWidth / (slides.length * 3)
|
|
87
|
+
track.scrollLeft = slideWidth * slides.length
|
|
88
|
+
}, 0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return () => track.removeEventListener("scroll", handleScroll)
|
|
92
|
+
}, [slides.length, loop])
|
|
93
|
+
|
|
41
94
|
const style = Object.fromEntries(
|
|
42
95
|
Object.entries(slidesPerView).map(([key, value]) => [`--ga-ds-slides-per-view-${key}`, `${value}`])
|
|
43
96
|
)
|
|
44
97
|
|
|
45
98
|
const goPrev = () => {
|
|
46
99
|
if (!trackRef.current) return
|
|
47
|
-
const
|
|
48
|
-
if (activeSlideIndex === 0) return
|
|
100
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
|
|
101
|
+
if (activeSlideIndex === 0 && !loop) return
|
|
49
102
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
103
|
+
isNavigatingRef.current = true
|
|
104
|
+
|
|
105
|
+
if (loop) {
|
|
106
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
107
|
+
allSlides[activeDOMIndex - 1]?.scrollIntoView({
|
|
108
|
+
behavior: "smooth",
|
|
109
|
+
block: "nearest",
|
|
110
|
+
inline: "start",
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
const nextIndex = activeSlideIndex - 1
|
|
114
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
115
|
+
behavior: "smooth",
|
|
116
|
+
block: "nearest",
|
|
117
|
+
inline: "start",
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
isNavigatingRef.current = false
|
|
123
|
+
}, 600)
|
|
57
124
|
}
|
|
58
125
|
|
|
59
126
|
const goNext = () => {
|
|
60
127
|
if (!trackRef.current) return
|
|
61
|
-
const
|
|
62
|
-
const totalSlides = slides.length
|
|
128
|
+
const allSlides = trackRef.current.querySelectorAll(".ga-ds-carousel__slide")
|
|
63
129
|
|
|
64
|
-
|
|
65
|
-
if (isAtEnd) return
|
|
130
|
+
isNavigatingRef.current = true
|
|
66
131
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
132
|
+
if (loop) {
|
|
133
|
+
// En mode loop, on utilise l'index DOM actuel
|
|
134
|
+
allSlides[activeDOMIndex + 1]?.scrollIntoView({
|
|
135
|
+
behavior: "smooth",
|
|
136
|
+
block: "nearest",
|
|
137
|
+
inline: "start",
|
|
138
|
+
})
|
|
139
|
+
} else {
|
|
140
|
+
const totalSlides = allSlides.length
|
|
141
|
+
if (activeSlideIndex >= totalSlides - 1) return
|
|
142
|
+
if (isAtEnd) return
|
|
143
|
+
|
|
144
|
+
const nextIndex = activeSlideIndex + 1
|
|
145
|
+
allSlides[nextIndex]?.scrollIntoView({
|
|
146
|
+
behavior: "smooth",
|
|
147
|
+
block: "nearest",
|
|
148
|
+
inline: "start",
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
isNavigatingRef.current = false
|
|
154
|
+
}, 600)
|
|
74
155
|
}
|
|
75
156
|
|
|
76
157
|
const goTo = (nextIndex: number) => {
|
|
@@ -81,13 +162,16 @@ export const Carousel = (props: CarouselProps) => {
|
|
|
81
162
|
block: "nearest",
|
|
82
163
|
inline: "start",
|
|
83
164
|
})
|
|
84
|
-
setActiveSlideIndex(nextIndex)
|
|
85
165
|
}
|
|
86
166
|
|
|
87
167
|
const classes = ["ga-ds-carousel"]
|
|
88
168
|
|
|
89
169
|
if (props.extraClassNames) {
|
|
90
|
-
|
|
170
|
+
if (typeof props.extraClassNames === "string") {
|
|
171
|
+
classes.push(props.extraClassNames)
|
|
172
|
+
} else {
|
|
173
|
+
classes.push(...props.extraClassNames)
|
|
174
|
+
}
|
|
91
175
|
}
|
|
92
176
|
|
|
93
177
|
return (
|
|
@@ -106,14 +190,16 @@ export const Carousel = (props: CarouselProps) => {
|
|
|
106
190
|
>
|
|
107
191
|
{context === "wp-editor"
|
|
108
192
|
? children
|
|
109
|
-
: slides.map((child, index) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
193
|
+
: slides.map((child, index) => {
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
key={index}
|
|
197
|
+
className={`ga-ds-carousel__slide ${index === activeDOMIndex ? "ga-ds-carousel__slide--active" : ""}`}
|
|
198
|
+
>
|
|
199
|
+
{child}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
})}
|
|
117
203
|
</div>
|
|
118
204
|
|
|
119
205
|
<div className="ga-ds-carousel__navigation">
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { WordpressDefault } from "../../global.types"
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type CarouselAttributes = {
|
|
4
4
|
context?: 'wp-editor'
|
|
5
5
|
slidesPerView?: {
|
|
6
6
|
sm: number
|
|
7
7
|
md: number
|
|
8
8
|
lg: number
|
|
9
|
+
xl: number
|
|
9
10
|
}
|
|
10
11
|
spaceBetween?: number
|
|
11
12
|
hasPagination?: boolean
|
|
12
13
|
hasNavigation?: boolean
|
|
14
|
+
loop?: boolean
|
|
13
15
|
pagination?: {
|
|
14
16
|
clickable: boolean
|
|
15
17
|
},
|
|
@@ -17,6 +19,6 @@ export type Attributes = {
|
|
|
17
19
|
positionY: 'bottom' | 'top';
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
export interface CarouselProps extends
|
|
22
|
+
export interface CarouselProps extends CarouselAttributes, WordpressDefault {
|
|
21
23
|
children: React.ReactNode | React.ReactNode[]
|
|
22
24
|
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { useReactAdapter } from "../../hooks/useReactAdaptater"
|
|
2
2
|
import { TabsProps } from "./Tabs.types"
|
|
3
3
|
export const Tabs = ({ tabs, children, extraClassNames, edit }: TabsProps) => {
|
|
4
|
-
const {
|
|
5
|
-
|
|
4
|
+
const { useState, useRef, Children } = useReactAdapter()
|
|
5
|
+
|
|
6
6
|
const [activeTab, setActiveTab] = useState(tabs[0]?.id || "")
|
|
7
7
|
const tabsRef = useRef<Array<HTMLButtonElement | null>>([])
|
|
8
8
|
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
const activeIndex = tabs.findIndex((tab) => tab.id === activeTab)
|
|
11
|
-
tabsRef.current[activeIndex]?.focus()
|
|
12
|
-
}, [activeTab, tabs])
|
|
13
|
-
|
|
14
9
|
const classes = ["ga-ds-tabs"]
|
|
15
10
|
if (extraClassNames) {
|
|
16
11
|
classes.push(...extraClassNames)
|