kelt-ui-kit-react 2.0.2 → 2.0.4
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/card/Card.d.ts +4 -1
- package/dist/expands/Expands.d.ts +1 -0
- package/dist/expands/expand/expand.d.ts +2 -1
- package/dist/form/Form.d.ts +6 -0
- package/dist/form/form.interface.d.ts +5 -3
- package/dist/grid/Grid.d.ts +5 -2
- package/dist/grid/col/colStyled/ColStyled.d.ts +2 -1
- package/dist/grid/grid.interface.d.ts +12 -1
- package/dist/grid/row/Row.d.ts +7 -0
- package/dist/index.js +3043 -2884
- package/dist/overlayPanel/OverlayPanel.d.ts +4 -2
- package/dist/overlayPanel/overlayPanelStyled/OverlayPanelStyled.d.ts +2 -0
- package/dist/select/Select.d.ts +6 -1
- package/dist/select/selectOption.interface.d.ts +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/card/Card.tsx +37 -11
- package/src/card/card.css +2 -3
- package/src/expands/Expands.tsx +2 -0
- package/src/expands/expand/expand.tsx +7 -1
- package/src/form/Form.tsx +42 -12
- package/src/form/form.css +15 -2
- package/src/form/form.interface.tsx +5 -3
- package/src/form/input/input.css +6 -2
- package/src/grid/Grid.tsx +34 -21
- package/src/grid/col/Col.tsx +24 -13
- package/src/grid/col/col.css +3 -0
- package/src/grid/col/colStyled/ColStyled.tsx +59 -55
- package/src/grid/container/container.css +2 -2
- package/src/grid/grid.interface.tsx +15 -1
- package/src/grid/row/Row.tsx +49 -12
- package/src/grid/row/row.css +9 -1
- package/src/overlayPanel/OverlayPanel.tsx +71 -18
- package/src/overlayPanel/overlay.context.tsx +6 -4
- package/src/overlayPanel/overlayPanel.css +10 -1
- package/src/overlayPanel/overlayPanelStyled/OverlayPanelStyled.tsx +7 -2
- package/src/select/Select.tsx +178 -39
- package/src/select/select.css +94 -13
- package/src/select/selectOption.interface.ts +1 -0
package/src/grid/row/Row.tsx
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { GridResponsive } from "../grid.interface";
|
|
3
|
+
import "./row.css";
|
|
4
|
+
|
|
5
|
+
export type GridGap = boolean | number | string;
|
|
6
|
+
|
|
7
|
+
export const RowResponsiveContext = createContext<GridResponsive>("app");
|
|
8
|
+
|
|
9
|
+
type RowProps = {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
scroll?: boolean;
|
|
13
|
+
/** true (défaut) : gap par défaut, false : pas de gap, number : px, string : valeur CSS ("1rem") */
|
|
14
|
+
gap?: GridGap;
|
|
15
|
+
/** Largeur sur laquelle se basent les breakpoints des Col (défaut : "app") */
|
|
16
|
+
responsive?: GridResponsive;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getGutter = (gap: GridGap) => {
|
|
20
|
+
if (gap === true) return undefined; // garde la valeur par défaut du CSS
|
|
21
|
+
if (gap === false) return "0px";
|
|
22
|
+
return typeof gap === "number" ? `${gap}px` : gap;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Row: React.FC<RowProps> = ({
|
|
26
|
+
children,
|
|
27
|
+
className,
|
|
28
|
+
scroll,
|
|
29
|
+
gap = true,
|
|
30
|
+
responsive = "app",
|
|
31
|
+
}) => {
|
|
32
|
+
const gutter = getGutter(gap);
|
|
33
|
+
return (
|
|
34
|
+
<RowResponsiveContext.Provider value={responsive}>
|
|
35
|
+
<div
|
|
36
|
+
className={`grid-row ${className ?? ""} ${scroll ? "scroll" : ""} ${
|
|
37
|
+
responsive === "row" ? "grid-row--responsive" : ""
|
|
38
|
+
}`}
|
|
39
|
+
style={
|
|
40
|
+
gutter
|
|
41
|
+
? ({ "--grid-gutter": gutter } as React.CSSProperties)
|
|
42
|
+
: undefined
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
</RowResponsiveContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
};
|
package/src/grid/row/row.css
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
.grid-row {
|
|
2
|
+
--grid-gutter: 15px;
|
|
2
3
|
display: flex;
|
|
3
4
|
flex-wrap: wrap;
|
|
5
|
+
column-gap: var(--grid-gutter);
|
|
6
|
+
|
|
7
|
+
/* responsive="row" : les breakpoints des Col se basent sur la largeur de la Row.
|
|
8
|
+
Attention : la Row a alors une largeur min de contenu de 0 dans un parent flex. */
|
|
9
|
+
&.grid-row--responsive {
|
|
10
|
+
container: kelt-grid / inline-size;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
&.scroll {
|
|
6
14
|
overflow-x: auto;
|
|
@@ -8,7 +16,7 @@
|
|
|
8
16
|
}
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
@container (min-width: 998px) {
|
|
19
|
+
@container app-container (min-width: 998px) {
|
|
12
20
|
.grid-row {
|
|
13
21
|
&.scroll {
|
|
14
22
|
overflow-x: auto;
|
|
@@ -4,6 +4,7 @@ import React, {
|
|
|
4
4
|
useEffect,
|
|
5
5
|
useId,
|
|
6
6
|
useImperativeHandle,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from "react";
|
|
@@ -23,14 +24,16 @@ export type PositionOverlay =
|
|
|
23
24
|
| "bottomRight";
|
|
24
25
|
|
|
25
26
|
export type OverlayPanelProps = {
|
|
26
|
-
referenceElement?: HTMLElement;
|
|
27
|
+
referenceElement?: HTMLElement | HTMLTableElement;
|
|
27
28
|
coordonnees?: { top: number; left: number };
|
|
28
29
|
position?: PositionOverlay;
|
|
29
30
|
children: React.ReactNode;
|
|
30
|
-
className?: string;
|
|
31
31
|
show?: boolean;
|
|
32
32
|
id?: string;
|
|
33
|
+
className?: string;
|
|
33
34
|
closeOverlay?: (close: boolean) => void;
|
|
35
|
+
usePortal?: boolean;
|
|
36
|
+
matchReferenceWidth?: boolean;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
export const OverlayPanel = forwardRef(
|
|
@@ -39,11 +42,13 @@ export const OverlayPanel = forwardRef(
|
|
|
39
42
|
referenceElement,
|
|
40
43
|
position,
|
|
41
44
|
coordonnees,
|
|
42
|
-
className,
|
|
43
45
|
children,
|
|
44
46
|
show,
|
|
47
|
+
className,
|
|
45
48
|
id,
|
|
46
49
|
closeOverlay,
|
|
50
|
+
usePortal = true,
|
|
51
|
+
matchReferenceWidth = false,
|
|
47
52
|
}: OverlayPanelProps,
|
|
48
53
|
ref: React.Ref<{
|
|
49
54
|
setCoords?: (coords: { top: number; left: number }) => void;
|
|
@@ -54,10 +59,16 @@ export const OverlayPanel = forwardRef(
|
|
|
54
59
|
const [coords, setCoords] = useState<{ top: number; left: number }>(
|
|
55
60
|
coordonnees ?? { top: 0, left: 0 },
|
|
56
61
|
);
|
|
62
|
+
const [referenceWidth, setReferenceWidth] = useState<number | undefined>();
|
|
63
|
+
// Masque l'overlay tant que sa position n'a pas été calculée (évite le flash en 0,0)
|
|
64
|
+
const [isPositioned, setIsPositioned] = useState(false);
|
|
57
65
|
const boxRef = useRef<HTMLDivElement>(null);
|
|
58
66
|
const overlayId = useId();
|
|
59
67
|
useImperativeHandle(ref, () => ({
|
|
60
|
-
setCoords
|
|
68
|
+
setCoords: (newCoords: { top: number; left: number }) => {
|
|
69
|
+
setCoords(newCoords);
|
|
70
|
+
setIsPositioned(true);
|
|
71
|
+
},
|
|
61
72
|
}));
|
|
62
73
|
|
|
63
74
|
useEffect(() => {
|
|
@@ -66,10 +77,11 @@ export const OverlayPanel = forwardRef(
|
|
|
66
77
|
}
|
|
67
78
|
}, [activeOverlayId, overlayId]);
|
|
68
79
|
|
|
69
|
-
|
|
80
|
+
useLayoutEffect(() => {
|
|
70
81
|
const updatePosition = () => {
|
|
71
82
|
if (!position && coordonnees) {
|
|
72
83
|
setCoords(coordonnees);
|
|
84
|
+
setIsPositioned(true);
|
|
73
85
|
} else if (referenceElement) {
|
|
74
86
|
const rect = referenceElement.getBoundingClientRect();
|
|
75
87
|
const boxRect = boxRef.current?.getBoundingClientRect();
|
|
@@ -108,6 +120,14 @@ export const OverlayPanel = forwardRef(
|
|
|
108
120
|
if (position && positions[position]) {
|
|
109
121
|
// Si une position est définie, on tente de l'appliquer
|
|
110
122
|
({ top, left } = positions[position]());
|
|
123
|
+
// Manque d'espace en bas → afficher au-dessus de l'élément
|
|
124
|
+
if (
|
|
125
|
+
(position === "bottomLeft" || position === "bottomRight") &&
|
|
126
|
+
top + boxRect.height > innerHeight &&
|
|
127
|
+
rect.top - boxRect.height >= 0
|
|
128
|
+
) {
|
|
129
|
+
top = rect.top - boxRect.height;
|
|
130
|
+
}
|
|
111
131
|
} else {
|
|
112
132
|
// Sinon, on adapte dynamiquement
|
|
113
133
|
left = rect.right;
|
|
@@ -122,18 +142,35 @@ export const OverlayPanel = forwardRef(
|
|
|
122
142
|
}
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
|
|
145
|
+
// Coordonnées viewport → coordonnées document (position absolute dans le body)
|
|
146
|
+
setCoords({
|
|
147
|
+
top: top + window.scrollY,
|
|
148
|
+
left: (left < 0 ? 0 : left) + window.scrollX,
|
|
149
|
+
});
|
|
150
|
+
if (matchReferenceWidth) {
|
|
151
|
+
setReferenceWidth(rect.width);
|
|
152
|
+
}
|
|
153
|
+
setIsPositioned(true);
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
};
|
|
129
157
|
|
|
158
|
+
if (!isShow) {
|
|
159
|
+
setIsPositioned(false);
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
if (isShow) {
|
|
131
163
|
updatePosition();
|
|
132
164
|
window.addEventListener("resize", updatePosition);
|
|
165
|
+
// capture : suit aussi le scroll des conteneurs (ex. contenu d'une modale)
|
|
166
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
133
167
|
}
|
|
134
168
|
|
|
135
|
-
return () =>
|
|
136
|
-
|
|
169
|
+
return () => {
|
|
170
|
+
window.removeEventListener("resize", updatePosition);
|
|
171
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
172
|
+
};
|
|
173
|
+
}, [referenceElement, isShow, position, matchReferenceWidth]);
|
|
137
174
|
|
|
138
175
|
const openOverlay = () => {
|
|
139
176
|
setIsShow(true);
|
|
@@ -154,18 +191,24 @@ export const OverlayPanel = forwardRef(
|
|
|
154
191
|
useEffect(() => {
|
|
155
192
|
const handleClickOutside = (event: MouseEvent) => {
|
|
156
193
|
if (boxRef.current && !boxRef.current.contains(event.target as Node)) {
|
|
157
|
-
|
|
194
|
+
// Pas de stopPropagation ici : le clic doit continuer vers sa cible
|
|
195
|
+
setIsShow(false);
|
|
196
|
+
setActiveOverlay(null);
|
|
197
|
+
if (closeOverlay) {
|
|
198
|
+
closeOverlay(false);
|
|
199
|
+
}
|
|
158
200
|
}
|
|
159
201
|
};
|
|
160
202
|
|
|
161
203
|
if (isShow) {
|
|
162
|
-
|
|
204
|
+
// Phase de capture : fonctionne même si un parent (ex. Modal) stoppe la propagation
|
|
205
|
+
document.addEventListener("mousedown", handleClickOutside, true);
|
|
163
206
|
}
|
|
164
207
|
|
|
165
208
|
return () => {
|
|
166
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
209
|
+
document.removeEventListener("mousedown", handleClickOutside, true);
|
|
167
210
|
};
|
|
168
|
-
}, [isShow,
|
|
211
|
+
}, [isShow, closeOverlay, setActiveOverlay]);
|
|
169
212
|
useEffect(() => {
|
|
170
213
|
if (show) {
|
|
171
214
|
openOverlay();
|
|
@@ -174,15 +217,16 @@ export const OverlayPanel = forwardRef(
|
|
|
174
217
|
|
|
175
218
|
if (!isShow) return null;
|
|
176
219
|
|
|
177
|
-
|
|
220
|
+
const overlayContent = (
|
|
178
221
|
<OverlayPanelStyled
|
|
179
|
-
className={className}
|
|
180
222
|
ref={boxRef}
|
|
181
223
|
$top={coords.top}
|
|
182
224
|
$left={coords.left}
|
|
225
|
+
$minWidth={referenceWidth}
|
|
226
|
+
$visible={isPositioned}
|
|
183
227
|
>
|
|
184
228
|
<div
|
|
185
|
-
className={`overlayPanel ${
|
|
229
|
+
className={`overlayPanel ${className ? className : ""} ${
|
|
186
230
|
coords.top > 0 || coords.left > 0 ? "visible" : "hide"
|
|
187
231
|
}`}
|
|
188
232
|
>
|
|
@@ -193,10 +237,19 @@ export const OverlayPanel = forwardRef(
|
|
|
193
237
|
>
|
|
194
238
|
<Icon classIcon="bi-x-lg" />
|
|
195
239
|
</button>
|
|
196
|
-
{children}
|
|
240
|
+
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
|
197
241
|
</div>
|
|
198
|
-
</OverlayPanelStyled
|
|
199
|
-
document.body,
|
|
242
|
+
</OverlayPanelStyled>
|
|
200
243
|
);
|
|
244
|
+
|
|
245
|
+
if (!usePortal) {
|
|
246
|
+
return (
|
|
247
|
+
<div className={`overlayPanel overlayPanel--no-portal `} ref={boxRef}>
|
|
248
|
+
<div onClick={(e) => e.stopPropagation()}>{children}</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return ReactDOM.createPortal(overlayContent, document.body);
|
|
201
254
|
},
|
|
202
255
|
);
|
|
@@ -7,6 +7,11 @@ interface OverlayContextType {
|
|
|
7
7
|
|
|
8
8
|
const OverlayContext = createContext<OverlayContextType | undefined>(undefined);
|
|
9
9
|
|
|
10
|
+
const defaultOverlayContext: OverlayContextType = {
|
|
11
|
+
activeOverlayId: null,
|
|
12
|
+
setActiveOverlay: () => {},
|
|
13
|
+
};
|
|
14
|
+
|
|
10
15
|
export const OverlayProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
11
16
|
children,
|
|
12
17
|
}) => {
|
|
@@ -21,8 +26,5 @@ export const OverlayProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
|
21
26
|
|
|
22
27
|
export const useOverlayContext = () => {
|
|
23
28
|
const context = useContext(OverlayContext);
|
|
24
|
-
|
|
25
|
-
throw new Error("useOverlayContext must be used within an OverlayProvider");
|
|
26
|
-
}
|
|
27
|
-
return context;
|
|
29
|
+
return context ?? defaultOverlayContext;
|
|
28
30
|
};
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
.overlayPanel {
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
|
+
&.overlayPanel--no-portal {
|
|
5
|
+
position: absolute;
|
|
6
|
+
z-index: 14 !important;
|
|
7
|
+
width: 100%;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
&.hide {
|
|
5
11
|
visibility: hidden;
|
|
6
12
|
}
|
|
13
|
+
|
|
7
14
|
&.visible {
|
|
8
15
|
visibility: visible !important;
|
|
9
16
|
}
|
|
17
|
+
|
|
10
18
|
button {
|
|
11
19
|
span {
|
|
12
20
|
text-align: left;
|
|
13
21
|
}
|
|
14
22
|
}
|
|
23
|
+
|
|
15
24
|
.toggle-btn {
|
|
16
25
|
display: flex;
|
|
17
26
|
justify-content: flex-end;
|
|
@@ -32,4 +41,4 @@
|
|
|
32
41
|
font-size: 1rem;
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
|
-
}
|
|
44
|
+
}
|
|
@@ -3,16 +3,21 @@ import styled from "styled-components";
|
|
|
3
3
|
interface OverlayPanelStyledProps {
|
|
4
4
|
$top?: number;
|
|
5
5
|
$left?: number;
|
|
6
|
+
$minWidth?: number;
|
|
7
|
+
$visible?: boolean;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export const OverlayPanelStyled = styled.div<OverlayPanelStyledProps>`
|
|
9
11
|
position: absolute;
|
|
10
|
-
z-index:
|
|
12
|
+
z-index: 1050;
|
|
11
13
|
top: ${({ $top }) => $top}px;
|
|
12
14
|
left: ${({ $left }) => $left}px;
|
|
15
|
+
${({ $minWidth }) => ($minWidth ? `min-width: ${$minWidth}px;` : "")}
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
visibility: ${({ $visible }) => ($visible ? "visible" : "hidden")};
|
|
13
18
|
background-color: white;
|
|
14
19
|
padding: 10px;
|
|
15
20
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
16
|
-
border-radius:
|
|
21
|
+
border-radius: 2px;
|
|
17
22
|
border: 1px solid #ede;
|
|
18
23
|
`;
|
package/src/select/Select.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { useCallback, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Icon } from "../icon/Icon";
|
|
3
|
+
|
|
4
|
+
import { OverlayPanel } from "../overlayPanel/OverlayPanel";
|
|
3
5
|
import "./select.css";
|
|
4
6
|
import { SelectOptionInterface } from "./selectOption.interface";
|
|
5
7
|
|
|
@@ -12,6 +14,10 @@ export interface SelectProps {
|
|
|
12
14
|
name?: string;
|
|
13
15
|
defaultValue?: string | string[];
|
|
14
16
|
multiple?: boolean;
|
|
17
|
+
showOptions?: boolean;
|
|
18
|
+
usePortal?: boolean;
|
|
19
|
+
searchable?: boolean;
|
|
20
|
+
searchPlaceholder?: string;
|
|
15
21
|
required?: boolean;
|
|
16
22
|
value?: string | string[];
|
|
17
23
|
className?: string;
|
|
@@ -19,6 +25,7 @@ export interface SelectProps {
|
|
|
19
25
|
onChangeMultiple?: (value: string[]) => void;
|
|
20
26
|
onChange?: (value: string) => void;
|
|
21
27
|
placeholder?: string;
|
|
28
|
+
classNameOverlay?: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export const Select = ({
|
|
@@ -29,38 +36,112 @@ export const Select = ({
|
|
|
29
36
|
disabled,
|
|
30
37
|
style,
|
|
31
38
|
multiple = false,
|
|
32
|
-
|
|
39
|
+
showOptions = false,
|
|
33
40
|
defaultValue,
|
|
34
|
-
|
|
41
|
+
usePortal = false,
|
|
42
|
+
searchable = false,
|
|
43
|
+
searchPlaceholder = "Rechercher...",
|
|
35
44
|
onChange,
|
|
36
45
|
onChangeMultiple,
|
|
37
46
|
value,
|
|
38
47
|
placeholder,
|
|
48
|
+
classNameOverlay,
|
|
39
49
|
}: SelectProps): JSX.Element => {
|
|
40
|
-
const
|
|
50
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
51
|
+
const [overlayRef, setOverlayRef] = useState<HTMLElement | null>(null);
|
|
52
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
53
|
+
const hasAutoOpened = useRef(false);
|
|
54
|
+
const optionsListRef = useRef<HTMLDivElement | null>(null);
|
|
55
|
+
const [search, setSearch] = useState("");
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!showOverlay) setSearch("");
|
|
59
|
+
}, [showOverlay]);
|
|
60
|
+
|
|
61
|
+
const filteredOptions = useMemo(() => {
|
|
62
|
+
const normalize = (text: string) =>
|
|
63
|
+
text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
64
|
+
const term = normalize(search);
|
|
65
|
+
if (!searchable || !term) return options;
|
|
66
|
+
return options.filter((option) => normalize(option.label).includes(term));
|
|
67
|
+
}, [options, search, searchable]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!showOverlay) return;
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
const container = optionsListRef.current;
|
|
73
|
+
if (!container) return;
|
|
74
|
+
const selected = container.querySelector<HTMLElement>(
|
|
75
|
+
".select-overlay-option.selected",
|
|
48
76
|
);
|
|
77
|
+
selected?.scrollIntoView({ block: "nearest" });
|
|
78
|
+
}, 0);
|
|
79
|
+
return () => clearTimeout(timer);
|
|
80
|
+
}, [showOverlay]);
|
|
81
|
+
// "" est une vraie valeur si une option la porte (ex. "Aucun", "État")
|
|
82
|
+
const hasEmptyOption = options.some((option) => option.value === "");
|
|
83
|
+
const isDefined = (v?: string | string[]) =>
|
|
84
|
+
v !== undefined && v !== null && (v !== "" || hasEmptyOption);
|
|
85
|
+
const selectedValues = Array.isArray(value)
|
|
86
|
+
? value
|
|
87
|
+
: isDefined(value)
|
|
88
|
+
? [value as string]
|
|
89
|
+
: isDefined(defaultValue)
|
|
90
|
+
? Array.isArray(defaultValue)
|
|
91
|
+
? defaultValue
|
|
92
|
+
: [defaultValue as string]
|
|
93
|
+
: [];
|
|
94
|
+
const hasSelection = selectedValues.some((v) => v !== "");
|
|
95
|
+
|
|
96
|
+
const getDisplayValue = () => {
|
|
97
|
+
if (selectedValues.length === 0) return placeholder || "";
|
|
98
|
+
if (multiple) {
|
|
99
|
+
const labels = selectedValues
|
|
100
|
+
.map((v) => options.find((opt) => opt.value === v)?.label)
|
|
101
|
+
.filter((l): l is string => !!l);
|
|
102
|
+
return labels.join(", ");
|
|
103
|
+
}
|
|
104
|
+
const selected = options.find((opt) => opt.value === selectedValues[0]);
|
|
105
|
+
return selected?.label || "";
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleOptionClick = (optionValue: string) => {
|
|
109
|
+
if (multiple) {
|
|
110
|
+
const newValues = selectedValues.includes(optionValue)
|
|
111
|
+
? selectedValues.filter((v) => v !== optionValue)
|
|
112
|
+
: [...selectedValues, optionValue];
|
|
49
113
|
if (onChangeMultiple) {
|
|
50
|
-
onChangeMultiple(
|
|
114
|
+
onChangeMultiple(newValues);
|
|
51
115
|
}
|
|
52
116
|
} else {
|
|
53
|
-
selectedValue = event.target.value;
|
|
54
117
|
if (onChange) {
|
|
55
|
-
onChange(
|
|
118
|
+
onChange(optionValue);
|
|
56
119
|
}
|
|
120
|
+
setShowOverlay(false);
|
|
57
121
|
}
|
|
58
122
|
};
|
|
123
|
+
|
|
59
124
|
const resetValue = useCallback(() => {
|
|
60
|
-
if (
|
|
125
|
+
if (multiple && onChangeMultiple) {
|
|
126
|
+
onChangeMultiple([]);
|
|
127
|
+
} else if (onChange) {
|
|
61
128
|
onChange("");
|
|
62
129
|
}
|
|
63
|
-
}, [onChange]);
|
|
130
|
+
}, [onChange, onChangeMultiple, multiple]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (
|
|
134
|
+
showOptions &&
|
|
135
|
+
!disabled &&
|
|
136
|
+
!hasAutoOpened.current &&
|
|
137
|
+
triggerRef.current
|
|
138
|
+
) {
|
|
139
|
+
setOverlayRef(triggerRef.current);
|
|
140
|
+
setShowOverlay(true);
|
|
141
|
+
hasAutoOpened.current = true;
|
|
142
|
+
}
|
|
143
|
+
}, [showOptions, disabled]);
|
|
144
|
+
|
|
64
145
|
return (
|
|
65
146
|
<div id={id} className={`select-container ${className ? className : ""}`}>
|
|
66
147
|
{label && (
|
|
@@ -68,37 +149,95 @@ export const Select = ({
|
|
|
68
149
|
{label}
|
|
69
150
|
</label>
|
|
70
151
|
)}
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
required={required}
|
|
74
|
-
name={name}
|
|
75
|
-
style={style}
|
|
76
|
-
disabled={disabled}
|
|
152
|
+
<button
|
|
153
|
+
ref={triggerRef}
|
|
77
154
|
id={`select-${id}`}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
style={style}
|
|
157
|
+
className="select-container-button"
|
|
158
|
+
onClick={(e) => {
|
|
159
|
+
setOverlayRef(e.currentTarget as HTMLElement);
|
|
160
|
+
setShowOverlay(true);
|
|
161
|
+
}}
|
|
162
|
+
type="button"
|
|
163
|
+
aria-valuetext={getDisplayValue() || placeholder}
|
|
164
|
+
title={getDisplayValue() || placeholder}
|
|
165
|
+
aria-haspopup="listbox"
|
|
166
|
+
aria-expanded={showOverlay}
|
|
83
167
|
>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{placeholder}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
168
|
+
<span className="select-container-value">
|
|
169
|
+
{getDisplayValue() || (
|
|
170
|
+
<span className="placeholder">{placeholder}</span>
|
|
171
|
+
)}
|
|
172
|
+
</span>
|
|
173
|
+
</button>
|
|
174
|
+
|
|
175
|
+
{showOverlay && overlayRef && (
|
|
176
|
+
<OverlayPanel
|
|
177
|
+
show={showOverlay}
|
|
178
|
+
position="bottomLeft"
|
|
179
|
+
className={classNameOverlay ?? ""}
|
|
180
|
+
closeOverlay={() => setShowOverlay(false)}
|
|
181
|
+
referenceElement={overlayRef}
|
|
182
|
+
usePortal={usePortal}
|
|
183
|
+
matchReferenceWidth={usePortal}
|
|
184
|
+
children={
|
|
185
|
+
<>
|
|
186
|
+
{searchable && (
|
|
187
|
+
<div className="select-overlay-search">
|
|
188
|
+
<input
|
|
189
|
+
type="text"
|
|
190
|
+
autoFocus
|
|
191
|
+
value={search}
|
|
192
|
+
placeholder={searchPlaceholder}
|
|
193
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
194
|
+
onKeyDown={(e) => {
|
|
195
|
+
// évite de soumettre le formulaire parent
|
|
196
|
+
if (e.key === "Enter") e.preventDefault();
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<div className="select-overlay-options" ref={optionsListRef}>
|
|
202
|
+
{searchable && filteredOptions.length === 0 && (
|
|
203
|
+
<div className="select-overlay-empty">Aucun résultat</div>
|
|
204
|
+
)}
|
|
205
|
+
{filteredOptions.map((option) => (
|
|
206
|
+
<div
|
|
207
|
+
key={option.value}
|
|
208
|
+
aria-valuetext={option.label}
|
|
209
|
+
className={`select-overlay-option ${option.disabled ? "disabled" : ""} ${selectedValues.includes(option.value) ? "selected" : ""
|
|
210
|
+
}`}
|
|
211
|
+
onClick={() =>
|
|
212
|
+
!option.disabled && handleOptionClick(option.value)
|
|
213
|
+
}
|
|
214
|
+
role="option"
|
|
215
|
+
aria-selected={selectedValues.includes(option.value)}
|
|
216
|
+
>
|
|
217
|
+
{multiple && (
|
|
218
|
+
<input
|
|
219
|
+
type="checkbox"
|
|
220
|
+
checked={selectedValues.includes(option.value)}
|
|
221
|
+
readOnly
|
|
222
|
+
className="select-overlay-checkbox"
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
<span>{option.label}</span>
|
|
226
|
+
</div>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{hasSelection && placeholder && (
|
|
96
235
|
<div
|
|
97
236
|
className="clear-btn"
|
|
98
237
|
onClick={resetValue}
|
|
99
238
|
aria-label="Réinitialiser"
|
|
100
239
|
>
|
|
101
|
-
<Icon classIcon="bi-x" />
|
|
240
|
+
<Icon classIcon="bi-x-circle" />
|
|
102
241
|
</div>
|
|
103
242
|
)}
|
|
104
243
|
</div>
|