kelt-ui-kit-react 2.0.1 → 2.0.3

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.
Files changed (115) hide show
  1. package/dist/_core/type/generiqueType.d.ts +4 -0
  2. package/dist/badge/Badge.d.ts +3 -1
  3. package/dist/button/Button.d.ts +1 -1
  4. package/dist/button/button.interface.d.ts +1 -2
  5. package/dist/dataTable/DataTable.d.ts +17 -14
  6. package/dist/dataTable/dataTable.interface.d.ts +24 -19
  7. package/dist/dataTable/dataTableActions/DataTableAction.d.ts +3 -5
  8. package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +13 -7
  9. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.d.ts +10 -0
  10. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.d.ts +9 -0
  11. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.d.ts +9 -0
  12. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.d.ts +13 -0
  13. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.d.ts +9 -0
  14. package/dist/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.d.ts +11 -0
  15. package/dist/dataTable/dataTableFilters/DataTableFilters.d.ts +7 -0
  16. package/dist/dataTable/dataTableLoading/DataTableLoading.d.ts +1 -3
  17. package/dist/dataTable/dataTableRow/DataTableRow.d.ts +5 -6
  18. package/dist/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.d.ts +12 -0
  19. package/dist/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.d.ts +11 -0
  20. package/dist/expands/expand/expand.d.ts +1 -1
  21. package/dist/form/Form.d.ts +1 -2
  22. package/dist/form/form.enum.d.ts +2 -2
  23. package/dist/form/form.interface.d.ts +2 -4
  24. package/dist/form/input/Input.d.ts +3 -0
  25. package/dist/form/inputNumber/InputNumber.d.ts +0 -2
  26. package/dist/grid/col/Col.d.ts +6 -3
  27. package/dist/grid/col/colStyled/ColStyled.d.ts +5 -7
  28. package/dist/grid/grid.interface.d.ts +5 -9
  29. package/dist/header/Header.d.ts +1 -1
  30. package/dist/header/header.interface.d.ts +1 -2
  31. package/dist/icon/Icon.d.ts +1 -1
  32. package/dist/icon/icon.interface.d.ts +1 -0
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +3740 -3443
  35. package/dist/menus/menu/Menu.d.ts +0 -1
  36. package/dist/overlayPanel/OverlayPanel.d.ts +4 -2
  37. package/dist/overlayPanel/overlayPanelStyled/OverlayPanelStyled.d.ts +2 -0
  38. package/dist/select/Select.d.ts +6 -2
  39. package/dist/select/selectOption.interface.d.ts +1 -0
  40. package/dist/style.css +1 -1
  41. package/dist/tabs/Tabs.d.ts +2 -1
  42. package/dist/toaster/store/useToasterStore.d.ts +0 -1
  43. package/index.html +2 -1
  44. package/package/package.json +56 -0
  45. package/package/src/sidebar/Sidebar.tsx +208 -0
  46. package/package/src/sidebar/sidebar.css +104 -0
  47. package/package.json +1 -1
  48. package/src/_core/type/generiqueType.tsx +4 -0
  49. package/src/badge/Badge.tsx +9 -1
  50. package/src/badge/badge.css +4 -0
  51. package/src/button/Button.tsx +1 -3
  52. package/src/button/button.css +2 -2
  53. package/src/button/button.interface.tsx +1 -2
  54. package/src/card/card.css +25 -5
  55. package/src/card/cardAction/CardAction.tsx +4 -4
  56. package/src/damier/Damier.tsx +7 -16
  57. package/src/damier/damier.css +4 -11
  58. package/src/dataTable/DataTable.tsx +685 -542
  59. package/src/dataTable/DataTable.view.tsx +48 -24
  60. package/src/dataTable/dataTable.css +134 -159
  61. package/src/dataTable/dataTable.interface.ts +25 -19
  62. package/src/dataTable/dataTableActions/DataTableAction.tsx +14 -25
  63. package/src/dataTable/dataTableColumn/DataTableColumn.tsx +64 -14
  64. package/src/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.tsx +36 -0
  65. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilter.css +0 -0
  66. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.tsx +39 -0
  67. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.tsx +80 -0
  68. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.tsx +89 -0
  69. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.tsx +37 -0
  70. package/src/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.tsx +42 -0
  71. package/src/dataTable/dataTableFilters/DataTableFilters.tsx +91 -0
  72. package/src/dataTable/dataTableFilters/dataTableFilters.css +16 -0
  73. package/src/dataTable/dataTableLoading/DataTableLoading.tsx +4 -15
  74. package/src/dataTable/dataTableRow/DataTableRow.tsx +64 -180
  75. package/src/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.tsx +80 -0
  76. package/src/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.tsx +123 -0
  77. package/src/expands/Expands.tsx +2 -16
  78. package/src/expands/expand/expand.tsx +5 -122
  79. package/src/expands/expands.css +3 -8
  80. package/src/form/Form.tsx +25 -125
  81. package/src/form/Form.view.tsx +1 -9
  82. package/src/form/form.css +1 -38
  83. package/src/form/form.enum.ts +1 -1
  84. package/src/form/form.interface.tsx +2 -7
  85. package/src/form/input/Input.tsx +61 -7
  86. package/src/form/input/input.css +23 -1
  87. package/src/form/inputNumber/InputNumber.tsx +6 -11
  88. package/src/form/inputPrice/InputPrice.tsx +2 -4
  89. package/src/grid/col/Col.tsx +7 -5
  90. package/src/grid/col/colStyled/ColStyled.tsx +7 -21
  91. package/src/grid/grid.interface.tsx +5 -11
  92. package/src/header/Header.tsx +2 -4
  93. package/src/header/header.css +0 -17
  94. package/src/header/header.interface.tsx +1 -2
  95. package/src/icon/Icon.tsx +6 -2
  96. package/src/icon/icon.interface.tsx +1 -0
  97. package/src/index.css +12 -58
  98. package/src/index.ts +2 -1
  99. package/src/menus/Menus.tsx +0 -1
  100. package/src/menus/menu/Menu.tsx +1 -2
  101. package/src/overlayPanel/OverlayPanel.tsx +71 -18
  102. package/src/overlayPanel/overlay.context.tsx +6 -4
  103. package/src/overlayPanel/overlayPanel.css +10 -1
  104. package/src/overlayPanel/overlayPanelStyled/OverlayPanelStyled.tsx +7 -2
  105. package/src/select/Select.tsx +178 -41
  106. package/src/select/select.css +94 -13
  107. package/src/select/selectOption.interface.ts +1 -0
  108. package/src/sidebar/Sidebar.tsx +5 -107
  109. package/src/sidebar/sidebar.css +1 -18
  110. package/src/tabs/Tabs.tsx +20 -1
  111. package/src/tabs/tabs.css +3 -9
  112. package/src/toaster/Toaster.tsx +2 -7
  113. package/src/toaster/Toaster.view.tsx +0 -1
  114. package/src/toaster/store/useToasterStore.tsx +0 -2
  115. package/src/toaster/toaster.css +2 -7
@@ -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
- useEffect(() => {
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
- setCoords({ top, left: left < 0 ? 0 : left });
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 () => window.removeEventListener("resize", updatePosition);
136
- }, [referenceElement, isShow, position]);
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
- closeOverlayHandler(event);
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
- document.addEventListener("mousedown", handleClickOutside);
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, closeOverlayHandler]);
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
- return ReactDOM.createPortal(
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
- if (!context) {
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: 5;
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: 4px;
21
+ border-radius: 2px;
17
22
  border: 1px solid #ede;
18
23
  `;
@@ -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
 
@@ -8,10 +10,13 @@ export interface SelectProps {
8
10
  label?: string;
9
11
  title?: string;
10
12
  disabled?: boolean;
11
- style?: React.CSSProperties;
12
13
  name?: string;
13
14
  defaultValue?: string | string[];
14
15
  multiple?: boolean;
16
+ showOptions?: boolean;
17
+ usePortal?: boolean;
18
+ searchable?: boolean;
19
+ searchPlaceholder?: string;
15
20
  required?: boolean;
16
21
  value?: string | string[];
17
22
  className?: string;
@@ -19,6 +24,7 @@ export interface SelectProps {
19
24
  onChangeMultiple?: (value: string[]) => void;
20
25
  onChange?: (value: string) => void;
21
26
  placeholder?: string;
27
+ classNameOverlay?: string;
22
28
  }
23
29
 
24
30
  export const Select = ({
@@ -27,40 +33,113 @@ export const Select = ({
27
33
  options,
28
34
  className,
29
35
  disabled,
30
- style,
31
36
  multiple = false,
32
- required = false,
37
+ showOptions = false,
33
38
  defaultValue,
34
- name,
39
+ usePortal = false,
40
+ searchable = false,
41
+ searchPlaceholder = "Rechercher...",
35
42
  onChange,
36
43
  onChangeMultiple,
37
44
  value,
38
45
  placeholder,
46
+ classNameOverlay,
39
47
  }: SelectProps): JSX.Element => {
40
- const inputRefs = useRef<{ [key: string]: HTMLSelectElement | null }>({});
48
+ const [showOverlay, setShowOverlay] = useState(false);
49
+ const [overlayRef, setOverlayRef] = useState<HTMLElement | null>(null);
50
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
51
+ const hasAutoOpened = useRef(false);
52
+ const optionsListRef = useRef<HTMLDivElement | null>(null);
53
+ const [search, setSearch] = useState("");
41
54
 
42
- const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
43
- let selectedValue: string | string[];
44
- if (multiple) {
45
- selectedValue = Array.from(
46
- event.target.selectedOptions,
47
- (opt) => opt.value,
55
+ useEffect(() => {
56
+ if (!showOverlay) setSearch("");
57
+ }, [showOverlay]);
58
+
59
+ const filteredOptions = useMemo(() => {
60
+ const normalize = (text: string) =>
61
+ text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
62
+ const term = normalize(search);
63
+ if (!searchable || !term) return options;
64
+ return options.filter((option) => normalize(option.label).includes(term));
65
+ }, [options, search, searchable]);
66
+
67
+ useEffect(() => {
68
+ if (!showOverlay) return;
69
+ const timer = setTimeout(() => {
70
+ const container = optionsListRef.current;
71
+ if (!container) return;
72
+ const selected = container.querySelector<HTMLElement>(
73
+ ".select-overlay-option.selected",
48
74
  );
75
+ selected?.scrollIntoView({ block: "nearest" });
76
+ }, 0);
77
+ return () => clearTimeout(timer);
78
+ }, [showOverlay]);
79
+ // "" est une vraie valeur si une option la porte (ex. "Aucun", "État")
80
+ const hasEmptyOption = options.some((option) => option.value === "");
81
+ const isDefined = (v?: string | string[]) =>
82
+ v !== undefined && v !== null && (v !== "" || hasEmptyOption);
83
+ const selectedValues = Array.isArray(value)
84
+ ? value
85
+ : isDefined(value)
86
+ ? [value as string]
87
+ : isDefined(defaultValue)
88
+ ? Array.isArray(defaultValue)
89
+ ? defaultValue
90
+ : [defaultValue as string]
91
+ : [];
92
+ const hasSelection = selectedValues.some((v) => v !== "");
93
+
94
+ const getDisplayValue = () => {
95
+ if (selectedValues.length === 0) return placeholder || "";
96
+ if (multiple) {
97
+ const labels = selectedValues
98
+ .map((v) => options.find((opt) => opt.value === v)?.label)
99
+ .filter((l): l is string => !!l);
100
+ return labels.join(", ");
101
+ }
102
+ const selected = options.find((opt) => opt.value === selectedValues[0]);
103
+ return selected?.label || "";
104
+ };
105
+
106
+ const handleOptionClick = (optionValue: string) => {
107
+ if (multiple) {
108
+ const newValues = selectedValues.includes(optionValue)
109
+ ? selectedValues.filter((v) => v !== optionValue)
110
+ : [...selectedValues, optionValue];
49
111
  if (onChangeMultiple) {
50
- onChangeMultiple(selectedValue as string[]);
112
+ onChangeMultiple(newValues);
51
113
  }
52
114
  } else {
53
- selectedValue = event.target.value;
54
115
  if (onChange) {
55
- onChange(selectedValue);
116
+ onChange(optionValue);
56
117
  }
118
+ setShowOverlay(false);
57
119
  }
58
120
  };
121
+
59
122
  const resetValue = useCallback(() => {
60
- if (onChange) {
123
+ if (multiple && onChangeMultiple) {
124
+ onChangeMultiple([]);
125
+ } else if (onChange) {
61
126
  onChange("");
62
127
  }
63
- }, [onChange]);
128
+ }, [onChange, onChangeMultiple, multiple]);
129
+
130
+ useEffect(() => {
131
+ if (
132
+ showOptions &&
133
+ !disabled &&
134
+ !hasAutoOpened.current &&
135
+ triggerRef.current
136
+ ) {
137
+ setOverlayRef(triggerRef.current);
138
+ setShowOverlay(true);
139
+ hasAutoOpened.current = true;
140
+ }
141
+ }, [showOptions, disabled]);
142
+
64
143
  return (
65
144
  <div id={id} className={`select-container ${className ? className : ""}`}>
66
145
  {label && (
@@ -68,37 +147,95 @@ export const Select = ({
68
147
  {label}
69
148
  </label>
70
149
  )}
71
- <select
72
- defaultValue=""
73
- required={required}
74
- name={name}
75
- style={style}
76
- disabled={disabled}
150
+ <button
151
+ ref={triggerRef}
77
152
  id={`select-${id}`}
78
- className="select-container-select"
79
- value={value ?? defaultValue ?? (multiple ? [] : "")}
80
- onChange={handleChange}
81
- multiple={multiple}
82
- ref={(el) => (inputRefs.current[id ?? "select"] = el)}
153
+ disabled={disabled}
154
+ className="select-container-button"
155
+ onClick={(e) => {
156
+ setOverlayRef(e.currentTarget as HTMLElement);
157
+ setShowOverlay(true);
158
+ }}
159
+ type="button"
160
+ aria-valuetext={getDisplayValue() || placeholder}
161
+ title={getDisplayValue() || placeholder}
162
+ aria-haspopup="listbox"
163
+ aria-expanded={showOverlay}
83
164
  >
84
- {placeholder && !defaultValue && !multiple && (
85
- <option selected disabled hidden value="">
86
- {placeholder}
87
- </option>
88
- )}
89
- {options.map((option, index) => (
90
- <option key={index} value={option.value}>
91
- {option.label}
92
- </option>
93
- ))}
94
- </select>
95
- {value !== "" && placeholder && (
165
+ <span className="select-container-value">
166
+ {getDisplayValue() || (
167
+ <span className="placeholder">{placeholder}</span>
168
+ )}
169
+ </span>
170
+ </button>
171
+
172
+ {showOverlay && overlayRef && (
173
+ <OverlayPanel
174
+ show={showOverlay}
175
+ position="bottomLeft"
176
+ className={classNameOverlay ?? ""}
177
+ closeOverlay={() => setShowOverlay(false)}
178
+ referenceElement={overlayRef}
179
+ usePortal={usePortal}
180
+ matchReferenceWidth={usePortal}
181
+ children={
182
+ <>
183
+ {searchable && (
184
+ <div className="select-overlay-search">
185
+ <input
186
+ type="text"
187
+ autoFocus
188
+ value={search}
189
+ placeholder={searchPlaceholder}
190
+ onChange={(e) => setSearch(e.target.value)}
191
+ onKeyDown={(e) => {
192
+ // évite de soumettre le formulaire parent
193
+ if (e.key === "Enter") e.preventDefault();
194
+ }}
195
+ />
196
+ </div>
197
+ )}
198
+ <div className="select-overlay-options" ref={optionsListRef}>
199
+ {searchable && filteredOptions.length === 0 && (
200
+ <div className="select-overlay-empty">Aucun résultat</div>
201
+ )}
202
+ {filteredOptions.map((option) => (
203
+ <div
204
+ key={option.value}
205
+ aria-valuetext={option.label}
206
+ className={`select-overlay-option ${option.disabled ? "disabled" : ""} ${
207
+ selectedValues.includes(option.value) ? "selected" : ""
208
+ }`}
209
+ onClick={() =>
210
+ !option.disabled && handleOptionClick(option.value)
211
+ }
212
+ role="option"
213
+ aria-selected={selectedValues.includes(option.value)}
214
+ >
215
+ {multiple && (
216
+ <input
217
+ type="checkbox"
218
+ checked={selectedValues.includes(option.value)}
219
+ readOnly
220
+ className="select-overlay-checkbox"
221
+ />
222
+ )}
223
+ <span>{option.label}</span>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ </>
228
+ }
229
+ />
230
+ )}
231
+
232
+ {hasSelection && placeholder && (
96
233
  <div
97
234
  className="clear-btn"
98
235
  onClick={resetValue}
99
236
  aria-label="Réinitialiser"
100
237
  >
101
- <Icon classIcon="bi-x" />
238
+ <Icon classIcon="bi-x-circle" />
102
239
  </div>
103
240
  )}
104
241
  </div>