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
@@ -1,20 +1,62 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
2
- import { createPortal } from "react-dom";
3
- import { useIsMobile } from "../_core/hooks/useIsMobile";
4
- import { Card } from "../card/Card";
1
+ import {
2
+ useCallback,
3
+ useDeferredValue,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
5
10
  import { TypeInputEnum } from "../form/form.enum";
6
11
  import { Input } from "../form/input/Input";
7
- import { Icon } from "../icon/Icon";
8
12
  import { Loader } from "../loader/Loader";
13
+ import { Select } from "../select/Select";
9
14
  import "./dataTable.css";
10
15
  import {
11
- dataTableActionsInterface,
12
- dataTableColumnsInterface,
16
+ DataTableActionsInterface,
17
+ DataTableColumnsInterface,
13
18
  } from "./dataTable.interface";
14
19
  import { DataTableAction } from "./dataTableActions/DataTableAction";
15
20
  import { DataTableColumn } from "./dataTableColumn/DataTableColumn";
21
+ import { DataTableColumnFilter } from "./dataTableColumn/dataTableColumnFilter/DataTableColumnFilter";
22
+ import { DataTableColumnFilterDateRange } from "./dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange";
23
+ import { DataTableColumnSort } from "./dataTableColumn/dataTableColumnSort/DataTableColumnSort";
24
+ import { DataTableFilters } from "./dataTableFilters/DataTableFilters";
16
25
  import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
17
26
  import { DataTableRow } from "./dataTableRow/DataTableRow";
27
+ export type DataTableDateRangeFilterValue = {
28
+ startDate: string;
29
+ endDate: string;
30
+ };
31
+
32
+ export type FilterValue =
33
+ | string
34
+ | number
35
+ | boolean
36
+ | string[]
37
+ | DataTableDateRangeFilterValue
38
+ | null;
39
+ export type DataTableFilterState = Record<string, FilterValue>;
40
+ export type DataTableSortDirection = "asc" | "desc";
41
+ export type DataTableSortState = {
42
+ key: string;
43
+ direction: DataTableSortDirection;
44
+ };
45
+ const DEFAULT_FILTER_COLUMN_WIDTH = 100;
46
+ const SELECTION_COLUMN_WIDTH = 40;
47
+ const DEFAULT_FILTER_ROW_STICKY_HEIGHT = 44;
48
+
49
+ const isDateRangeFilterValue = (
50
+ value: FilterValue | undefined,
51
+ ): value is DataTableDateRangeFilterValue => {
52
+ return (
53
+ typeof value === "object" &&
54
+ value !== null &&
55
+ "startDate" in value &&
56
+ "endDate" in value
57
+ );
58
+ };
59
+
18
60
  export type DataTableProps<T> = {
19
61
  id: string;
20
62
  name?: string;
@@ -23,28 +65,20 @@ export type DataTableProps<T> = {
23
65
  element?: JSX.Element;
24
66
  data: T[];
25
67
  emptyMessage?: string;
26
- actionsColumnWidth?: number;
27
- actions?: dataTableActionsInterface<T>[];
28
- footerAction?: (props: {
29
- close: () => void;
30
- selectedRows: T[];
31
- }) => JSX.Element;
32
- columns: dataTableColumnsInterface<T>[];
68
+ actions?: DataTableActionsInterface<T>[];
69
+ columns: DataTableColumnsInterface<T>[];
70
+ onFiltersChange?: (next: DataTableFilterState) => void;
71
+ onSortChange?: (next: DataTableSortState | null) => void;
33
72
  loading?: boolean;
34
73
  onRowClick?: (row: any) => void;
35
74
  onColumnClick?: (column: any) => void;
75
+ onEditColumnByOverlay?: (row: T) => void;
36
76
  onLoadMore?: () => void;
37
77
  hasMore?: boolean; // pour savoir s'il reste des données à charger
38
78
  onSelectionChange?: (selectedRows: T[]) => void;
39
- autoFocusFirstEditable?: boolean;
40
79
  hideToggleAll?: boolean;
41
80
  limitSelect?: number;
42
81
  getRowId?: (row: T) => string | number;
43
- onRowStyle?: (row: T) => React.CSSProperties | undefined;
44
- onRowClassName?: (row: T) => string | undefined;
45
- rowStyle?: (row: T) => React.CSSProperties | undefined;
46
- rowClassName?: (row: T) => string | undefined;
47
- mobileCardView?: boolean;
48
82
  };
49
83
  export function DataTable<T>(props: DataTableProps<T>) {
50
84
  const {
@@ -54,82 +88,169 @@ export function DataTable<T>(props: DataTableProps<T>) {
54
88
  children,
55
89
  element,
56
90
  data = [],
57
- actionsColumnWidth = 200,
58
91
  actions = [],
59
- footerAction,
60
92
  columns = [],
61
93
  loading,
62
94
  onRowClick,
63
95
  onColumnClick,
64
96
  onLoadMore,
97
+ onEditColumnByOverlay,
65
98
  emptyMessage,
66
99
  hasMore,
67
100
  onSelectionChange,
68
- autoFocusFirstEditable = false,
101
+ onFiltersChange,
102
+ onSortChange,
69
103
  hideToggleAll = false,
70
104
  limitSelect = undefined,
71
105
  getRowId,
72
- onRowStyle,
73
- onRowClassName,
74
- rowStyle,
75
- rowClassName,
76
- mobileCardView = false,
77
106
  } = props;
78
-
79
- const isMobile = useIsMobile();
80
- const showMobileCardView = mobileCardView && isMobile;
81
- const loaderRef = useRef<HTMLDivElement | null>(null);
82
- const footerActionRef = useRef<HTMLDivElement | null>(null);
83
- const hasAutoFocusedRef = useRef(false);
84
- const [isFooterActionVisible, setIsFooterActionVisible] = useState(false);
85
- const [footerActionOffset, setFooterActionOffset] = useState(0);
86
- const [selectedRowIds, setSelectedRowIds] = useState<Set<string | number>>(
107
+ const [currentFilters, setCurrentFilters] = useState<DataTableFilterState>(
108
+ {},
109
+ );
110
+ const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(
87
111
  new Set(),
88
112
  );
113
+ const [focusFilterKey, setFocusFilterKey] = useState<string | null>(null);
114
+ const [currentSort, setCurrentSort] = useState<DataTableSortState | null>(
115
+ null,
116
+ );
89
117
 
90
- const focusEditableElement = useCallback((element: HTMLElement | null) => {
91
- if (!element) {
92
- return;
93
- }
118
+ const getFilterKey = useCallback(
119
+ (column: DataTableColumnsInterface<T>) => column.name ?? column.id,
120
+ [],
121
+ );
94
122
 
95
- element.scrollIntoView({ block: "nearest", inline: "nearest" });
123
+ const toggleColumnFilterVisibility = useCallback(
124
+ (column: DataTableColumnsInterface<T>) => {
125
+ if (!column.filterable) return;
126
+ const key = getFilterKey(column);
127
+ setVisibleFilterColumns((prev) => {
128
+ const next = new Set(prev);
129
+ if (next.has(key)) {
130
+ next.delete(key);
131
+ setFocusFilterKey(null);
132
+ } else {
133
+ next.add(key);
134
+ setFocusFilterKey(key);
135
+ }
136
+ return next;
137
+ });
138
+ },
139
+ [getFilterKey],
140
+ );
141
+
142
+ const toggleColumnSort = useCallback(
143
+ (column: DataTableColumnsInterface<T>) => {
144
+ if (!column.sortable) return;
145
+ const key = getFilterKey(column);
146
+
147
+ setCurrentSort((prev) => {
148
+ if (!prev || prev.key !== key) {
149
+ return { key, direction: "asc" };
150
+ }
151
+
152
+ if (prev.direction === "asc") {
153
+ return { key, direction: "desc" };
154
+ }
96
155
 
97
- requestAnimationFrame(() => {
98
- element.focus();
156
+ return null;
157
+ });
158
+ },
159
+ [getFilterKey],
160
+ );
161
+
162
+ const updateFilterValue = useCallback((key: string, value: string) => {
163
+ setCurrentFilters((prev) => {
164
+ if (!value) {
165
+ if (!(key in prev)) {
166
+ return prev;
167
+ }
168
+ const { [key]: _removed, ...rest } = prev;
169
+ return rest;
170
+ }
99
171
 
100
- if (
101
- element instanceof HTMLInputElement ||
102
- element instanceof HTMLTextAreaElement
103
- ) {
104
- element.select();
172
+ if (prev[key] === value) {
173
+ return prev;
105
174
  }
175
+
176
+ return { ...prev, [key]: value };
106
177
  });
107
178
  }, []);
108
179
 
109
- const isInteractiveClick = useCallback((target: EventTarget | null) => {
110
- const element =
111
- target instanceof Element
112
- ? target
113
- : target instanceof Node
114
- ? target.parentElement
115
- : null;
180
+ const updateDateRangeFilterValue = useCallback(
181
+ (key: string, startDate: string, endDate: string) => {
182
+ setCurrentFilters((prev) => {
183
+ if (!startDate && !endDate) {
184
+ if (!(key in prev)) {
185
+ return prev;
186
+ }
187
+ const { [key]: _removed, ...rest } = prev;
188
+ return rest;
189
+ }
190
+
191
+ const previousValue = prev[key];
192
+ if (
193
+ isDateRangeFilterValue(previousValue) &&
194
+ previousValue.startDate === startDate &&
195
+ previousValue.endDate === endDate
196
+ ) {
197
+ return prev;
198
+ }
199
+
200
+ return {
201
+ ...prev,
202
+ [key]: {
203
+ startDate,
204
+ endDate,
205
+ },
206
+ };
207
+ });
208
+ },
209
+ [],
210
+ );
116
211
 
117
- if (!element) {
118
- return false;
212
+ const onFiltersChangeRef = useRef(onFiltersChange);
213
+ const deferredFilters = useDeferredValue(currentFilters);
214
+ useEffect(() => {
215
+ onFiltersChangeRef.current = onFiltersChange;
216
+ }, [onFiltersChange]);
217
+
218
+ useEffect(() => {
219
+ if (onFiltersChangeRef.current) {
220
+ onFiltersChangeRef.current(deferredFilters);
119
221
  }
222
+ }, [deferredFilters]);
223
+ const onSortChangeRef = useRef(onSortChange);
224
+ useEffect(() => {
225
+ onSortChangeRef.current = onSortChange;
226
+ }, [onSortChange]);
120
227
 
121
- return Boolean(
122
- element.closest(
123
- "input,button,a,select,textarea,label,option,[role='button'],[role='checkbox'],[role='option'],[contenteditable='true'],[data-stop-row-click='true']",
124
- ),
125
- );
126
- }, []);
228
+ useEffect(() => {
229
+ if (onSortChangeRef.current) {
230
+ onSortChangeRef.current(currentSort);
231
+ }
232
+ }, [currentSort]);
127
233
 
234
+ const getColumnValue = useCallback(
235
+ (row: T, column: DataTableColumnsInterface<T>) => {
236
+ return (row as any)[column.id];
237
+ },
238
+ [],
239
+ );
240
+ const loaderRef = useRef<HTMLDivElement | null>(null);
241
+ const [selectedRowIds, setSelectedRowIds] = useState<Set<string | number>>(
242
+ new Set(),
243
+ );
244
+ const filterRowRef = useRef<HTMLTableRowElement | null>(null);
245
+ const headerRowRef = useRef<HTMLTableRowElement | null>(null);
246
+ const [filterRowHeight, setFilterRowHeight] = useState(
247
+ DEFAULT_FILTER_ROW_STICKY_HEIGHT,
248
+ );
249
+ const [headerRowHeight, setHeaderRowHeight] = useState(0);
128
250
  const toggleSelectAll = useCallback(() => {
129
251
  if (selectedRowIds.size === data.length) {
130
252
  setSelectedRowIds(new Set());
131
253
  onSelectionChange?.([]);
132
- setIsFooterActionVisible(false);
133
254
  } else {
134
255
  let allIds = data.map((d) =>
135
256
  getRowId ? getRowId(d) : ((d as any).id ?? d),
@@ -141,7 +262,6 @@ export function DataTable<T>(props: DataTableProps<T>) {
141
262
 
142
263
  const newSelection = new Set(allIds);
143
264
  setSelectedRowIds(newSelection);
144
- setIsFooterActionVisible(true);
145
265
  onSelectionChange?.(
146
266
  data.filter((d) =>
147
267
  newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
@@ -150,175 +270,14 @@ export function DataTable<T>(props: DataTableProps<T>) {
150
270
  }
151
271
  }, [data, onSelectionChange, limitSelect, getRowId, selectedRowIds.size]);
152
272
 
153
- const focusNextColumn = useCallback(
154
- (columnIndex: number, rowIndex: number) => {
155
- if (typeof document === "undefined") {
156
- return;
157
- }
158
-
159
- const table =
160
- document.getElementById(`${id}-mobile`) ??
161
- document.getElementById(id);
162
- const eligibleColumnIndexes = columns.reduce<number[]>(
163
- (indexes, column, idx) => {
164
- if (
165
- column.editable &&
166
- column.enterToNextColumn &&
167
- column.type !== "select" &&
168
- column.type !== "image"
169
- ) {
170
- indexes.push(idx);
171
- }
172
-
173
- return indexes;
174
- },
175
- [],
176
- );
177
-
178
- if (!table || eligibleColumnIndexes.length === 0) {
179
- return;
180
- }
181
-
182
- const currentEligibleIndex = eligibleColumnIndexes.indexOf(columnIndex);
183
-
184
- if (currentEligibleIndex === -1) {
185
- return;
186
- }
187
-
188
- const editableCells = data.flatMap((_, currentRowIndex) =>
189
- eligibleColumnIndexes.map((currentColumnIndex) => ({
190
- rowIndex: currentRowIndex,
191
- columnIndex: currentColumnIndex,
192
- })),
193
- );
194
-
195
- const currentCellIndex = editableCells.findIndex(
196
- (cell) =>
197
- cell.rowIndex === rowIndex && cell.columnIndex === columnIndex,
198
- );
199
-
200
- if (currentCellIndex === -1) {
201
- return;
202
- }
203
-
204
- const focusableSelector =
205
- 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]';
206
- let nextInput: HTMLElement | null = null;
207
-
208
- for (let offset = 1; offset <= editableCells.length; offset += 1) {
209
- const nextCell =
210
- editableCells[(currentCellIndex + offset) % editableCells.length];
211
- const cell = table.querySelector(
212
- `tbody td[data-row-index="${nextCell.rowIndex}"][data-column-index="${nextCell.columnIndex}"]`,
213
- );
214
-
215
- if (!(cell instanceof HTMLElement)) {
216
- continue;
217
- }
218
-
219
- const focusableElement =
220
- cell.querySelector<HTMLElement>(focusableSelector);
221
-
222
- if (focusableElement) {
223
- nextInput = focusableElement;
224
- break;
225
- }
226
- }
227
-
228
- if (!nextInput) {
229
- return;
230
- }
231
-
232
- focusEditableElement(nextInput);
233
- },
234
- [columns, data, focusEditableElement, id],
235
- );
236
-
237
- const focusNextRow = useCallback(
238
- (rowIndex: number) => {
239
- if (typeof document === "undefined") {
240
- return;
241
- }
242
-
243
- const table =
244
- document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
245
- if (!table) {
246
- return;
247
- }
248
-
249
- const eligibleColumnIndexes = columns.reduce<number[]>(
250
- (indexes, column, idx) => {
251
- if (
252
- column.editable &&
253
- column.type !== "select" &&
254
- column.type !== "image"
255
- ) {
256
- indexes.push(idx);
257
- }
258
-
259
- return indexes;
260
- },
261
- [],
262
- );
263
-
264
- if (eligibleColumnIndexes.length === 0) {
265
- return;
266
- }
267
-
268
- const firstEligibleColumnIndex = eligibleColumnIndexes[0];
269
- const focusableSelector =
270
- 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]';
271
- let nextInput: HTMLElement | null = null;
272
-
273
- for (let offset = 1; offset <= data.length; offset += 1) {
274
- const nextRowIndex = (rowIndex + offset) % data.length;
275
- const cell = table.querySelector(
276
- `[data-row-index="${nextRowIndex}"][data-column-index="${firstEligibleColumnIndex}"]`,
277
- );
278
-
279
- if (!(cell instanceof HTMLElement)) {
280
- continue;
281
- }
282
-
283
- const focusableElement =
284
- cell.querySelector<HTMLElement>(focusableSelector);
285
-
286
- if (focusableElement) {
287
- nextInput = focusableElement;
288
- break;
289
- }
290
- }
291
-
292
- if (!nextInput) {
293
- return;
294
- }
295
-
296
- focusEditableElement(nextInput);
297
- },
298
- [columns, data.length, focusEditableElement, id],
299
- );
300
-
301
273
  const handleClickRow = useCallback(
302
274
  (e: React.MouseEvent, row: T) => {
303
275
  e.stopPropagation();
304
-
305
- if (isInteractiveClick(e.target)) {
306
- return;
307
- }
308
-
309
- const selectedText =
310
- typeof window !== "undefined"
311
- ? window.getSelection?.()?.toString().trim()
312
- : "";
313
- if (selectedText) {
314
- return;
315
- }
316
-
317
276
  if (onRowClick) {
318
277
  onRowClick?.(row);
319
278
  }
320
279
  },
321
- [isInteractiveClick, onRowClick],
280
+ [onRowClick],
322
281
  );
323
282
  const toggleRow = useCallback(
324
283
  (e: React.ChangeEvent<HTMLInputElement>, row: T) => {
@@ -337,7 +296,6 @@ export function DataTable<T>(props: DataTableProps<T>) {
337
296
  }
338
297
 
339
298
  setSelectedRowIds(newSelection);
340
- setIsFooterActionVisible(newSelection.size > 0);
341
299
  const selectedItems = data.filter((d) =>
342
300
  newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
343
301
  );
@@ -345,238 +303,388 @@ export function DataTable<T>(props: DataTableProps<T>) {
345
303
  },
346
304
  [data, getRowId, limitSelect, onSelectionChange, selectedRowIds],
347
305
  );
348
- const showFooterAction = Boolean(footerAction && isFooterActionVisible);
349
- const closeFooterAction = useCallback(() => {
350
- setIsFooterActionVisible(false);
351
- }, []);
352
- const selectedRows = data.filter((d) =>
353
- selectedRowIds.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
354
- );
355
306
  useEffect(() => {
356
- if (loading || !autoFocusFirstEditable || data.length === 0) {
357
- hasAutoFocusedRef.current = false;
358
- return;
359
- }
360
-
361
- if (hasAutoFocusedRef.current) {
362
- return;
363
- }
307
+ if (!onLoadMore || !hasMore) return;
364
308
 
365
- const firstEditableColumnIndex = columns.findIndex(
366
- (column) =>
367
- column.editable &&
368
- column.enterToNextColumn &&
369
- column.type !== "select" &&
370
- column.type !== "image",
309
+ const observer = new IntersectionObserver(
310
+ (entries) => {
311
+ if (entries[0].isIntersecting) {
312
+ onLoadMore();
313
+ }
314
+ },
315
+ {
316
+ root: null,
317
+ rootMargin: "0px",
318
+ threshold: 1.0,
319
+ },
371
320
  );
372
321
 
373
- if (firstEditableColumnIndex === -1) {
374
- return;
322
+ if (loaderRef.current) {
323
+ observer.observe(loaderRef.current);
375
324
  }
376
325
 
377
- const table =
378
- document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
379
- const firstCell = table?.querySelector(
380
- `[data-row-index="0"][data-column-index="${firstEditableColumnIndex}"]`,
381
- );
326
+ return () => {
327
+ if (loaderRef.current) {
328
+ observer.unobserve(loaderRef.current);
329
+ }
330
+ };
331
+ }, [onLoadMore, hasMore]);
332
+ const hasNestedColumns = columns.some((col) => col.children?.length);
382
333
 
383
- if (!(firstCell instanceof HTMLElement)) {
384
- return;
334
+ const flatColumns = columns.flatMap((col) =>
335
+ col.children?.length ? col.children : [col],
336
+ );
337
+
338
+ const stickyColumnsLeftByKey = useMemo(() => {
339
+ const offsets = new Map<string, number>();
340
+ const hasSelectionColumn = !!onSelectionChange;
341
+ let currentLeft = hasSelectionColumn ? SELECTION_COLUMN_WIDTH : 0;
342
+
343
+ flatColumns.forEach((column) => {
344
+ const key = getFilterKey(column);
345
+ const width =
346
+ column.width ??
347
+ column.minWidth ??
348
+ column.maxWidth ??
349
+ DEFAULT_FILTER_COLUMN_WIDTH;
350
+
351
+ if (column.sticky) {
352
+ offsets.set(key, currentLeft);
353
+ currentLeft += width;
354
+ }
355
+ });
356
+
357
+ return offsets;
358
+ }, [flatColumns, getFilterKey, onSelectionChange]);
359
+
360
+ const hasStickyColumns = stickyColumnsLeftByKey.size > 0;
361
+
362
+ const sortedData = useMemo(() => {
363
+ if (!currentSort) {
364
+ return data;
385
365
  }
386
366
 
387
- const firstInput = firstCell.querySelector<HTMLElement>(
388
- 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]',
367
+ const sortedColumn = flatColumns.find(
368
+ (column) => getFilterKey(column) === currentSort.key,
389
369
  );
390
370
 
391
- if (!firstInput) {
392
- return;
371
+ if (!sortedColumn) {
372
+ return data;
393
373
  }
394
374
 
395
- hasAutoFocusedRef.current = true;
396
- focusEditableElement(firstInput);
397
- }, [
398
- autoFocusFirstEditable,
399
- columns,
400
- data.length,
401
- focusEditableElement,
402
- id,
403
- loading,
404
- ]);
405
- useEffect(() => {
406
- if (!showFooterAction) {
407
- setFooterActionOffset(0);
408
- return;
409
- }
375
+ const factor = currentSort.direction === "asc" ? 1 : -1;
376
+ const isDateColumn = sortedColumn.type === TypeInputEnum.DATE;
377
+
378
+ const normalizeDate = (value: unknown): number | null => {
379
+ if (value instanceof Date) {
380
+ return value.getTime();
381
+ }
410
382
 
411
- const footerActionElement = footerActionRef.current;
383
+ if (typeof value === "string" || typeof value === "number") {
384
+ const timestamp = new Date(value).getTime();
385
+ return Number.isNaN(timestamp) ? null : timestamp;
386
+ }
412
387
 
413
- if (!footerActionElement) {
414
- return;
415
- }
388
+ return null;
389
+ };
390
+
391
+ const compareValues = (left: unknown, right: unknown): number => {
392
+ if (left == null && right == null) {
393
+ return 0;
394
+ }
395
+
396
+ if (left == null) {
397
+ return 1;
398
+ }
399
+
400
+ if (right == null) {
401
+ return -1;
402
+ }
403
+
404
+ if (typeof left === "number" && typeof right === "number") {
405
+ return left - right;
406
+ }
416
407
 
417
- const updateFooterActionOffset = () => {
418
- const { height } = footerActionElement.getBoundingClientRect();
419
- setFooterActionOffset(Math.ceil(height) + 16);
408
+ if (typeof left === "boolean" && typeof right === "boolean") {
409
+ return Number(left) - Number(right);
410
+ }
411
+
412
+ if (isDateColumn) {
413
+ const leftDate = normalizeDate(left);
414
+ const rightDate = normalizeDate(right);
415
+
416
+ if (leftDate !== null && rightDate !== null) {
417
+ return leftDate - rightDate;
418
+ }
419
+ }
420
+
421
+ return String(left).localeCompare(String(right), undefined, {
422
+ numeric: true,
423
+ sensitivity: "base",
424
+ });
420
425
  };
421
426
 
422
- updateFooterActionOffset();
427
+ return [...data].sort((leftRow, rightRow) => {
428
+ const leftValue = getColumnValue(leftRow, sortedColumn);
429
+ const rightValue = getColumnValue(rightRow, sortedColumn);
430
+ return factor * compareValues(leftValue, rightValue);
431
+ });
432
+ }, [currentSort, data, flatColumns, getColumnValue, getFilterKey]);
433
+
434
+ // Largeur de la colonne des actions : somme des largeurs déclarées, 200px par défaut
435
+ const actionsWidth = actions.some((action) => action.width !== undefined)
436
+ ? actions.reduce((total, action) => total + (action.width ?? 0), 0)
437
+ : 200;
423
438
 
424
- if (typeof ResizeObserver === "undefined") {
439
+ const hasVisibleFilterInputs = visibleFilterColumns.size > 0;
440
+ const headerStickyTop = hasVisibleFilterInputs ? filterRowHeight : 0;
441
+ const groupedHeaderStickyTop = headerStickyTop + headerRowHeight;
442
+
443
+ useLayoutEffect(() => {
444
+ if (!hasVisibleFilterInputs) {
445
+ setFilterRowHeight(DEFAULT_FILTER_ROW_STICKY_HEIGHT);
425
446
  return;
426
447
  }
427
448
 
428
- const resizeObserver = new ResizeObserver(() => {
429
- updateFooterActionOffset();
430
- });
449
+ const updateFilterRowHeight = () => {
450
+ const measuredHeight =
451
+ filterRowRef.current?.getBoundingClientRect().height ??
452
+ DEFAULT_FILTER_ROW_STICKY_HEIGHT;
453
+ setFilterRowHeight((prev) =>
454
+ Math.abs(prev - measuredHeight) < 0.1 ? prev : measuredHeight,
455
+ );
456
+ };
431
457
 
432
- resizeObserver.observe(footerActionElement);
458
+ updateFilterRowHeight();
433
459
 
460
+ if (typeof ResizeObserver !== "undefined" && filterRowRef.current) {
461
+ const resizeObserver = new ResizeObserver(updateFilterRowHeight);
462
+ resizeObserver.observe(filterRowRef.current);
463
+ return () => resizeObserver.disconnect();
464
+ }
465
+
466
+ window.addEventListener("resize", updateFilterRowHeight);
434
467
  return () => {
435
- resizeObserver.disconnect();
468
+ window.removeEventListener("resize", updateFilterRowHeight);
469
+ };
470
+ }, [hasVisibleFilterInputs]);
471
+
472
+ useLayoutEffect(() => {
473
+ const updateHeaderRowHeight = () => {
474
+ const measuredHeight =
475
+ headerRowRef.current?.getBoundingClientRect().height ?? 0;
476
+ setHeaderRowHeight((prev) =>
477
+ Math.abs(prev - measuredHeight) < 0.1 ? prev : measuredHeight,
478
+ );
436
479
  };
437
- }, [showFooterAction]);
438
- useEffect(() => {
439
- if (!onLoadMore || !hasMore) return;
440
480
 
441
- const observer = new IntersectionObserver(
442
- (entries) => {
443
- if (entries[0].isIntersecting) {
444
- onLoadMore();
445
- }
446
- },
447
- {
448
- root: null,
449
- rootMargin: "0px",
450
- threshold: 1.0,
451
- },
452
- );
481
+ updateHeaderRowHeight();
453
482
 
454
- if (loaderRef.current) {
455
- observer.observe(loaderRef.current);
483
+ if (typeof ResizeObserver !== "undefined" && headerRowRef.current) {
484
+ const resizeObserver = new ResizeObserver(updateHeaderRowHeight);
485
+ resizeObserver.observe(headerRowRef.current);
486
+ return () => resizeObserver.disconnect();
456
487
  }
457
488
 
489
+ window.addEventListener("resize", updateHeaderRowHeight);
458
490
  return () => {
459
- if (loaderRef.current) {
460
- observer.unobserve(loaderRef.current);
461
- }
491
+ window.removeEventListener("resize", updateHeaderRowHeight);
462
492
  };
463
- }, [onLoadMore, hasMore]);
493
+ }, [columns.length, hasVisibleFilterInputs]);
494
+
495
+ const filterRowCells = useMemo(
496
+ () =>
497
+ flatColumns.map((column, idx) => {
498
+ const key = getFilterKey(column);
499
+ const isVisible = visibleFilterColumns.has(key);
500
+ const currentFilterValue = currentFilters[key];
501
+ const currentValue = isDateRangeFilterValue(currentFilterValue)
502
+ ? ""
503
+ : String(currentFilterValue ?? "");
504
+ const currentDateRangeValue = isDateRangeFilterValue(currentFilterValue)
505
+ ? currentFilterValue
506
+ : { startDate: "", endDate: "" };
507
+ const shouldAutoFocus = focusFilterKey === key && isVisible;
508
+ const columnWidth = column.width ?? DEFAULT_FILTER_COLUMN_WIDTH;
509
+ const stickyLeft = stickyColumnsLeftByKey.get(key);
510
+ const isSticky = stickyLeft !== undefined;
511
+
512
+ const onTextDateChange = (e: React.ChangeEvent<HTMLInputElement>) =>
513
+ updateFilterValue(key, e.target.value);
514
+
515
+ const onEnumSelectMultipleChange = (values: string[]) =>
516
+ updateFilterValue(key, values.join(","));
517
+
518
+ return (
519
+ <th
520
+ key={`filter-${column.id}-${idx}`}
521
+ className={`filter-row-cell ${
522
+ isSticky ? "data-table-sticky-cell" : ""
523
+ }`.trim()}
524
+ style={{
525
+ width: columnWidth,
526
+ minWidth: columnWidth,
527
+ maxWidth: columnWidth,
528
+ position: "sticky",
529
+ top: 0,
530
+ zIndex: isSticky ? 14 : 13,
531
+ backgroundColor: "rgb(249, 251, 252)",
532
+ ...(isSticky
533
+ ? {
534
+ left: stickyLeft,
535
+ }
536
+ : {}),
537
+ }}
538
+ >
539
+ {isVisible && column.filterable && (
540
+ <div className="data-table-filter-input-wrapper">
541
+ {column.filterType === TypeInputEnum.DATE ? (
542
+ <Input
543
+ id={`${id}-filter-date-${column.id}`}
544
+ name={`${id}-filter-date-${column.id}`}
545
+ type={TypeInputEnum.DATE}
546
+ value={currentValue}
547
+ autoFocus={shouldAutoFocus}
548
+ disabledDebounce={true}
549
+ showClearButton
550
+ onChange={onTextDateChange}
551
+ onClick={(e) => e.stopPropagation()}
552
+ className="data-table-filter-input"
553
+ />
554
+ ) : column.filterType === TypeInputEnum.DATERANGE ? (
555
+ <DataTableColumnFilterDateRange
556
+ id={`${id}-filter-date-range-${column.id}`}
557
+ name={`${id}-filter-date-range-${column.id}`}
558
+ currentStartValue={currentDateRangeValue.startDate}
559
+ currentEndValue={currentDateRangeValue.endDate}
560
+ onChange={(startDate, endDate) =>
561
+ updateDateRangeFilterValue(key, startDate, endDate)
562
+ }
563
+ />
564
+ ) : column.filterType === TypeInputEnum.RADIO ||
565
+ column.filterType === TypeInputEnum.CHECKBOX ? (
566
+ <div onClick={(e) => e.stopPropagation()}>
567
+ <Select
568
+ id={`${id}-filter-${column.id}`}
569
+ options={column.filterDataEnum ?? []}
570
+ multiple={true}
571
+ showOptions={shouldAutoFocus}
572
+ value={currentValue.split(",").filter(Boolean)}
573
+ onChangeMultiple={onEnumSelectMultipleChange}
574
+ placeholder={`Filtrer ${column.label}`}
575
+ className="data-table-filter-select"
576
+ />
577
+ </div>
578
+ ) : (
579
+ <Input
580
+ id={`${id}-filter-text-${column.id}`}
581
+ name={`${id}-filter-text-${column.id}`}
582
+ type={TypeInputEnum.TEXT}
583
+ value={currentValue}
584
+ showClearButton
585
+ autoFocus={shouldAutoFocus}
586
+ disabledDebounce={true}
587
+ onChange={onTextDateChange}
588
+ onClick={(e) => e.stopPropagation()}
589
+ className="data-table-filter-input"
590
+ placeholder={`Filtrer ${column.label}`}
591
+ />
592
+ )}
593
+ </div>
594
+ )}
595
+ </th>
596
+ );
597
+ }),
598
+ [
599
+ currentFilters,
600
+ flatColumns,
601
+ focusFilterKey,
602
+ getFilterKey,
603
+ id,
604
+ stickyColumnsLeftByKey,
605
+ updateDateRangeFilterValue,
606
+ updateFilterValue,
607
+ visibleFilterColumns,
608
+ ],
609
+ );
464
610
  return (
465
- <div
466
- className={`data-table-container ${showFooterAction ? "data-table-container--with-footer-action" : ""}`}
467
- style={
468
- showFooterAction
469
- ? { paddingBottom: `${footerActionOffset}px` }
470
- : undefined
471
- }
472
- >
611
+ <div className="data-table-container">
473
612
  {name && <h2 className="mb-2">{name}</h2>}
474
613
  {element}
475
- {showMobileCardView && (
476
- <div
477
- id={`${id}-mobile`}
478
- className={`data-table-mobile-cards ${className ? className : ""}`}
479
- >
480
- {loading && <Loader display={loading} />}
481
- {!loading &&
482
- data.map((row, rowIndex) => {
483
- const customStyle = (onRowStyle ?? rowStyle)?.(row);
484
- const customClass =
485
- (onRowClassName ?? rowClassName)?.(row) ?? "";
486
- const rowId = getRowId
487
- ? getRowId(row)
488
- : ((row as any).id ?? row);
489
-
490
- return (
491
- <Card
492
- key={rowId as React.Key}
493
- item={{ title: "" }}
494
- className={`data-table-mobile-card ${customClass}`.trim()}
495
- handleClick={onRowClick ? () => onRowClick(row) : undefined}
496
- >
497
- <div
498
- className="data-table-mobile-card__content"
499
- style={customStyle}
500
- data-row-index={rowIndex}
501
- onMouseDown={(event) => {
502
- if (isInteractiveClick(event.target)) {
503
- event.stopPropagation();
504
- }
505
- }}
506
- >
507
- {onSelectionChange && (
508
- <div className="data-table-mobile-card__field data-table-mobile-card__selection">
509
- <span className="data-table-mobile-card__label">
510
- Sélection
511
- </span>
512
- <Input
513
- id={`select-card-${rowIndex}`}
514
- name={`select-card-${rowIndex}`}
515
- type={TypeInputEnum.CHECKBOX}
516
- disabled={
517
- limitSelect !== undefined &&
518
- selectedRowIds.size >= limitSelect &&
519
- !selectedRowIds.has(rowId)
520
- }
521
- checked={selectedRowIds.has(rowId)}
522
- onChange={(event) => toggleRow(event, row)}
523
- />
524
- </div>
525
- )}
526
- {columns.map((column, columnIndex) => (
527
- <div
528
- className="data-table-mobile-card__field"
529
- key={column.id}
530
- >
531
- <span className="data-table-mobile-card__label">
532
- {column.label}
533
- </span>
534
- <DataTableRow
535
- container="card"
536
- column={column}
537
- idx={columnIndex}
538
- row={row}
539
- rowIndex={rowIndex}
540
- onEnterNextColumn={focusNextColumn}
541
- />
542
- </div>
543
- ))}
544
- {actions.length > 0 && (
545
- <div className="data-table-mobile-card__actions card-actions">
546
- {actions.map((action, actionIndex) => (
547
- <DataTableAction
548
- key={action.id}
549
- action={action}
550
- row={row}
551
- rowIndex={rowIndex}
552
- index={actionIndex}
553
- onFocusNextRow={focusNextRow}
554
- />
555
- ))}
556
- </div>
557
- )}
558
- </div>
559
- </Card>
560
- );
561
- })}
562
- {!loading && data.length === 0 && (
563
- <div className="data-table-mobile-cards__empty">
564
- {emptyMessage ?? "Aucune donnée"}
565
- </div>
566
- )}
567
- </div>
614
+ {currentFilters && (
615
+ <DataTableFilters
616
+ currentFilters={currentFilters}
617
+ setCurrentFilters={setCurrentFilters}
618
+ />
568
619
  )}
569
- <table
570
- className={`data-table ${className ? className : ""}`}
571
- id={id}
572
- style={showMobileCardView ? { display: "none" } : undefined}
573
- >
620
+ <table className={`data-table ${className ? className : ""}`} id={id}>
621
+ <colgroup>
622
+ {onSelectionChange && <col style={{ width: 20 }} />}
623
+ {flatColumns.map((column, idx) => (
624
+ <col
625
+ key={`col-${column.id}-${idx}`}
626
+ style={{
627
+ width: column.width,
628
+ minWidth: column.minWidth,
629
+ maxWidth: column.maxWidth,
630
+ }}
631
+ />
632
+ ))}
633
+ {actions.length > 0 && <col style={{ width: actionsWidth }} />}
634
+ </colgroup>
574
635
  <thead>
575
- <tr>
636
+ {hasVisibleFilterInputs && (
637
+ <tr className="data-table-filter-row" ref={filterRowRef}>
638
+ {onSelectionChange && (
639
+ <th
640
+ className={`select-cell ${
641
+ hasStickyColumns ? "data-table-sticky-cell" : ""
642
+ }`}
643
+ style={{
644
+ position: "sticky",
645
+ top: 0,
646
+ zIndex: hasStickyColumns ? 14 : 13,
647
+ backgroundColor: "#f9fbfc",
648
+ ...(hasStickyColumns
649
+ ? {
650
+ left: 0,
651
+ }
652
+ : {}),
653
+ }}
654
+ ></th>
655
+ )}
656
+ {filterRowCells}
657
+ {actions.length > 0 && (
658
+ <th
659
+ style={{
660
+ position: "sticky",
661
+ top: 0,
662
+ zIndex: 13,
663
+ backgroundColor: "#f9fbfc",
664
+ }}
665
+ ></th>
666
+ )}
667
+ </tr>
668
+ )}
669
+ <tr className="data-table-header-row" ref={headerRowRef}>
576
670
  {onSelectionChange && (
577
671
  <th
578
- className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
579
- style={{ width: "20px", textAlign: "center" }}
672
+ className={`select-cell ${hideToggleAll ? "hidden" : ""} ${
673
+ hasStickyColumns ? "data-table-sticky-cell" : ""
674
+ }`.trim()}
675
+ style={{
676
+ width: "20px",
677
+ textAlign: "center",
678
+ top: headerStickyTop,
679
+ ...(hasStickyColumns
680
+ ? {
681
+ position: "sticky",
682
+ left: 0,
683
+ zIndex: 6,
684
+ backgroundColor: "#fff",
685
+ }
686
+ : {}),
687
+ }}
580
688
  >
581
689
  <Input
582
690
  id={`select-th-${id}`}
@@ -589,151 +697,186 @@ export function DataTable<T>(props: DataTableProps<T>) {
589
697
  />
590
698
  </th>
591
699
  )}
700
+
592
701
  {columns.map((column, idx) => (
593
- <DataTableColumn
702
+ <DataTableColumn<T>
594
703
  key={idx}
704
+ hasNestedColumns={hasNestedColumns}
595
705
  column={column}
596
706
  idx={idx}
597
707
  onColumnClick={onColumnClick}
708
+ isFilterVisible={visibleFilterColumns.has(getFilterKey(column))}
709
+ onToggleFilterVisibility={toggleColumnFilterVisibility}
710
+ currentSortDirection={
711
+ currentSort?.key === getFilterKey(column)
712
+ ? currentSort.direction
713
+ : null
714
+ }
715
+ onToggleSort={toggleColumnSort}
716
+ stickyLeft={stickyColumnsLeftByKey.get(getFilterKey(column))}
717
+ stickyTop={headerStickyTop}
598
718
  />
599
719
  ))}
600
720
  {actions.length > 0 && (
601
721
  <th
602
722
  style={{
603
- width: `${actionsColumnWidth}px`,
604
- minWidth: `${actionsColumnWidth}px`,
723
+ width: `${actionsWidth}px`,
724
+ top: headerStickyTop,
605
725
  }}
606
726
  align="right"
607
727
  key={"actions"}
728
+ rowSpan={hasNestedColumns ? 2 : 1}
608
729
  ></th>
609
730
  )}
610
731
  </tr>
611
- </thead>
612
- <tbody>
613
- {loading ? (
732
+ {hasNestedColumns && (
614
733
  <tr>
615
- <td
616
- align="center"
617
- colSpan={columns.length + (actions.length > 0 ? 1 : 0)}
618
- >
619
- <div className="mt-3">
620
- <Loader display={loading} />
621
- </div>
622
- </td>
734
+ {columns.flatMap(
735
+ (col, idx) =>
736
+ col.children?.map(
737
+ (
738
+ child: DataTableColumnsInterface<any>,
739
+ childIdx: number,
740
+ ) => (
741
+ <th
742
+ key={`${idx}-${childIdx}`}
743
+ style={{
744
+ minWidth: child.minWidth,
745
+ maxWidth: child.maxWidth,
746
+ width: child.width,
747
+ textAlign: child.align ?? "left",
748
+ position: "sticky",
749
+ top: groupedHeaderStickyTop,
750
+ zIndex: 11,
751
+ backgroundColor: "#f9fbfc",
752
+ ...(stickyColumnsLeftByKey.has(getFilterKey(child))
753
+ ? {
754
+ left: stickyColumnsLeftByKey.get(
755
+ getFilterKey(child),
756
+ ),
757
+ zIndex: 12,
758
+ }
759
+ : {}),
760
+ }}
761
+ className={
762
+ stickyColumnsLeftByKey.has(getFilterKey(child))
763
+ ? "data-table-sticky-cell"
764
+ : undefined
765
+ }
766
+ onClick={() => onColumnClick?.(child)}
767
+ >
768
+ <div className="d-flex align-items-center">
769
+ <div className="flex-1">{child.label}</div>
770
+ {child.filterable && (
771
+ <DataTableColumnFilter
772
+ column={child}
773
+ isFilterVisible={visibleFilterColumns.has(
774
+ getFilterKey(child),
775
+ )}
776
+ onToggleVisibility={toggleColumnFilterVisibility}
777
+ />
778
+ )}
779
+ {child.sortable && (
780
+ <DataTableColumnSort
781
+ column={child}
782
+ sortDirection={
783
+ currentSort?.key === getFilterKey(child)
784
+ ? currentSort.direction
785
+ : null
786
+ }
787
+ onToggleSort={toggleColumnSort}
788
+ />
789
+ )}
790
+ </div>
791
+ </th>
792
+ ),
793
+ ) ?? [],
794
+ )}
623
795
  </tr>
624
- ) : (
625
- <></>
626
796
  )}
797
+ </thead>
798
+ <tbody>
627
799
  {!loading &&
628
- data.map((row, rowIndex) => {
629
- const customStyle = (onRowStyle ?? rowStyle)?.(row);
630
- const customClass = (onRowClassName ?? rowClassName)?.(row) ?? "";
631
-
632
- return (
633
- <tr
634
- key={rowIndex}
635
- className={`${rowIndex % 2 === 0 ? "even" : "odd"} ${customClass}`.trim()}
636
- style={customStyle}
637
- data-row-index={rowIndex}
638
- onClick={(e) => handleClickRow?.(e, row)}
639
- >
640
- {onSelectionChange && (
641
- <td
642
- className="select-cell"
643
- style={{ textAlign: "center" }}
644
- onClick={(e) => {
645
- e.stopPropagation();
646
- setIsFooterActionVisible(true);
647
- }}
648
- >
649
- <Input
650
- id={`select-row-${rowIndex}`}
651
- name={`select-row-${rowIndex}`}
652
- type={TypeInputEnum.CHECKBOX}
653
- disabled={
654
- limitSelect !== undefined &&
655
- selectedRowIds.size >= limitSelect &&
656
- !selectedRowIds.has(
657
- getRowId ? getRowId(row) : ((row as any).id ?? row),
658
- )
659
- }
660
- checked={selectedRowIds.has(
800
+ sortedData.map((row, rowIndex) => (
801
+ <tr
802
+ key={getRowId ? getRowId(row) : ((row as any).id ?? rowIndex)}
803
+ onClick={(e) => handleClickRow?.(e, row)}
804
+ >
805
+ {onSelectionChange && (
806
+ <td
807
+ className={`select-cell ${
808
+ hasStickyColumns ? "data-table-sticky-cell" : ""
809
+ }`.trim()}
810
+ style={{
811
+ textAlign: "center",
812
+ ...(hasStickyColumns
813
+ ? {
814
+ position: "sticky",
815
+ left: 0,
816
+ zIndex: 3,
817
+ backgroundColor: "#fff",
818
+ }
819
+ : {}),
820
+ }}
821
+ >
822
+ <Input
823
+ id={`select-row-${rowIndex}`}
824
+ name={`select-row-${rowIndex}`}
825
+ type={TypeInputEnum.CHECKBOX}
826
+ disabled={
827
+ limitSelect !== undefined &&
828
+ selectedRowIds.size >= limitSelect &&
829
+ !selectedRowIds.has(
661
830
  getRowId ? getRowId(row) : ((row as any).id ?? row),
662
- )}
663
- onChange={(e) => toggleRow(e, row)}
664
- />
665
- </td>
666
- )}
667
- {columns.map((column, idx) => (
668
- <DataTableRow
669
- column={column}
670
- idx={idx}
671
- row={row}
672
- rowIndex={rowIndex}
673
- key={idx}
674
- onEnterNextColumn={focusNextColumn}
831
+ )
832
+ }
833
+ checked={selectedRowIds.has(
834
+ getRowId ? getRowId(row) : ((row as any).id ?? row),
835
+ )}
836
+ onChange={(e) => toggleRow(e, row)} // empêche le clic de déclencher onRowClick
675
837
  />
676
- ))}
677
- {actions.length > 0 && (
678
- <td
679
- style={{
680
- width: `${actionsColumnWidth}px`,
681
- minWidth: `${actionsColumnWidth}px`,
682
- }}
683
- align="right"
684
- onClick={(e) => e.stopPropagation()}
685
- >
686
- <div className="d-flex align-items-center justify-content-end">
687
- {actions.map((action, index) => (
688
- <DataTableAction
689
- key={index}
690
- action={action}
691
- row={row}
692
- rowIndex={rowIndex}
693
- index={index}
694
- onFocusNextRow={focusNextRow}
695
- />
696
- ))}
697
- </div>
698
- </td>
699
- )}
700
- </tr>
701
- );
702
- })}
703
- {!loading && data.length === 0 && (
704
- <DataTableLoading
705
- columns={columns}
706
- actions={actions}
707
- emptyMessage={emptyMessage}
708
- />
709
- )}
838
+ </td>
839
+ )}
840
+ {flatColumns.map((column, idx) => (
841
+ <DataTableRow
842
+ onEditColumnByOverlay={onEditColumnByOverlay}
843
+ column={column}
844
+ idx={idx}
845
+ row={row}
846
+ key={idx}
847
+ stickyLeft={stickyColumnsLeftByKey.get(
848
+ getFilterKey(column),
849
+ )}
850
+ />
851
+ ))}
852
+ {actions.length > 0 && (
853
+ <td align="right">
854
+ <div className="d-flex align-items-center justify-content-end">
855
+ {actions.map((action, index) => (
856
+ <DataTableAction
857
+ key={index}
858
+ action={action}
859
+ row={row}
860
+ index={index}
861
+ />
862
+ ))}
863
+ </div>
864
+ </td>
865
+ )}
866
+ </tr>
867
+ ))}
710
868
  </tbody>
711
869
  </table>
712
870
 
713
- {showFooterAction &&
714
- typeof document !== "undefined" &&
715
- createPortal(
716
- <div
717
- ref={footerActionRef}
718
- className="data-table-footer-action"
719
- data-footer-action="true"
720
- >
721
- <div className="data-table-footer-action__content">
722
- <div
723
- className="data-table-footer-action__close"
724
- aria-label="Fermer les actions"
725
- onClick={closeFooterAction}
726
- >
727
- <Icon classIcon={"bi-x"} />
728
- </div>
729
- {footerAction?.({
730
- close: closeFooterAction,
731
- selectedRows,
732
- })}
733
- </div>
734
- </div>,
735
- document.body,
736
- )}
871
+ {loading && (
872
+ <div className="data-table-empty-state-content mt-3">
873
+ <Loader display={true} />
874
+ </div>
875
+ )}
876
+
877
+ {!loading && data.length === 0 && (
878
+ <DataTableLoading emptyMessage={emptyMessage} />
879
+ )}
737
880
 
738
881
  {children}
739
882
  {hasMore && (