kelt-ui-kit-react 2.0.3 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +26 -0
  2. package/dist/addressSearch/AddressSearch.d.ts +20 -0
  3. package/dist/addressSearch/AddressSearch.view.d.ts +1 -0
  4. package/dist/badge/Badge.d.ts +1 -3
  5. package/dist/button/Button.d.ts +1 -1
  6. package/dist/button/button.interface.d.ts +2 -1
  7. package/dist/card/Card.d.ts +4 -1
  8. package/dist/dataTable/DataTable.d.ts +14 -17
  9. package/dist/dataTable/dataTable.interface.d.ts +19 -24
  10. package/dist/dataTable/dataTableActions/DataTableAction.d.ts +5 -3
  11. package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +7 -13
  12. package/dist/dataTable/dataTableLoading/DataTableLoading.d.ts +3 -1
  13. package/dist/dataTable/dataTableRow/DataTableRow.d.ts +6 -5
  14. package/dist/expands/Expands.d.ts +1 -0
  15. package/dist/expands/expand/expand.d.ts +3 -2
  16. package/dist/form/Form.d.ts +10 -2
  17. package/dist/form/form.enum.d.ts +3 -2
  18. package/dist/form/form.interface.d.ts +22 -3
  19. package/dist/form/input/Input.d.ts +0 -3
  20. package/dist/form/inputNumber/InputNumber.d.ts +2 -0
  21. package/dist/geocoding/GeocodingService.d.ts +13 -0
  22. package/dist/geocoding/geocoding.interface.d.ts +20 -0
  23. package/dist/grid/Grid.d.ts +5 -2
  24. package/dist/grid/col/Col.d.ts +3 -6
  25. package/dist/grid/col/colStyled/ColStyled.d.ts +8 -5
  26. package/dist/grid/grid.interface.d.ts +20 -5
  27. package/dist/grid/row/Row.d.ts +7 -0
  28. package/dist/header/Header.d.ts +1 -1
  29. package/dist/header/header.interface.d.ts +2 -1
  30. package/dist/icon/Icon.d.ts +1 -1
  31. package/dist/icon/icon.interface.d.ts +0 -1
  32. package/dist/index.d.ts +5 -1
  33. package/dist/index.js +3724 -3628
  34. package/dist/menus/menu/Menu.d.ts +1 -0
  35. package/dist/select/Select.d.ts +2 -1
  36. package/dist/style.css +1 -1
  37. package/dist/tabs/Tabs.d.ts +1 -2
  38. package/dist/toaster/store/useToasterStore.d.ts +1 -0
  39. package/index.html +1 -2
  40. package/package.json +1 -1
  41. package/src/App.menu.tsx +8 -0
  42. package/src/addressSearch/AddressSearch.tsx +246 -0
  43. package/src/addressSearch/AddressSearch.view.tsx +52 -0
  44. package/src/addressSearch/addressSearch.css +133 -0
  45. package/src/badge/Badge.tsx +1 -9
  46. package/src/badge/badge.css +0 -4
  47. package/src/button/Button.tsx +3 -1
  48. package/src/button/button.css +2 -2
  49. package/src/button/button.interface.tsx +2 -1
  50. package/src/card/Card.tsx +37 -11
  51. package/src/card/card.css +7 -28
  52. package/src/card/cardAction/CardAction.tsx +4 -4
  53. package/src/damier/Damier.tsx +16 -7
  54. package/src/damier/damier.css +11 -4
  55. package/src/dataTable/DataTable.tsx +542 -685
  56. package/src/dataTable/DataTable.view.tsx +24 -48
  57. package/src/dataTable/dataTable.css +159 -134
  58. package/src/dataTable/dataTable.interface.ts +19 -25
  59. package/src/dataTable/dataTableActions/DataTableAction.tsx +25 -14
  60. package/src/dataTable/dataTableColumn/DataTableColumn.tsx +14 -64
  61. package/src/dataTable/dataTableLoading/DataTableLoading.tsx +15 -4
  62. package/src/dataTable/dataTableRow/DataTableRow.tsx +180 -64
  63. package/src/expands/Expands.tsx +18 -2
  64. package/src/expands/expand/expand.tsx +128 -5
  65. package/src/expands/expands.css +8 -3
  66. package/src/form/Form.tsx +294 -99
  67. package/src/form/Form.view.tsx +9 -1
  68. package/src/form/form.css +64 -1
  69. package/src/form/form.enum.ts +2 -1
  70. package/src/form/form.interface.tsx +31 -3
  71. package/src/form/input/Input.tsx +7 -61
  72. package/src/form/input/input.css +1 -19
  73. package/src/form/inputNumber/InputNumber.tsx +11 -6
  74. package/src/form/inputPrice/InputPrice.tsx +4 -2
  75. package/src/geocoding/GeocodingService.ts +89 -0
  76. package/src/geocoding/geocoding.interface.ts +25 -0
  77. package/src/grid/Grid.tsx +34 -21
  78. package/src/grid/col/Col.tsx +24 -15
  79. package/src/grid/col/col.css +3 -0
  80. package/src/grid/col/colStyled/ColStyled.tsx +59 -41
  81. package/src/grid/container/container.css +2 -2
  82. package/src/grid/grid.interface.tsx +25 -5
  83. package/src/grid/row/Row.tsx +49 -12
  84. package/src/grid/row/row.css +11 -3
  85. package/src/header/Header.tsx +4 -2
  86. package/src/header/header.css +17 -0
  87. package/src/header/header.interface.tsx +2 -1
  88. package/src/icon/Icon.tsx +2 -6
  89. package/src/icon/icon.interface.tsx +0 -1
  90. package/src/index.css +58 -12
  91. package/src/index.ts +12 -2
  92. package/src/menus/Menus.tsx +1 -0
  93. package/src/menus/menu/Menu.tsx +2 -1
  94. package/src/select/Select.tsx +5 -3
  95. package/src/sidebar/Sidebar.tsx +107 -5
  96. package/src/sidebar/sidebar.css +18 -1
  97. package/src/tabs/Tabs.tsx +1 -20
  98. package/src/tabs/tabs.css +9 -3
  99. package/src/toaster/Toaster.tsx +7 -2
  100. package/src/toaster/Toaster.view.tsx +1 -0
  101. package/src/toaster/store/useToasterStore.tsx +2 -0
  102. package/src/toaster/toaster.css +7 -2
  103. package/dist/_core/type/generiqueType.d.ts +0 -4
  104. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.d.ts +0 -10
  105. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.d.ts +0 -9
  106. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.d.ts +0 -9
  107. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.d.ts +0 -13
  108. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.d.ts +0 -9
  109. package/dist/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.d.ts +0 -11
  110. package/dist/dataTable/dataTableFilters/DataTableFilters.d.ts +0 -7
  111. package/dist/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.d.ts +0 -12
  112. package/dist/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.d.ts +0 -11
  113. package/package/package.json +0 -56
  114. package/package/src/sidebar/Sidebar.tsx +0 -208
  115. package/package/src/sidebar/sidebar.css +0 -104
  116. package/src/_core/type/generiqueType.tsx +0 -4
  117. package/src/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.tsx +0 -36
  118. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilter.css +0 -0
  119. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.tsx +0 -39
  120. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.tsx +0 -80
  121. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.tsx +0 -89
  122. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.tsx +0 -37
  123. package/src/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.tsx +0 -42
  124. package/src/dataTable/dataTableFilters/DataTableFilters.tsx +0 -91
  125. package/src/dataTable/dataTableFilters/dataTableFilters.css +0 -16
  126. package/src/dataTable/dataTableRow/dataTableRowOverlay/DataTableRowOverlay.tsx +0 -80
  127. package/src/dataTable/dataTableRow/dataTableRowValue/DataTableRowValue.tsx +0 -123
@@ -1,62 +1,20 @@
1
- import {
2
- useCallback,
3
- useDeferredValue,
4
- useEffect,
5
- useLayoutEffect,
6
- useMemo,
7
- useRef,
8
- useState,
9
- } from "react";
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";
10
5
  import { TypeInputEnum } from "../form/form.enum";
11
6
  import { Input } from "../form/input/Input";
7
+ import { Icon } from "../icon/Icon";
12
8
  import { Loader } from "../loader/Loader";
13
- import { Select } from "../select/Select";
14
9
  import "./dataTable.css";
15
10
  import {
16
- DataTableActionsInterface,
17
- DataTableColumnsInterface,
11
+ dataTableActionsInterface,
12
+ dataTableColumnsInterface,
18
13
  } from "./dataTable.interface";
19
14
  import { DataTableAction } from "./dataTableActions/DataTableAction";
20
15
  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";
25
16
  import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
26
17
  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
-
60
18
  export type DataTableProps<T> = {
61
19
  id: string;
62
20
  name?: string;
@@ -65,20 +23,28 @@ export type DataTableProps<T> = {
65
23
  element?: JSX.Element;
66
24
  data: T[];
67
25
  emptyMessage?: string;
68
- actions?: DataTableActionsInterface<T>[];
69
- columns: DataTableColumnsInterface<T>[];
70
- onFiltersChange?: (next: DataTableFilterState) => void;
71
- onSortChange?: (next: DataTableSortState | null) => void;
26
+ actionsColumnWidth?: number;
27
+ actions?: dataTableActionsInterface<T>[];
28
+ footerAction?: (props: {
29
+ close: () => void;
30
+ selectedRows: T[];
31
+ }) => JSX.Element;
32
+ columns: dataTableColumnsInterface<T>[];
72
33
  loading?: boolean;
73
34
  onRowClick?: (row: any) => void;
74
35
  onColumnClick?: (column: any) => void;
75
- onEditColumnByOverlay?: (row: T) => void;
76
36
  onLoadMore?: () => void;
77
37
  hasMore?: boolean; // pour savoir s'il reste des données à charger
78
38
  onSelectionChange?: (selectedRows: T[]) => void;
39
+ autoFocusFirstEditable?: boolean;
79
40
  hideToggleAll?: boolean;
80
41
  limitSelect?: number;
81
42
  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;
82
48
  };
83
49
  export function DataTable<T>(props: DataTableProps<T>) {
84
50
  const {
@@ -88,169 +54,82 @@ export function DataTable<T>(props: DataTableProps<T>) {
88
54
  children,
89
55
  element,
90
56
  data = [],
57
+ actionsColumnWidth = 200,
91
58
  actions = [],
59
+ footerAction,
92
60
  columns = [],
93
61
  loading,
94
62
  onRowClick,
95
63
  onColumnClick,
96
64
  onLoadMore,
97
- onEditColumnByOverlay,
98
65
  emptyMessage,
99
66
  hasMore,
100
67
  onSelectionChange,
101
- onFiltersChange,
102
- onSortChange,
68
+ autoFocusFirstEditable = false,
103
69
  hideToggleAll = false,
104
70
  limitSelect = undefined,
105
71
  getRowId,
72
+ onRowStyle,
73
+ onRowClassName,
74
+ rowStyle,
75
+ rowClassName,
76
+ mobileCardView = false,
106
77
  } = props;
107
- const [currentFilters, setCurrentFilters] = useState<DataTableFilterState>(
108
- {},
109
- );
110
- const [visibleFilterColumns, setVisibleFilterColumns] = useState<Set<string>>(
111
- new Set(),
112
- );
113
- const [focusFilterKey, setFocusFilterKey] = useState<string | null>(null);
114
- const [currentSort, setCurrentSort] = useState<DataTableSortState | null>(
115
- null,
116
- );
117
78
 
118
- const getFilterKey = useCallback(
119
- (column: DataTableColumnsInterface<T>) => column.name ?? column.id,
120
- [],
121
- );
122
-
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],
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>>(
87
+ new Set(),
140
88
  );
141
89
 
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
- }
90
+ const focusEditableElement = useCallback((element: HTMLElement | null) => {
91
+ if (!element) {
92
+ return;
93
+ }
155
94
 
156
- return null;
157
- });
158
- },
159
- [getFilterKey],
160
- );
95
+ element.scrollIntoView({ block: "nearest", inline: "nearest" });
161
96
 
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
- }
97
+ requestAnimationFrame(() => {
98
+ element.focus();
171
99
 
172
- if (prev[key] === value) {
173
- return prev;
100
+ if (
101
+ element instanceof HTMLInputElement ||
102
+ element instanceof HTMLTextAreaElement
103
+ ) {
104
+ element.select();
174
105
  }
175
-
176
- return { ...prev, [key]: value };
177
106
  });
178
107
  }, []);
179
108
 
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
- );
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;
211
116
 
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);
117
+ if (!element) {
118
+ return false;
221
119
  }
222
- }, [deferredFilters]);
223
- const onSortChangeRef = useRef(onSortChange);
224
- useEffect(() => {
225
- onSortChangeRef.current = onSortChange;
226
- }, [onSortChange]);
227
120
 
228
- useEffect(() => {
229
- if (onSortChangeRef.current) {
230
- onSortChangeRef.current(currentSort);
231
- }
232
- }, [currentSort]);
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
+ }, []);
233
127
 
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);
250
128
  const toggleSelectAll = useCallback(() => {
251
129
  if (selectedRowIds.size === data.length) {
252
130
  setSelectedRowIds(new Set());
253
131
  onSelectionChange?.([]);
132
+ setIsFooterActionVisible(false);
254
133
  } else {
255
134
  let allIds = data.map((d) =>
256
135
  getRowId ? getRowId(d) : ((d as any).id ?? d),
@@ -262,6 +141,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
262
141
 
263
142
  const newSelection = new Set(allIds);
264
143
  setSelectedRowIds(newSelection);
144
+ setIsFooterActionVisible(true);
265
145
  onSelectionChange?.(
266
146
  data.filter((d) =>
267
147
  newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
@@ -270,14 +150,175 @@ export function DataTable<T>(props: DataTableProps<T>) {
270
150
  }
271
151
  }, [data, onSelectionChange, limitSelect, getRowId, selectedRowIds.size]);
272
152
 
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
+
273
301
  const handleClickRow = useCallback(
274
302
  (e: React.MouseEvent, row: T) => {
275
303
  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
+
276
317
  if (onRowClick) {
277
318
  onRowClick?.(row);
278
319
  }
279
320
  },
280
- [onRowClick],
321
+ [isInteractiveClick, onRowClick],
281
322
  );
282
323
  const toggleRow = useCallback(
283
324
  (e: React.ChangeEvent<HTMLInputElement>, row: T) => {
@@ -296,6 +337,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
296
337
  }
297
338
 
298
339
  setSelectedRowIds(newSelection);
340
+ setIsFooterActionVisible(newSelection.size > 0);
299
341
  const selectedItems = data.filter((d) =>
300
342
  newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
301
343
  );
@@ -303,388 +345,238 @@ export function DataTable<T>(props: DataTableProps<T>) {
303
345
  },
304
346
  [data, getRowId, limitSelect, onSelectionChange, selectedRowIds],
305
347
  );
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
+ );
306
355
  useEffect(() => {
307
- if (!onLoadMore || !hasMore) return;
308
-
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
- },
320
- );
321
-
322
- if (loaderRef.current) {
323
- observer.observe(loaderRef.current);
356
+ if (loading || !autoFocusFirstEditable || data.length === 0) {
357
+ hasAutoFocusedRef.current = false;
358
+ return;
324
359
  }
325
360
 
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);
333
-
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;
361
+ if (hasAutoFocusedRef.current) {
362
+ return;
365
363
  }
366
364
 
367
- const sortedColumn = flatColumns.find(
368
- (column) => getFilterKey(column) === currentSort.key,
365
+ const firstEditableColumnIndex = columns.findIndex(
366
+ (column) =>
367
+ column.editable &&
368
+ column.enterToNextColumn &&
369
+ column.type !== "select" &&
370
+ column.type !== "image",
369
371
  );
370
372
 
371
- if (!sortedColumn) {
372
- return data;
373
+ if (firstEditableColumnIndex === -1) {
374
+ return;
373
375
  }
374
376
 
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
- }
382
-
383
- if (typeof value === "string" || typeof value === "number") {
384
- const timestamp = new Date(value).getTime();
385
- return Number.isNaN(timestamp) ? null : timestamp;
386
- }
387
-
388
- return null;
389
- };
390
-
391
- const compareValues = (left: unknown, right: unknown): number => {
392
- if (left == null && right == null) {
393
- return 0;
394
- }
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
+ );
395
382
 
396
- if (left == null) {
397
- return 1;
398
- }
383
+ if (!(firstCell instanceof HTMLElement)) {
384
+ return;
385
+ }
399
386
 
400
- if (right == null) {
401
- return -1;
402
- }
387
+ const firstInput = firstCell.querySelector<HTMLElement>(
388
+ 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]',
389
+ );
403
390
 
404
- if (typeof left === "number" && typeof right === "number") {
405
- return left - right;
406
- }
391
+ if (!firstInput) {
392
+ return;
393
+ }
407
394
 
408
- if (typeof left === "boolean" && typeof right === "boolean") {
409
- return Number(left) - Number(right);
410
- }
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
+ }
411
410
 
412
- if (isDateColumn) {
413
- const leftDate = normalizeDate(left);
414
- const rightDate = normalizeDate(right);
411
+ const footerActionElement = footerActionRef.current;
415
412
 
416
- if (leftDate !== null && rightDate !== null) {
417
- return leftDate - rightDate;
418
- }
419
- }
413
+ if (!footerActionElement) {
414
+ return;
415
+ }
420
416
 
421
- return String(left).localeCompare(String(right), undefined, {
422
- numeric: true,
423
- sensitivity: "base",
424
- });
417
+ const updateFooterActionOffset = () => {
418
+ const { height } = footerActionElement.getBoundingClientRect();
419
+ setFooterActionOffset(Math.ceil(height) + 16);
425
420
  };
426
421
 
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;
422
+ updateFooterActionOffset();
438
423
 
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);
424
+ if (typeof ResizeObserver === "undefined") {
446
425
  return;
447
426
  }
448
427
 
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
- };
457
-
458
- updateFilterRowHeight();
428
+ const resizeObserver = new ResizeObserver(() => {
429
+ updateFooterActionOffset();
430
+ });
459
431
 
460
- if (typeof ResizeObserver !== "undefined" && filterRowRef.current) {
461
- const resizeObserver = new ResizeObserver(updateFilterRowHeight);
462
- resizeObserver.observe(filterRowRef.current);
463
- return () => resizeObserver.disconnect();
464
- }
432
+ resizeObserver.observe(footerActionElement);
465
433
 
466
- window.addEventListener("resize", updateFilterRowHeight);
467
434
  return () => {
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
- );
435
+ resizeObserver.disconnect();
479
436
  };
437
+ }, [showFooterAction]);
438
+ useEffect(() => {
439
+ if (!onLoadMore || !hasMore) return;
480
440
 
481
- updateHeaderRowHeight();
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
+ );
482
453
 
483
- if (typeof ResizeObserver !== "undefined" && headerRowRef.current) {
484
- const resizeObserver = new ResizeObserver(updateHeaderRowHeight);
485
- resizeObserver.observe(headerRowRef.current);
486
- return () => resizeObserver.disconnect();
454
+ if (loaderRef.current) {
455
+ observer.observe(loaderRef.current);
487
456
  }
488
457
 
489
- window.addEventListener("resize", updateHeaderRowHeight);
490
458
  return () => {
491
- window.removeEventListener("resize", updateHeaderRowHeight);
459
+ if (loaderRef.current) {
460
+ observer.unobserve(loaderRef.current);
461
+ }
492
462
  };
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
- );
463
+ }, [onLoadMore, hasMore]);
610
464
  return (
611
- <div className="data-table-container">
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
+ >
612
473
  {name && <h2 className="mb-2">{name}</h2>}
613
474
  {element}
614
- {currentFilters && (
615
- <DataTableFilters
616
- currentFilters={currentFilters}
617
- setCurrentFilters={setCurrentFilters}
618
- />
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>
619
568
  )}
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>
569
+ <table
570
+ className={`data-table ${className ? className : ""}`}
571
+ id={id}
572
+ style={showMobileCardView ? { display: "none" } : undefined}
573
+ >
635
574
  <thead>
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}>
575
+ <tr>
670
576
  {onSelectionChange && (
671
577
  <th
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
- }}
578
+ className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
579
+ style={{ width: "20px", textAlign: "center" }}
688
580
  >
689
581
  <Input
690
582
  id={`select-th-${id}`}
@@ -697,186 +589,151 @@ export function DataTable<T>(props: DataTableProps<T>) {
697
589
  />
698
590
  </th>
699
591
  )}
700
-
701
592
  {columns.map((column, idx) => (
702
- <DataTableColumn<T>
593
+ <DataTableColumn
703
594
  key={idx}
704
- hasNestedColumns={hasNestedColumns}
705
595
  column={column}
706
596
  idx={idx}
707
597
  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}
718
598
  />
719
599
  ))}
720
600
  {actions.length > 0 && (
721
601
  <th
722
602
  style={{
723
- width: `${actionsWidth}px`,
724
- top: headerStickyTop,
603
+ width: `${actionsColumnWidth}px`,
604
+ minWidth: `${actionsColumnWidth}px`,
725
605
  }}
726
606
  align="right"
727
607
  key={"actions"}
728
- rowSpan={hasNestedColumns ? 2 : 1}
729
608
  ></th>
730
609
  )}
731
610
  </tr>
732
- {hasNestedColumns && (
611
+ </thead>
612
+ <tbody>
613
+ {loading ? (
733
614
  <tr>
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
- )}
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>
795
623
  </tr>
624
+ ) : (
625
+ <></>
796
626
  )}
797
- </thead>
798
- <tbody>
799
627
  {!loading &&
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(
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(
830
661
  getRowId ? getRowId(row) : ((row as any).id ?? row),
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
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}
837
675
  />
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
- ))}
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
+ )}
868
710
  </tbody>
869
711
  </table>
870
712
 
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
- )}
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
+ )}
880
737
 
881
738
  {children}
882
739
  {hasMore && (