kelt-ui-kit-react 2.0.2 → 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 (127) hide show
  1. package/README.md +0 -26
  2. package/dist/_core/type/generiqueType.d.ts +4 -0
  3. package/dist/badge/Badge.d.ts +3 -1
  4. package/dist/button/Button.d.ts +1 -1
  5. package/dist/button/button.interface.d.ts +1 -2
  6. package/dist/dataTable/DataTable.d.ts +17 -14
  7. package/dist/dataTable/dataTable.interface.d.ts +24 -19
  8. package/dist/dataTable/dataTableActions/DataTableAction.d.ts +3 -5
  9. package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +13 -7
  10. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.d.ts +10 -0
  11. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.d.ts +9 -0
  12. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.d.ts +9 -0
  13. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.d.ts +13 -0
  14. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.d.ts +9 -0
  15. package/dist/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.d.ts +11 -0
  16. package/dist/dataTable/dataTableFilters/DataTableFilters.d.ts +7 -0
  17. package/dist/dataTable/dataTableLoading/DataTableLoading.d.ts +1 -3
  18. package/dist/dataTable/dataTableRow/DataTableRow.d.ts +5 -6
  19. package/dist/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.d.ts +12 -0
  20. package/dist/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.d.ts +11 -0
  21. package/dist/expands/expand/expand.d.ts +1 -1
  22. package/dist/form/Form.d.ts +2 -4
  23. package/dist/form/form.enum.d.ts +2 -3
  24. package/dist/form/form.interface.d.ts +2 -19
  25. package/dist/form/input/Input.d.ts +3 -0
  26. package/dist/form/inputNumber/InputNumber.d.ts +0 -2
  27. package/dist/grid/col/Col.d.ts +6 -3
  28. package/dist/grid/col/colStyled/ColStyled.d.ts +5 -7
  29. package/dist/grid/grid.interface.d.ts +5 -9
  30. package/dist/header/Header.d.ts +1 -1
  31. package/dist/header/header.interface.d.ts +1 -2
  32. package/dist/icon/Icon.d.ts +1 -1
  33. package/dist/icon/icon.interface.d.ts +1 -0
  34. package/dist/index.d.ts +1 -5
  35. package/dist/index.js +3606 -3543
  36. package/dist/menus/menu/Menu.d.ts +0 -1
  37. package/dist/overlayPanel/OverlayPanel.d.ts +4 -2
  38. package/dist/overlayPanel/overlayPanelStyled/OverlayPanelStyled.d.ts +2 -0
  39. package/dist/select/Select.d.ts +6 -2
  40. package/dist/select/selectOption.interface.d.ts +1 -0
  41. package/dist/style.css +1 -1
  42. package/dist/tabs/Tabs.d.ts +2 -1
  43. package/dist/toaster/store/useToasterStore.d.ts +0 -1
  44. package/index.html +2 -1
  45. package/package/package.json +56 -0
  46. package/package/src/sidebar/Sidebar.tsx +208 -0
  47. package/package/src/sidebar/sidebar.css +104 -0
  48. package/package.json +1 -1
  49. package/src/App.menu.tsx +0 -8
  50. package/src/_core/type/generiqueType.tsx +4 -0
  51. package/src/badge/Badge.tsx +9 -1
  52. package/src/badge/badge.css +4 -0
  53. package/src/button/Button.tsx +1 -3
  54. package/src/button/button.css +2 -2
  55. package/src/button/button.interface.tsx +1 -2
  56. package/src/card/card.css +25 -5
  57. package/src/card/cardAction/CardAction.tsx +4 -4
  58. package/src/damier/Damier.tsx +7 -16
  59. package/src/damier/damier.css +4 -11
  60. package/src/dataTable/DataTable.tsx +685 -542
  61. package/src/dataTable/DataTable.view.tsx +48 -24
  62. package/src/dataTable/dataTable.css +134 -159
  63. package/src/dataTable/dataTable.interface.ts +25 -19
  64. package/src/dataTable/dataTableActions/DataTableAction.tsx +14 -25
  65. package/src/dataTable/dataTableColumn/DataTableColumn.tsx +64 -14
  66. package/src/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.tsx +36 -0
  67. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilter.css +0 -0
  68. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.tsx +39 -0
  69. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.tsx +80 -0
  70. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.tsx +89 -0
  71. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.tsx +37 -0
  72. package/src/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.tsx +42 -0
  73. package/src/dataTable/dataTableFilters/DataTableFilters.tsx +91 -0
  74. package/src/dataTable/dataTableFilters/dataTableFilters.css +16 -0
  75. package/src/dataTable/dataTableLoading/DataTableLoading.tsx +4 -15
  76. package/src/dataTable/dataTableRow/DataTableRow.tsx +64 -180
  77. package/src/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.tsx +80 -0
  78. package/src/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.tsx +123 -0
  79. package/src/expands/Expands.tsx +2 -16
  80. package/src/expands/expand/expand.tsx +5 -122
  81. package/src/expands/expands.css +3 -8
  82. package/src/form/Form.tsx +93 -258
  83. package/src/form/Form.view.tsx +1 -9
  84. package/src/form/form.css +1 -51
  85. package/src/form/form.enum.ts +1 -2
  86. package/src/form/form.interface.tsx +2 -28
  87. package/src/form/input/Input.tsx +61 -7
  88. package/src/form/input/input.css +23 -1
  89. package/src/form/inputNumber/InputNumber.tsx +6 -11
  90. package/src/form/inputPrice/InputPrice.tsx +2 -4
  91. package/src/grid/col/Col.tsx +7 -5
  92. package/src/grid/col/colStyled/ColStyled.tsx +7 -21
  93. package/src/grid/grid.interface.tsx +5 -11
  94. package/src/grid/row/row.css +2 -2
  95. package/src/header/Header.tsx +2 -4
  96. package/src/header/header.css +0 -17
  97. package/src/header/header.interface.tsx +1 -2
  98. package/src/icon/Icon.tsx +6 -2
  99. package/src/icon/icon.interface.tsx +1 -0
  100. package/src/index.css +12 -58
  101. package/src/index.ts +2 -12
  102. package/src/menus/Menus.tsx +0 -1
  103. package/src/menus/menu/Menu.tsx +1 -2
  104. package/src/overlayPanel/OverlayPanel.tsx +71 -18
  105. package/src/overlayPanel/overlay.context.tsx +6 -4
  106. package/src/overlayPanel/overlayPanel.css +10 -1
  107. package/src/overlayPanel/overlayPanelStyled/OverlayPanelStyled.tsx +7 -2
  108. package/src/select/Select.tsx +178 -41
  109. package/src/select/select.css +94 -13
  110. package/src/select/selectOption.interface.ts +1 -0
  111. package/src/sidebar/Sidebar.tsx +5 -107
  112. package/src/sidebar/sidebar.css +1 -18
  113. package/src/tabs/Tabs.tsx +20 -1
  114. package/src/tabs/tabs.css +3 -9
  115. package/src/toaster/Toaster.tsx +2 -7
  116. package/src/toaster/Toaster.view.tsx +0 -1
  117. package/src/toaster/store/useToasterStore.tsx +0 -2
  118. package/src/toaster/toaster.css +2 -7
  119. package/dist/addressSearch/AddressSearch.d.ts +0 -20
  120. package/dist/addressSearch/AddressSearch.view.d.ts +0 -1
  121. package/dist/geocoding/GeocodingService.d.ts +0 -13
  122. package/dist/geocoding/geocoding.interface.d.ts +0 -20
  123. package/src/addressSearch/AddressSearch.tsx +0 -246
  124. package/src/addressSearch/AddressSearch.view.tsx +0 -52
  125. package/src/addressSearch/addressSearch.css +0 -133
  126. package/src/geocoding/GeocodingService.ts +0 -89
  127. package/src/geocoding/geocoding.interface.ts +0 -25
@@ -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>
@@ -1,5 +1,6 @@
1
1
  .select-container {
2
2
  position: relative;
3
+
3
4
  &::after,
4
5
  &::before {
5
6
  --size: 0.3rem;
@@ -8,23 +9,19 @@
8
9
  right: 1rem;
9
10
  pointer-events: none;
10
11
  }
11
- select:has(option[value=""]:checked) {
12
- color: gray;
13
- }
14
- option {
15
- color: #000;
16
- }
12
+
17
13
  .clear-btn {
18
14
  position: absolute;
19
- top: 2px;
15
+ top: 50%;
16
+ transform: translateY(-50%);
20
17
  display: flex;
21
18
  align-items: center;
22
19
  z-index: 2;
23
20
  justify-content: center;
24
- bottom: 0;
25
- right: 30px;
21
+ right: 10px;
26
22
  cursor: pointer;
27
23
  }
24
+
28
25
  &::before {
29
26
  border-left: var(--size) solid transparent;
30
27
  border-right: var(--size) solid transparent;
@@ -38,15 +35,18 @@
38
35
  border-top: var(--size) solid black;
39
36
  top: 55%;
40
37
  }
38
+
41
39
  .select-container-label {
42
40
  font-weight: bold;
43
41
  color: #333;
44
42
  margin-bottom: 2px;
43
+ display: block;
45
44
  }
46
- .select-container-select {
45
+
46
+ .select-container-button {
47
47
  appearance: none;
48
48
  width: 100%;
49
- font-size: 1rem;
49
+ font-size: 0.8rem;
50
50
  padding: 0.5rem 1rem;
51
51
  margin: 0.25rem 0;
52
52
  background-color: #fff;
@@ -54,16 +54,97 @@
54
54
  border-radius: 0.25rem;
55
55
  color: #000;
56
56
  cursor: pointer;
57
- font-size: 0.8rem;
57
+ text-align: left;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ padding-right: 2rem;
62
+
63
+ .select-container-value {
64
+ flex: 1;
65
+ min-width: 0;
66
+ overflow: hidden;
67
+ white-space: nowrap;
68
+ text-overflow: ellipsis;
69
+
70
+ .placeholder {
71
+ color: gray;
72
+ }
73
+ }
74
+
58
75
  &:disabled {
59
76
  background-color: #e9ecef;
60
77
  color: #6c757d;
61
78
  cursor: not-allowed;
62
79
  }
80
+
63
81
  &:focus {
64
- border-color: #007bff;
65
82
  outline: none;
66
83
  background-color: #fff;
67
84
  }
85
+
86
+
68
87
  }
69
88
  }
89
+
90
+ .select-overlay-options {
91
+ display: flex;
92
+ flex-direction: column;
93
+ max-height: 300px;
94
+ overflow-y: auto;
95
+ padding: 0.5rem 0;
96
+ background-color: #fff;
97
+ border: 1px solid #b2a1a1;
98
+ border-radius: 0.25rem;
99
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
100
+
101
+ .select-overlay-option {
102
+ padding: 0.5rem 1rem;
103
+ cursor: pointer;
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 0.5rem;
107
+ user-select: none;
108
+
109
+ .select-overlay-checkbox {
110
+ min-width: 20px;
111
+ width: 20px;
112
+ cursor: pointer;
113
+ }
114
+
115
+ &:hover {
116
+ background-color: #f0f0f0;
117
+ }
118
+
119
+ &.selected {
120
+ background-color: #e7f3ff;
121
+ color: #007bff;
122
+ }
123
+ }
124
+ }
125
+
126
+ .select-overlay-search {
127
+ padding-bottom: 0.5rem;
128
+ /* laisse la place à la croix de fermeture de l'overlay (.toggle-btn) */
129
+ padding-right: 1.5rem;
130
+
131
+ input {
132
+ width: 100%;
133
+ box-sizing: border-box;
134
+ font-size: 0.8rem;
135
+ padding: 0.4rem 0.75rem;
136
+ border: 1px solid #b2a1a1;
137
+ border-radius: 0.25rem;
138
+
139
+ &:focus {
140
+ outline: none;
141
+ border-color: #888;
142
+ }
143
+ }
144
+ }
145
+
146
+ .select-overlay-empty {
147
+ padding: 0.5rem 1rem;
148
+ color: gray;
149
+ font-size: 0.8rem;
150
+ }
@@ -1,4 +1,5 @@
1
1
  export interface SelectOptionInterface {
2
2
  value: string;
3
3
  label: string;
4
+ disabled?: boolean;
4
5
  }
@@ -15,9 +15,6 @@ export interface SidebarProps {
15
15
  onClose?: (close: boolean) => void;
16
16
  }
17
17
 
18
- const SWIPE_CLOSE_THRESHOLD = 60; // px à parcourir pour fermer
19
- const SWIPE_AXIS_LOCK_THRESHOLD = 10; // px pour déterminer l'axe du geste
20
-
21
18
  export const Sidebar = ({
22
19
  open,
23
20
  children,
@@ -30,13 +27,8 @@ export const Sidebar = ({
30
27
  displayOverlay = true,
31
28
  }: SidebarProps): JSX.Element => {
32
29
  const [isOpen, setIsOpen] = useState(open || false);
33
- const [dragX, setDragX] = useState(0);
34
- const [isDragging, setIsDragging] = useState(false);
35
30
  const sidebarRef = useRef<HTMLDivElement | null>(null);
36
31
  const rootRef = useRef<HTMLDivElement | null>(null);
37
- const touchStartX = useRef(0);
38
- const touchStartY = useRef(0);
39
- const axisLocked = useRef<"x" | "y" | null>(null);
40
32
 
41
33
  // Créer le conteneur une seule fois
42
34
  useEffect(() => {
@@ -68,7 +60,7 @@ export const Sidebar = ({
68
60
  }, [onClose]);
69
61
 
70
62
  const handleClickOutside = useCallback(
71
- (event: MouseEvent | TouchEvent) => {
63
+ (event: MouseEvent) => {
72
64
  if (!closeOnClickOutside) return;
73
65
  if (
74
66
  sidebarRef.current &&
@@ -77,98 +69,18 @@ export const Sidebar = ({
77
69
  toggleSidebar();
78
70
  }
79
71
  },
80
- [toggleSidebar, closeOnClickOutside],
81
- );
82
-
83
- const handleTouchStart = useCallback((event: React.TouchEvent) => {
84
- const touch = event.touches[0];
85
- touchStartX.current = touch.clientX;
86
- touchStartY.current = touch.clientY;
87
- axisLocked.current = null;
88
- setIsDragging(false);
89
- setDragX(0);
90
- }, []);
91
-
92
- const handleTouchMove = useCallback(
93
- (event: React.TouchEvent) => {
94
- if (!isOpen) return;
95
-
96
- const touch = event.touches[0];
97
- const deltaX = touch.clientX - touchStartX.current;
98
- const deltaY = touch.clientY - touchStartY.current;
99
-
100
- // Détermine l'axe du geste une seule fois, dès qu'on a un déplacement suffisant
101
- if (!axisLocked.current) {
102
- if (
103
- Math.abs(deltaX) < SWIPE_AXIS_LOCK_THRESHOLD &&
104
- Math.abs(deltaY) < SWIPE_AXIS_LOCK_THRESHOLD
105
- ) {
106
- return; // pas encore assez de mouvement pour trancher
107
- }
108
- axisLocked.current = Math.abs(deltaX) > Math.abs(deltaY) ? "x" : "y";
109
- }
110
-
111
- if (axisLocked.current !== "x") {
112
- return;
113
- }
114
-
115
- // Ignore les petits mouvements horizontaux
116
- if (deltaX < 20) {
117
- return;
118
- }
119
-
120
- event.preventDefault();
121
-
122
- setIsDragging(true);
123
- setDragX(Math.max(0, deltaX));
124
- },
125
- [isOpen],
126
- );
127
-
128
- const handleTouchEnd = useCallback(
129
- (event: React.TouchEvent) => {
130
- if (!isOpen) {
131
- return;
132
- }
133
-
134
- if (axisLocked.current === "x") {
135
- const touch = event.changedTouches[0];
136
- const deltaX = touch.clientX - touchStartX.current;
137
-
138
- if (deltaX > SWIPE_CLOSE_THRESHOLD) {
139
- toggleSidebar();
140
- }
141
- }
142
-
143
- setIsDragging(false);
144
- setDragX(0);
145
- axisLocked.current = null;
146
- },
147
- [isOpen, toggleSidebar],
148
- );
149
-
150
- const handleOverlayTouchEnd = useCallback(
151
- (event: React.TouchEvent) => {
152
- event.preventDefault();
153
- toggleSidebar();
154
- },
155
- [toggleSidebar],
72
+ [toggleSidebar, closeOnClickOutside]
156
73
  );
157
74
 
158
75
  useEffect(() => {
159
76
  if (isOpen) {
160
77
  document.addEventListener("mousedown", handleClickOutside);
161
- document.addEventListener("touchstart", handleClickOutside, {
162
- passive: true,
163
- });
164
78
  } else {
165
79
  document.removeEventListener("mousedown", handleClickOutside);
166
- document.removeEventListener("touchstart", handleClickOutside);
167
80
  }
168
81
 
169
82
  return () => {
170
83
  document.removeEventListener("mousedown", handleClickOutside);
171
- document.removeEventListener("touchstart", handleClickOutside);
172
84
  };
173
85
  }, [isOpen, handleClickOutside]);
174
86
 
@@ -178,25 +90,11 @@ export const Sidebar = ({
178
90
 
179
91
  return ReactDOM.createPortal(
180
92
  <>
181
- {isOpen && displayOverlay && (
182
- <div
183
- className="overlay"
184
- onClick={toggleSidebar}
185
- onTouchEnd={handleOverlayTouchEnd}
186
- ></div>
187
- )}
93
+ {isOpen && displayOverlay && <div className="overlay"></div>}
188
94
  <div
189
95
  ref={sidebarRef}
190
- style={{
191
- ...(width ? { width } : {}),
192
- transform: dragX ? `translateX(${dragX}px)` : undefined,
193
- transition: isDragging ? "none" : undefined,
194
- }}
96
+ style={width ? { width: width } : {}}
195
97
  className={`sidebar ${isOpen ? "open" : "closed"} ${className ?? ""}`}
196
- onClick={(event) => event.stopPropagation()}
197
- onTouchStart={handleTouchStart}
198
- onTouchMove={handleTouchMove}
199
- onTouchEnd={handleTouchEnd}
200
98
  >
201
99
  <div className="sidebar-header">
202
100
  {title && <h2 className="flex-1">{title}</h2>}
@@ -208,6 +106,6 @@ export const Sidebar = ({
208
106
  {children && children}
209
107
  </div>
210
108
  </>,
211
- rootRef.current,
109
+ rootRef.current
212
110
  );
213
111
  };
@@ -1,18 +1,3 @@
1
- .sidebar {
2
- touch-action: pan-y;
3
- /* laisse le scroll vertical natif, gère le X nous-même */
4
- will-change: transform;
5
- }
6
-
7
- .overlay {
8
- touch-action: none;
9
- }
10
-
11
- .toggle-btn {
12
- -webkit-tap-highlight-color: transparent;
13
- touch-action: manipulation;
14
- }
15
-
16
1
  .sidebar {
17
2
  width: 200px;
18
3
  height: 100vh;
@@ -25,8 +10,6 @@
25
10
  transform: translateX(0);
26
11
  z-index: 2;
27
12
 
28
-
29
-
30
13
  &:not(.full) {
31
14
  inset: 0;
32
15
  }
@@ -101,4 +84,4 @@
101
84
  width: 40vw;
102
85
  }
103
86
  }
104
- }
87
+ }
package/src/tabs/Tabs.tsx CHANGED
@@ -15,6 +15,7 @@ export type TabsProps = {
15
15
  className?: string;
16
16
  tabClassName?: string;
17
17
  panelClassName?: string;
18
+ keepMountedVisited?: boolean;
18
19
  };
19
20
 
20
21
  export const Tabs = ({
@@ -25,9 +26,14 @@ export const Tabs = ({
25
26
  className = "",
26
27
  tabClassName = "",
27
28
  panelClassName = "",
29
+ keepMountedVisited = false,
28
30
  }: TabsProps) => {
29
31
  const isControlled = typeof index === "number";
30
32
  const [active, setActive] = useState<number>(defaultIndex);
33
+ // En mode contrôlé, seul l'onglet réellement affiché est considéré comme visité
34
+ const [visitedTabs, setVisitedTabs] = useState<Set<number>>(
35
+ () => new Set([isControlled ? (index as number) : defaultIndex]),
36
+ );
31
37
  const current = isControlled ? (index as number) : active;
32
38
 
33
39
  useEffect(() => {
@@ -37,6 +43,18 @@ export const Tabs = ({
37
43
  }
38
44
  }, [defaultIndex, items.length, isControlled]);
39
45
 
46
+ useEffect(() => {
47
+ setVisitedTabs((prev) => {
48
+ if (prev.has(current)) {
49
+ return prev;
50
+ }
51
+
52
+ const next = new Set(prev);
53
+ next.add(current);
54
+ return next;
55
+ });
56
+ }, [current]);
57
+
40
58
  const tabsRef = useRef<Array<HTMLButtonElement | null>>([]);
41
59
 
42
60
  const activate = (next: number) => {
@@ -82,7 +100,8 @@ export const Tabs = ({
82
100
  hidden={current !== i}
83
101
  className={`${panelClassName} mt-3`}
84
102
  >
85
- {current === i && t.panel}
103
+ {(current === i || (keepMountedVisited && visitedTabs.has(i))) &&
104
+ t.panel}
86
105
  </div>
87
106
  ))}
88
107
  </div>