kelt-ui-kit-react 2.0.8 → 2.1.0

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 (319) hide show
  1. package/.github/workflows/publish.yml +40 -31
  2. package/README.md +124 -72
  3. package/dist/App.d.ts +2 -3
  4. package/dist/App.menu.d.ts +10 -10
  5. package/dist/App.routes.d.ts +1 -1
  6. package/dist/_core/hooks/HistoryProvider.d.ts +5 -5
  7. package/dist/_core/hooks/ImageChecker.d.ts +5 -6
  8. package/dist/_core/hooks/imageExist.d.ts +5 -6
  9. package/dist/_core/hooks/isEAN13.d.ts +1 -1
  10. package/dist/_core/hooks/objectToQueryParams.d.ts +1 -1
  11. package/dist/_core/hooks/useCurrency.d.ts +3 -3
  12. package/dist/_core/hooks/useDateutils.d.ts +4 -4
  13. package/dist/_core/hooks/useIsMobile.d.ts +1 -1
  14. package/dist/_core/hooks/useToken.d.ts +3 -3
  15. package/dist/_core/hooks/useValidate.d.ts +5 -5
  16. package/dist/_core/hooks/widgetSupport.d.ts +1 -1
  17. package/dist/_core/type/generiqueType.d.ts +4 -0
  18. package/dist/action/Action.view.d.ts +1 -1
  19. package/dist/action/ButtonAction.d.ts +8 -9
  20. package/dist/addressSearch/AddressSearch.d.ts +18 -19
  21. package/dist/addressSearch/AddressSearch.view.d.ts +1 -1
  22. package/dist/badge/Badge.d.ts +10 -9
  23. package/dist/badge/Badge.view.d.ts +1 -1
  24. package/dist/button/Button.d.ts +1 -2
  25. package/dist/button/Button.view.d.ts +1 -1
  26. package/dist/button/button.interface.d.ts +19 -20
  27. package/dist/button/buttonActions/ButtonActions.d.ts +11 -12
  28. package/dist/button/buttonFile/ButtonFile.d.ts +9 -9
  29. package/dist/button/buttonFile/ButtonFile.view.d.ts +1 -1
  30. package/dist/button/buttonFilters/ButtonFilters.d.ts +16 -17
  31. package/dist/card/Card.d.ts +14 -15
  32. package/dist/card/Card.view.d.ts +1 -1
  33. package/dist/card/card.interface.d.ts +10 -10
  34. package/dist/card/cardAction/CardAction.d.ts +14 -14
  35. package/dist/card/cardAction.interface.d.ts +8 -9
  36. package/dist/card/hook/useCardInteractions.d.ts +8 -8
  37. package/dist/carousel/Carousel.d.ts +14 -15
  38. package/dist/carousel/Carousel.view.d.ts +1 -1
  39. package/dist/damier/Damier.d.ts +9 -10
  40. package/dist/damier/Damier.view.d.ts +1 -1
  41. package/dist/damier/damierCell/DamierCell.d.ts +6 -7
  42. package/dist/damier/damierCell/damierCell.interface.d.ts +6 -6
  43. package/dist/dataTable/DataTable.d.ts +37 -33
  44. package/dist/dataTable/DataTable.view.d.ts +1 -1
  45. package/dist/dataTable/dataTable.interface.d.ts +70 -47
  46. package/dist/dataTable/dataTable.utils.d.ts +17 -0
  47. package/dist/dataTable/dataTableActions/DataTableAction.d.ts +9 -10
  48. package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +14 -8
  49. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.d.ts +9 -0
  50. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.d.ts +9 -0
  51. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.d.ts +9 -0
  52. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.d.ts +12 -0
  53. package/dist/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.d.ts +9 -0
  54. package/dist/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.d.ts +10 -0
  55. package/dist/dataTable/dataTableFilters/DataTableFilters.d.ts +6 -0
  56. package/dist/dataTable/dataTableLoading/DataTableLoading.d.ts +8 -7
  57. package/dist/dataTable/dataTableRow/DataTableRow.d.ts +12 -11
  58. package/dist/dataTable/useDataTableState.d.ts +13 -0
  59. package/dist/datePicker/DatePicker.d.ts +7 -8
  60. package/dist/datePicker/DatePicker.view.d.ts +1 -1
  61. package/dist/docs/ComponentDocs.d.ts +6 -0
  62. package/dist/docs/catalog.d.ts +8 -0
  63. package/dist/docs/documentation.types.d.ts +21 -0
  64. package/dist/expands/Expands.d.ts +11 -12
  65. package/dist/expands/Expands.view.d.ts +1 -1
  66. package/dist/expands/expand/expand.d.ts +10 -11
  67. package/dist/expands/expand/expand.interface.d.ts +9 -9
  68. package/dist/filAriane/FilAriane.d.ts +8 -9
  69. package/dist/filAriane/FilAriane.view.d.ts +1 -1
  70. package/dist/filAriane/filAriane.interface.d.ts +6 -6
  71. package/dist/form/Form.d.ts +35 -36
  72. package/dist/form/Form.view.d.ts +1 -1
  73. package/dist/form/form.enum.d.ts +16 -15
  74. package/dist/form/form.interface.d.ts +53 -54
  75. package/dist/form/input/Input.d.ts +27 -25
  76. package/dist/form/inputNumber/InputNumber.d.ts +21 -22
  77. package/dist/form/inputNumber/InputNumber.view.d.ts +1 -1
  78. package/dist/form/inputPrice/InputPrice.d.ts +14 -15
  79. package/dist/form/inputPrice/InputPrice.view.d.ts +1 -1
  80. package/dist/form/textArea/TextArea.d.ts +14 -15
  81. package/dist/form/textArea/TextArea.view.d.ts +1 -1
  82. package/dist/geocoding/GeocodingService.d.ts +11 -12
  83. package/dist/geocoding/geocoding.interface.d.ts +20 -20
  84. package/dist/grid/Grid.d.ts +9 -10
  85. package/dist/grid/Grid.view.d.ts +1 -1
  86. package/dist/grid/col/Col.d.ts +5 -6
  87. package/dist/grid/col/colStyled/ColStyled.d.ts +10 -11
  88. package/dist/grid/container/Container.d.ts +5 -6
  89. package/dist/grid/grid.interface.d.ts +23 -23
  90. package/dist/grid/row/Row.d.ts +13 -14
  91. package/dist/header/Header.d.ts +1 -2
  92. package/dist/header/Header.view.d.ts +1 -1
  93. package/dist/header/header.interface.d.ts +12 -12
  94. package/dist/home/Home.d.ts +1 -1
  95. package/dist/icon/Icon.d.ts +1 -2
  96. package/dist/icon/Icons.view.d.ts +1 -1
  97. package/dist/icon/icon.interface.d.ts +5 -5
  98. package/dist/icon/iconSize.enum.d.ts +7 -7
  99. package/dist/index.d.ts +56 -54
  100. package/dist/index.html +18 -18
  101. package/dist/index.js +3966 -3460
  102. package/dist/loader/Loader.d.ts +7 -8
  103. package/dist/loader/Loader.view.d.ts +1 -1
  104. package/dist/main.d.ts +0 -1
  105. package/dist/manifest.json +25 -25
  106. package/dist/menus/Menus.d.ts +5 -6
  107. package/dist/menus/Menus.view.d.ts +1 -1
  108. package/dist/menus/menu/Menu.d.ts +7 -8
  109. package/dist/menus/menu/menu.interface.d.ts +10 -10
  110. package/dist/message/Message.d.ts +6 -7
  111. package/dist/message/Message.view.d.ts +1 -1
  112. package/dist/modal/Modal.d.ts +13 -14
  113. package/dist/modal/Modal.view.d.ts +1 -1
  114. package/dist/notFound/NotFound.d.ts +1 -1
  115. package/dist/overlayPanel/OverlayPanel.d.ts +22 -23
  116. package/dist/overlayPanel/OverlayPanel.view.d.ts +1 -1
  117. package/dist/overlayPanel/overlay.context.d.ts +9 -10
  118. package/dist/overlayPanel/overlayPanelStyled/OverlayPanelStyled.d.ts +9 -9
  119. package/dist/pdfViewer/PdfViewer.d.ts +12 -12
  120. package/dist/pdfViewer/PdfViewer.view.d.ts +1 -1
  121. package/dist/quantity/Quantity.d.ts +10 -11
  122. package/dist/robots.txt +3 -3
  123. package/dist/search/Search.d.ts +21 -22
  124. package/dist/search/Search.view.d.ts +1 -1
  125. package/dist/select/Select.d.ts +25 -24
  126. package/dist/select/Select.view.d.ts +1 -1
  127. package/dist/select/selectOption.interface.d.ts +5 -5
  128. package/dist/sidebar/Sidebar.d.ts +12 -13
  129. package/dist/sidebar/Sidebar.view.d.ts +1 -1
  130. package/dist/sidebarData/SidebarData.d.ts +6 -7
  131. package/dist/sidebarData/SidebarData.view.d.ts +1 -1
  132. package/dist/style.css +1 -1
  133. package/dist/tabs/Tabs.d.ts +17 -17
  134. package/dist/tabs/Tabs.view.d.ts +1 -1
  135. package/dist/toaster/Toaster.d.ts +1 -2
  136. package/dist/toaster/Toaster.view.d.ts +1 -1
  137. package/dist/toaster/store/useToasterStore.d.ts +24 -24
  138. package/docs.plugin.ts +223 -0
  139. package/index.html +20 -18
  140. package/package.json +63 -55
  141. package/public/index.html +18 -18
  142. package/public/manifest.json +25 -25
  143. package/public/robots.txt +3 -3
  144. package/src/App.css +178 -11
  145. package/src/App.menu.tsx +277 -257
  146. package/src/App.routes.tsx +21 -16
  147. package/src/App.tsx +31 -30
  148. package/src/_core/hooks/HistoryProvider.tsx +25 -25
  149. package/src/_core/hooks/ImageChecker.tsx +26 -26
  150. package/src/_core/hooks/imageExist.tsx +9 -9
  151. package/src/_core/hooks/isEAN13.tsx +16 -16
  152. package/src/_core/hooks/objectToQueryParams.tsx +32 -32
  153. package/src/_core/hooks/useCurrency.tsx +18 -18
  154. package/src/_core/hooks/useDateutils.ts +64 -64
  155. package/src/_core/hooks/useIsMobile.ts +18 -18
  156. package/src/_core/hooks/useToken.tsx +19 -19
  157. package/src/_core/hooks/useValidate.tsx +18 -18
  158. package/src/_core/hooks/widgetSupport.tsx +39 -39
  159. package/src/_core/type/generiqueType.tsx +4 -0
  160. package/src/action/Action.view.tsx +21 -21
  161. package/src/action/ButtonAction.tsx +32 -32
  162. package/src/action/action.css +20 -20
  163. package/src/addressSearch/AddressSearch.tsx +246 -246
  164. package/src/addressSearch/AddressSearch.view.tsx +52 -52
  165. package/src/addressSearch/addressSearch.css +132 -132
  166. package/src/badge/Badge.tsx +41 -34
  167. package/src/badge/Badge.view.tsx +15 -15
  168. package/src/badge/badge.css +63 -55
  169. package/src/button/Button.tsx +48 -48
  170. package/src/button/Button.view.tsx +85 -85
  171. package/src/button/button.css +53 -53
  172. package/src/button/button.interface.tsx +22 -22
  173. package/src/button/buttonActions/ButtonActions.tsx +104 -104
  174. package/src/button/buttonFile/ButtonFile.tsx +59 -59
  175. package/src/button/buttonFile/ButtonFile.view.tsx +70 -70
  176. package/src/button/buttonFilters/ButtonFilters.tsx +102 -102
  177. package/src/button/buttonFilters/buttonFilters.css +22 -22
  178. package/src/card/Card.tsx +151 -151
  179. package/src/card/Card.view.tsx +73 -73
  180. package/src/card/card.css +144 -144
  181. package/src/card/card.interface.tsx +9 -9
  182. package/src/card/cardAction/CardAction.tsx +129 -135
  183. package/src/card/cardAction/cardAction.css +10 -10
  184. package/src/card/cardAction.interface.tsx +10 -10
  185. package/src/card/hook/useCardInteractions.tsx +30 -30
  186. package/src/carousel/Carousel.css +44 -44
  187. package/src/carousel/Carousel.tsx +115 -115
  188. package/src/carousel/Carousel.view.tsx +13 -13
  189. package/src/damier/Damier.tsx +64 -64
  190. package/src/damier/Damier.view.tsx +31 -31
  191. package/src/damier/damier.css +50 -50
  192. package/src/damier/damierCell/DamierCell.tsx +18 -18
  193. package/src/damier/damierCell/damierCell.interface.tsx +5 -5
  194. package/src/dataTable/DataTable.tsx +845 -746
  195. package/src/dataTable/DataTable.view.tsx +105 -71
  196. package/src/dataTable/dataTable.css +333 -208
  197. package/src/dataTable/dataTable.interface.ts +66 -45
  198. package/src/dataTable/dataTable.utils.ts +45 -0
  199. package/src/dataTable/dataTableActions/DataTableAction.tsx +82 -65
  200. package/src/dataTable/dataTableColumn/DataTableColumn.tsx +48 -27
  201. package/src/dataTable/dataTableColumn/dataTableColumnFilter/DataTableColumnFilter.tsx +36 -0
  202. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilter.css +0 -0
  203. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDate/DataTableColumnFilterDate.tsx +39 -0
  204. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterDateRange/dataTableColumnFilterDateRange.tsx +80 -0
  205. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterEnum/DataTableColumnFilterEnum.tsx +89 -0
  206. package/src/dataTable/dataTableColumn/dataTableColumnFilter/dataTableColumnFilterString/DataTableColumnFilterString.tsx +37 -0
  207. package/src/dataTable/dataTableColumn/dataTableColumnSort/DataTableColumnSort.tsx +42 -0
  208. package/src/dataTable/dataTableFilters/DataTableFilters.tsx +91 -0
  209. package/src/dataTable/dataTableFilters/dataTableFilters.css +16 -0
  210. package/src/dataTable/dataTableLoading/DataTableLoading.tsx +23 -21
  211. package/src/dataTable/dataTableRow/DataTableRow.tsx +155 -208
  212. package/src/dataTable/useDataTableState.tsx +95 -0
  213. package/src/datePicker/DatePicker.tsx +110 -110
  214. package/src/datePicker/DatePicker.view.tsx +9 -9
  215. package/src/datePicker/datePicker.css +77 -77
  216. package/src/docs/ComponentDocs.tsx +156 -0
  217. package/src/docs/catalog.ts +40 -0
  218. package/src/docs/documentation.types.ts +20 -0
  219. package/src/docs/virtual.d.ts +7 -0
  220. package/src/expands/Expands.tsx +46 -46
  221. package/src/expands/Expands.view.tsx +90 -90
  222. package/src/expands/expand/expand.interface.tsx +8 -8
  223. package/src/expands/expand/expand.tsx +191 -191
  224. package/src/expands/expands.css +101 -101
  225. package/src/filAriane/FilAriane.tsx +57 -57
  226. package/src/filAriane/FilAriane.view.tsx +28 -28
  227. package/src/filAriane/filAriane.css +23 -23
  228. package/src/filAriane/filAriane.interface.tsx +6 -6
  229. package/src/form/Form.tsx +477 -477
  230. package/src/form/Form.view.tsx +67 -67
  231. package/src/form/form.css +135 -135
  232. package/src/form/form.enum.ts +16 -15
  233. package/src/form/form.interface.tsx +67 -67
  234. package/src/form/input/Input.tsx +146 -106
  235. package/src/form/input/input.css +39 -29
  236. package/src/form/inputNumber/InputNumber.tsx +132 -132
  237. package/src/form/inputNumber/InputNumber.view.tsx +15 -15
  238. package/src/form/inputPrice/InputPrice.tsx +42 -42
  239. package/src/form/inputPrice/InputPrice.view.tsx +15 -15
  240. package/src/form/textArea/TextArea.tsx +59 -59
  241. package/src/form/textArea/TextArea.view.tsx +34 -34
  242. package/src/form/textArea/textArea.css +9 -9
  243. package/src/geocoding/GeocodingService.ts +89 -89
  244. package/src/geocoding/geocoding.interface.ts +25 -25
  245. package/src/grid/Grid.tsx +34 -34
  246. package/src/grid/Grid.view.tsx +24 -24
  247. package/src/grid/col/Col.tsx +24 -24
  248. package/src/grid/col/col.css +3 -3
  249. package/src/grid/col/colStyled/ColStyled.tsx +59 -59
  250. package/src/grid/container/Container.tsx +8 -8
  251. package/src/grid/container/container.css +4 -4
  252. package/src/grid/grid.interface.tsx +27 -27
  253. package/src/grid/row/Row.tsx +49 -49
  254. package/src/grid/row/row.css +25 -25
  255. package/src/header/Header.tsx +64 -64
  256. package/src/header/Header.view.tsx +5 -5
  257. package/src/header/header.css +43 -43
  258. package/src/header/header.interface.tsx +11 -11
  259. package/src/home/Home.tsx +40 -3
  260. package/src/icon/Icon.tsx +6 -6
  261. package/src/icon/Icons.view.tsx +29 -29
  262. package/src/icon/icon.css +20 -20
  263. package/src/icon/icon.interface.tsx +7 -6
  264. package/src/icon/iconSize.enum.ts +7 -7
  265. package/src/index.css +547 -547
  266. package/src/index.ts +76 -70
  267. package/src/loader/Loader.tsx +37 -37
  268. package/src/loader/Loader.view.tsx +20 -20
  269. package/src/loader/loader.css +30 -30
  270. package/src/main.tsx +10 -10
  271. package/src/menus/Menus.tsx +43 -43
  272. package/src/menus/Menus.view.tsx +39 -39
  273. package/src/menus/menu/Menu.tsx +18 -18
  274. package/src/menus/menu/menu.interface.tsx +9 -9
  275. package/src/menus/menus.css +47 -47
  276. package/src/message/Message.tsx +30 -30
  277. package/src/message/Message.view.tsx +22 -22
  278. package/src/message/message.css +29 -29
  279. package/src/modal/Modal.tsx +59 -59
  280. package/src/modal/Modal.view.tsx +26 -26
  281. package/src/modal/modal.css +71 -71
  282. package/src/notFound/NotFound.tsx +3 -3
  283. package/src/overlayPanel/OverlayPanel.tsx +265 -255
  284. package/src/overlayPanel/OverlayPanel.view.tsx +25 -25
  285. package/src/overlayPanel/overlay.context.tsx +30 -30
  286. package/src/overlayPanel/overlayPanel.css +43 -43
  287. package/src/overlayPanel/overlayPanelStyled/OverlayPanelStyled.tsx +23 -23
  288. package/src/pdfViewer/PdfViewer.tsx +73 -73
  289. package/src/pdfViewer/PdfViewer.view.tsx +5 -5
  290. package/src/quantity/Quantity.tsx +103 -103
  291. package/src/quantity/quantity.css +26 -26
  292. package/src/search/Search.tsx +161 -161
  293. package/src/search/Search.view.tsx +14 -14
  294. package/src/search/search.css +59 -59
  295. package/src/select/Select.tsx +247 -245
  296. package/src/select/Select.view.tsx +71 -71
  297. package/src/select/select.css +150 -150
  298. package/src/select/selectOption.interface.ts +5 -5
  299. package/src/sidebar/Sidebar.tsx +213 -213
  300. package/src/sidebar/Sidebar.view.tsx +17 -17
  301. package/src/sidebar/sidebar.css +103 -103
  302. package/src/sidebarData/SidebarData.tsx +19 -19
  303. package/src/sidebarData/SidebarData.view.tsx +19 -19
  304. package/src/sidebarData/sidebarData.css +27 -27
  305. package/src/tabs/Tabs.tsx +101 -90
  306. package/src/tabs/Tabs.view.tsx +20 -20
  307. package/src/tabs/tabs.css +29 -29
  308. package/src/toaster/Toaster.tsx +61 -61
  309. package/src/toaster/Toaster.view.tsx +25 -25
  310. package/src/toaster/store/useToasterStore.tsx +54 -54
  311. package/src/toaster/toaster.css +105 -105
  312. package/tests/dataTable.test.mjs +93 -0
  313. package/tests/documentation.test.mjs +75 -0
  314. package/tsconfig.docs.json +10 -0
  315. package/tsconfig.json +28 -28
  316. package/vite.config.ts +26 -20
  317. package/vite.config.ts.timestamp-1733262892554-a13dfef6e8a29.mjs +24 -24
  318. package/vite.config.ts.timestamp-1757847121745-e5a5bf54b81b.mjs +28 -28
  319. package/vite.config.ts.timestamp-1759742478362-4ac5c5a7db34b.mjs +28 -28
@@ -1,746 +1,845 @@
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";
5
- import { TypeInputEnum } from "../form/form.enum";
6
- import { Input } from "../form/input/Input";
7
- import { Icon } from "../icon/Icon";
8
- import { Loader } from "../loader/Loader";
9
- import "./dataTable.css";
10
- import {
11
- dataTableActionsInterface,
12
- dataTableColumnsInterface,
13
- } from "./dataTable.interface";
14
- import { DataTableAction } from "./dataTableActions/DataTableAction";
15
- import { DataTableColumn } from "./dataTableColumn/DataTableColumn";
16
- import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
17
- import { DataTableRow } from "./dataTableRow/DataTableRow";
18
- export type DataTableProps<T> = {
19
- id: string;
20
- name?: string;
21
- className?: string;
22
- children?: JSX.Element;
23
- element?: JSX.Element;
24
- data: T[];
25
- 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>[];
33
- loading?: boolean;
34
- onRowClick?: (row: any) => void;
35
- onColumnClick?: (column: any) => void;
36
- onLoadMore?: () => void;
37
- hasMore?: boolean; // pour savoir s'il reste des données à charger
38
- onSelectionChange?: (selectedRows: T[]) => void;
39
- autoFocusFirstEditable?: boolean;
40
- hideToggleAll?: boolean;
41
- limitSelect?: number;
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;
48
- };
49
- export function DataTable<T>(props: DataTableProps<T>) {
50
- const {
51
- id,
52
- name,
53
- className,
54
- children,
55
- element,
56
- data = [],
57
- actionsColumnWidth = 200,
58
- actions = [],
59
- footerAction,
60
- columns = [],
61
- loading,
62
- onRowClick,
63
- onColumnClick,
64
- onLoadMore,
65
- emptyMessage,
66
- hasMore,
67
- onSelectionChange,
68
- autoFocusFirstEditable = false,
69
- hideToggleAll = false,
70
- limitSelect = undefined,
71
- getRowId,
72
- onRowStyle,
73
- onRowClassName,
74
- rowStyle,
75
- rowClassName,
76
- mobileCardView = false,
77
- } = 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>>(
87
- new Set(),
88
- );
89
-
90
- const focusEditableElement = useCallback((element: HTMLElement | null) => {
91
- if (!element) {
92
- return;
93
- }
94
-
95
- element.scrollIntoView({ block: "nearest", inline: "nearest" });
96
-
97
- requestAnimationFrame(() => {
98
- element.focus();
99
-
100
- if (
101
- element instanceof HTMLInputElement ||
102
- element instanceof HTMLTextAreaElement
103
- ) {
104
- element.select();
105
- }
106
- });
107
- }, []);
108
-
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;
116
-
117
- if (!element) {
118
- return false;
119
- }
120
-
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
- }, []);
127
-
128
- const toggleSelectAll = useCallback(() => {
129
- if (selectedRowIds.size === data.length) {
130
- setSelectedRowIds(new Set());
131
- onSelectionChange?.([]);
132
- setIsFooterActionVisible(false);
133
- } else {
134
- let allIds = data.map((d) =>
135
- getRowId ? getRowId(d) : ((d as any).id ?? d),
136
- );
137
-
138
- if (limitSelect && allIds.length > limitSelect) {
139
- allIds = allIds.slice(0, limitSelect);
140
- }
141
-
142
- const newSelection = new Set(allIds);
143
- setSelectedRowIds(newSelection);
144
- setIsFooterActionVisible(true);
145
- onSelectionChange?.(
146
- data.filter((d) =>
147
- newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
148
- ),
149
- );
150
- }
151
- }, [data, onSelectionChange, limitSelect, getRowId, selectedRowIds.size]);
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
-
301
- const handleClickRow = useCallback(
302
- (e: React.MouseEvent, row: T) => {
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
-
317
- if (onRowClick) {
318
- onRowClick?.(row);
319
- }
320
- },
321
- [isInteractiveClick, onRowClick],
322
- );
323
- const toggleRow = useCallback(
324
- (e: React.ChangeEvent<HTMLInputElement>, row: T) => {
325
- e.stopPropagation();
326
- const rowId = getRowId ? getRowId(row) : ((row as any).id ?? row);
327
- const newSelection = new Set(selectedRowIds);
328
-
329
- if (newSelection.has(rowId)) {
330
- newSelection.delete(rowId);
331
- } else {
332
- if (limitSelect && newSelection.size >= limitSelect) {
333
- // 🚫 limite atteinte
334
- return;
335
- }
336
- newSelection.add(rowId);
337
- }
338
-
339
- setSelectedRowIds(newSelection);
340
- setIsFooterActionVisible(newSelection.size > 0);
341
- const selectedItems = data.filter((d) =>
342
- newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
343
- );
344
- onSelectionChange?.(selectedItems);
345
- },
346
- [data, getRowId, limitSelect, onSelectionChange, selectedRowIds],
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
- );
355
- useEffect(() => {
356
- if (loading || !autoFocusFirstEditable || data.length === 0) {
357
- hasAutoFocusedRef.current = false;
358
- return;
359
- }
360
-
361
- if (hasAutoFocusedRef.current) {
362
- return;
363
- }
364
-
365
- const firstEditableColumnIndex = columns.findIndex(
366
- (column) =>
367
- column.editable &&
368
- column.enterToNextColumn &&
369
- column.type !== "select" &&
370
- column.type !== "image",
371
- );
372
-
373
- if (firstEditableColumnIndex === -1) {
374
- return;
375
- }
376
-
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
- );
382
-
383
- if (!(firstCell instanceof HTMLElement)) {
384
- return;
385
- }
386
-
387
- const firstInput = firstCell.querySelector<HTMLElement>(
388
- 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]',
389
- );
390
-
391
- if (!firstInput) {
392
- return;
393
- }
394
-
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
- }
410
-
411
- const footerActionElement = footerActionRef.current;
412
-
413
- if (!footerActionElement) {
414
- return;
415
- }
416
-
417
- const updateFooterActionOffset = () => {
418
- const { height } = footerActionElement.getBoundingClientRect();
419
- setFooterActionOffset(Math.ceil(height) + 16);
420
- };
421
-
422
- updateFooterActionOffset();
423
-
424
- if (typeof ResizeObserver === "undefined") {
425
- return;
426
- }
427
-
428
- const resizeObserver = new ResizeObserver(() => {
429
- updateFooterActionOffset();
430
- });
431
-
432
- resizeObserver.observe(footerActionElement);
433
-
434
- return () => {
435
- resizeObserver.disconnect();
436
- };
437
- }, [showFooterAction]);
438
- useEffect(() => {
439
- if (!onLoadMore || !hasMore) return;
440
-
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
- );
453
-
454
- if (loaderRef.current) {
455
- observer.observe(loaderRef.current);
456
- }
457
-
458
- return () => {
459
- if (loaderRef.current) {
460
- observer.unobserve(loaderRef.current);
461
- }
462
- };
463
- }, [onLoadMore, hasMore]);
464
- 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
- >
473
- {name && <h2 className="mb-2">{name}</h2>}
474
- {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>
568
- )}
569
- <table
570
- className={`data-table ${className ? className : ""}`}
571
- id={id}
572
- style={showMobileCardView ? { display: "none" } : undefined}
573
- >
574
- <thead>
575
- <tr>
576
- {onSelectionChange && (
577
- <th
578
- className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
579
- style={{ width: "20px", textAlign: "center" }}
580
- >
581
- <Input
582
- id={`select-th-${id}`}
583
- name={`select-th-${id}`}
584
- type={TypeInputEnum.CHECKBOX}
585
- checked={
586
- data.length > 0 && selectedRowIds.size === data.length
587
- }
588
- onChange={toggleSelectAll}
589
- />
590
- </th>
591
- )}
592
- {columns.map((column, idx) => (
593
- <DataTableColumn
594
- key={idx}
595
- column={column}
596
- idx={idx}
597
- onColumnClick={onColumnClick}
598
- />
599
- ))}
600
- {actions.length > 0 && (
601
- <th
602
- style={{
603
- width: `${actionsColumnWidth}px`,
604
- minWidth: `${actionsColumnWidth}px`,
605
- }}
606
- align="right"
607
- key={"actions"}
608
- ></th>
609
- )}
610
- </tr>
611
- </thead>
612
- <tbody>
613
- {loading ? (
614
- <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>
623
- </tr>
624
- ) : (
625
- <></>
626
- )}
627
- {!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(
661
- 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}
675
- />
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
- )}
710
- </tbody>
711
- </table>
712
-
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
- )}
737
-
738
- {children}
739
- {hasMore && (
740
- <div ref={loaderRef} className="infinite-scroll-loader">
741
- <Loader display={true} />
742
- </div>
743
- )}
744
- </div>
745
- );
746
- }
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { useIsMobile } from "../_core/hooks/useIsMobile";
4
+ import { Card } from "../card/Card";
5
+ import { TypeInputEnum } from "../form/form.enum";
6
+ import { Input } from "../form/input/Input";
7
+ import { Icon } from "../icon/Icon";
8
+ import { Loader } from "../loader/Loader";
9
+ import "./dataTable.css";
10
+ import {
11
+ dataTableActionsInterface,
12
+ dataTableColumnsInterface,
13
+ } from "./dataTable.interface";
14
+ import { DataTableAction } from "./dataTableActions/DataTableAction";
15
+ import { DataTableColumn } from "./dataTableColumn/DataTableColumn";
16
+ import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
17
+ import { DataTableRow } from "./dataTableRow/DataTableRow";
18
+ import { DataTableFilters } from "./dataTableFilters/DataTableFilters";
19
+ import { useDataTableState } from "./useDataTableState";
20
+ import { DataTableFilterState, DataTableSortState, flattenColumns, getActionsWidth } from "./dataTable.utils";
21
+ export type {
22
+ DataTableDateRangeFilterValue, DataTableFilterState, DataTableSortDirection,
23
+ DataTableSortState, FilterValue,
24
+ } from "./dataTable.utils";
25
+ const DEFAULT_FILTER_COLUMN_WIDTH = 100;
26
+
27
+ export type DataTableProps<T> = {
28
+ id: string;
29
+ name?: string;
30
+ className?: string;
31
+ children?: JSX.Element;
32
+ element?: JSX.Element;
33
+ data: T[];
34
+ emptyMessage?: string;
35
+ actionsColumnWidth?: number;
36
+ actions?: dataTableActionsInterface<T>[];
37
+ footerAction?: (props: {
38
+ close: () => void;
39
+ selectedRows: T[];
40
+ }) => JSX.Element;
41
+ columns: dataTableColumnsInterface<T>[];
42
+ loading?: boolean;
43
+ onRowClick?: (row: any) => void;
44
+ onColumnClick?: (column: any) => void;
45
+ onLoadMore?: () => void;
46
+ hasMore?: boolean; // pour savoir s'il reste des données à charger
47
+ onSelectionChange?: (selectedRows: T[]) => void;
48
+ autoFocusFirstEditable?: boolean;
49
+ hideToggleAll?: boolean;
50
+ limitSelect?: number;
51
+ getRowId?: (row: T) => string | number;
52
+ onRowStyle?: (row: T) => React.CSSProperties | undefined;
53
+ onRowClassName?: (row: T) => string | undefined;
54
+ rowStyle?: (row: T) => React.CSSProperties | undefined;
55
+ rowClassName?: (row: T) => string | undefined;
56
+ mobileCardView?: boolean;
57
+ onFiltersChange?: (next: DataTableFilterState) => void;
58
+ onSortChange?: (next: DataTableSortState | null) => void;
59
+ onEditColumnByOverlay?: (row: T) => void;
60
+ };
61
+ export function DataTable<T>(props: DataTableProps<T>) {
62
+ const {
63
+ id,
64
+ name,
65
+ className,
66
+ children,
67
+ element,
68
+ data: sourceData = [],
69
+ actionsColumnWidth: explicitActionsWidth,
70
+ actions = [],
71
+ footerAction,
72
+ columns: groupedColumns = [],
73
+ loading,
74
+ onRowClick,
75
+ onColumnClick,
76
+ onLoadMore,
77
+ emptyMessage,
78
+ hasMore,
79
+ onSelectionChange,
80
+ autoFocusFirstEditable = false,
81
+ hideToggleAll = false,
82
+ limitSelect = undefined,
83
+ getRowId,
84
+ onRowStyle,
85
+ onRowClassName,
86
+ rowStyle,
87
+ rowClassName,
88
+ mobileCardView = false,
89
+ onFiltersChange,
90
+ onSortChange,
91
+ onEditColumnByOverlay,
92
+ } = props;
93
+ const tableState = useDataTableState(id, groupedColumns, sourceData, onFiltersChange, onSortChange);
94
+ const { flatColumns: columns, sortedData: data } = tableState;
95
+ const actionsColumnWidth = getActionsWidth(actions, explicitActionsWidth);
96
+ const filterRowRef = useRef<HTMLTableRowElement>(null);
97
+ const groupRowRef = useRef<HTMLTableRowElement>(null);
98
+ const [filterHeight, setFilterHeight] = useState(0);
99
+ const [groupHeight, setGroupHeight] = useState(0);
100
+ const hasGroups = groupedColumns.some((column) => column.children?.length);
101
+ const stickyOffsets = useMemo(() => {
102
+ const offsets = new Map<string, number>();
103
+ let left = onSelectionChange ? 40 : 0;
104
+ columns.forEach((column) => {
105
+ if (!column.sticky) return;
106
+ offsets.set(column.id, left);
107
+ const width = column.width ?? column.minWidth ?? column.maxWidth ?? 100;
108
+ left += typeof width === "number" ? width : Number.parseFloat(width);
109
+ });
110
+ return offsets;
111
+ }, [columns, onSelectionChange]);
112
+
113
+ useLayoutEffect(() => {
114
+ const measure = () => {
115
+ setFilterHeight(filterRowRef.current?.getBoundingClientRect().height ?? 0);
116
+ setGroupHeight(groupRowRef.current?.getBoundingClientRect().height ?? 0);
117
+ };
118
+ measure();
119
+ if (typeof ResizeObserver === "undefined") {
120
+ window.addEventListener("resize", measure);
121
+ return () => window.removeEventListener("resize", measure);
122
+ }
123
+ const observer = new ResizeObserver(measure);
124
+ if (filterRowRef.current) observer.observe(filterRowRef.current);
125
+ if (groupRowRef.current) observer.observe(groupRowRef.current);
126
+ return () => observer.disconnect();
127
+ }, [tableState.visibleFilters.size, hasGroups, groupedColumns]);
128
+
129
+ const isMobile = useIsMobile();
130
+ const showMobileCardView = mobileCardView && isMobile;
131
+ const loaderRef = useRef<HTMLDivElement | null>(null);
132
+ const footerActionRef = useRef<HTMLDivElement | null>(null);
133
+ const hasAutoFocusedRef = useRef(false);
134
+ const [isFooterActionVisible, setIsFooterActionVisible] = useState(false);
135
+ const [footerActionOffset, setFooterActionOffset] = useState(0);
136
+ const [selectedRowIds, setSelectedRowIds] = useState<Set<string | number>>(
137
+ new Set(),
138
+ );
139
+
140
+ const focusEditableElement = useCallback((element: HTMLElement | null) => {
141
+ if (!element) {
142
+ return;
143
+ }
144
+
145
+ element.scrollIntoView({ block: "nearest", inline: "nearest" });
146
+
147
+ requestAnimationFrame(() => {
148
+ element.focus();
149
+
150
+ if (
151
+ element instanceof HTMLInputElement ||
152
+ element instanceof HTMLTextAreaElement
153
+ ) {
154
+ element.select();
155
+ }
156
+ });
157
+ }, []);
158
+
159
+ const isInteractiveClick = useCallback((target: EventTarget | null) => {
160
+ const element =
161
+ target instanceof Element
162
+ ? target
163
+ : target instanceof Node
164
+ ? target.parentElement
165
+ : null;
166
+
167
+ if (!element) {
168
+ return false;
169
+ }
170
+
171
+ return Boolean(
172
+ element.closest(
173
+ "input,button,a,select,textarea,label,option,[role='button'],[role='checkbox'],[role='option'],[contenteditable='true'],[data-stop-row-click='true']",
174
+ ),
175
+ );
176
+ }, []);
177
+
178
+ const toggleSelectAll = useCallback(() => {
179
+ if (selectedRowIds.size === data.length) {
180
+ setSelectedRowIds(new Set());
181
+ onSelectionChange?.([]);
182
+ setIsFooterActionVisible(false);
183
+ } else {
184
+ let allIds = data.map((d) =>
185
+ getRowId ? getRowId(d) : ((d as any).id ?? d),
186
+ );
187
+
188
+ if (limitSelect && allIds.length > limitSelect) {
189
+ allIds = allIds.slice(0, limitSelect);
190
+ }
191
+
192
+ const newSelection = new Set(allIds);
193
+ setSelectedRowIds(newSelection);
194
+ setIsFooterActionVisible(true);
195
+ onSelectionChange?.(
196
+ data.filter((d) =>
197
+ newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
198
+ ),
199
+ );
200
+ }
201
+ }, [data, onSelectionChange, limitSelect, getRowId, selectedRowIds.size]);
202
+
203
+ const focusNextColumn = useCallback(
204
+ (columnIndex: number, rowIndex: number) => {
205
+ if (typeof document === "undefined") {
206
+ return;
207
+ }
208
+
209
+ const table =
210
+ document.getElementById(`${id}-mobile`) ??
211
+ document.getElementById(id);
212
+ const eligibleColumnIndexes = columns.reduce<number[]>(
213
+ (indexes, column, idx) => {
214
+ if (
215
+ column.editable &&
216
+ column.enterToNextColumn &&
217
+ column.type !== "select" &&
218
+ column.type !== "image"
219
+ ) {
220
+ indexes.push(idx);
221
+ }
222
+
223
+ return indexes;
224
+ },
225
+ [],
226
+ );
227
+
228
+ if (!table || eligibleColumnIndexes.length === 0) {
229
+ return;
230
+ }
231
+
232
+ const currentEligibleIndex = eligibleColumnIndexes.indexOf(columnIndex);
233
+
234
+ if (currentEligibleIndex === -1) {
235
+ return;
236
+ }
237
+
238
+ const editableCells = data.flatMap((_, currentRowIndex) =>
239
+ eligibleColumnIndexes.map((currentColumnIndex) => ({
240
+ rowIndex: currentRowIndex,
241
+ columnIndex: currentColumnIndex,
242
+ })),
243
+ );
244
+
245
+ const currentCellIndex = editableCells.findIndex(
246
+ (cell) =>
247
+ cell.rowIndex === rowIndex && cell.columnIndex === columnIndex,
248
+ );
249
+
250
+ if (currentCellIndex === -1) {
251
+ return;
252
+ }
253
+
254
+ const focusableSelector =
255
+ 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]';
256
+ let nextInput: HTMLElement | null = null;
257
+
258
+ for (let offset = 1; offset <= editableCells.length; offset += 1) {
259
+ const nextCell =
260
+ editableCells[(currentCellIndex + offset) % editableCells.length];
261
+ const cell = table.querySelector(
262
+ `tbody td[data-row-index="${nextCell.rowIndex}"][data-column-index="${nextCell.columnIndex}"]`,
263
+ );
264
+
265
+ if (!(cell instanceof HTMLElement)) {
266
+ continue;
267
+ }
268
+
269
+ const focusableElement =
270
+ cell.querySelector<HTMLElement>(focusableSelector);
271
+
272
+ if (focusableElement) {
273
+ nextInput = focusableElement;
274
+ break;
275
+ }
276
+ }
277
+
278
+ if (!nextInput) {
279
+ return;
280
+ }
281
+
282
+ focusEditableElement(nextInput);
283
+ },
284
+ [columns, data, focusEditableElement, id],
285
+ );
286
+
287
+ const focusNextRow = useCallback(
288
+ (rowIndex: number) => {
289
+ if (typeof document === "undefined") {
290
+ return;
291
+ }
292
+
293
+ const table =
294
+ document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
295
+ if (!table) {
296
+ return;
297
+ }
298
+
299
+ const eligibleColumnIndexes = columns.reduce<number[]>(
300
+ (indexes, column, idx) => {
301
+ if (
302
+ column.editable &&
303
+ column.type !== "select" &&
304
+ column.type !== "image"
305
+ ) {
306
+ indexes.push(idx);
307
+ }
308
+
309
+ return indexes;
310
+ },
311
+ [],
312
+ );
313
+
314
+ if (eligibleColumnIndexes.length === 0) {
315
+ return;
316
+ }
317
+
318
+ const firstEligibleColumnIndex = eligibleColumnIndexes[0];
319
+ const focusableSelector =
320
+ 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]';
321
+ let nextInput: HTMLElement | null = null;
322
+
323
+ for (let offset = 1; offset <= data.length; offset += 1) {
324
+ const nextRowIndex = (rowIndex + offset) % data.length;
325
+ const cell = table.querySelector(
326
+ `[data-row-index="${nextRowIndex}"][data-column-index="${firstEligibleColumnIndex}"]`,
327
+ );
328
+
329
+ if (!(cell instanceof HTMLElement)) {
330
+ continue;
331
+ }
332
+
333
+ const focusableElement =
334
+ cell.querySelector<HTMLElement>(focusableSelector);
335
+
336
+ if (focusableElement) {
337
+ nextInput = focusableElement;
338
+ break;
339
+ }
340
+ }
341
+
342
+ if (!nextInput) {
343
+ return;
344
+ }
345
+
346
+ focusEditableElement(nextInput);
347
+ },
348
+ [columns, data.length, focusEditableElement, id],
349
+ );
350
+
351
+ const handleClickRow = useCallback(
352
+ (e: React.MouseEvent, row: T) => {
353
+ e.stopPropagation();
354
+
355
+ if (isInteractiveClick(e.target)) {
356
+ return;
357
+ }
358
+
359
+ const selectedText =
360
+ typeof window !== "undefined"
361
+ ? window.getSelection?.()?.toString().trim()
362
+ : "";
363
+ if (selectedText) {
364
+ return;
365
+ }
366
+
367
+ if (onRowClick) {
368
+ onRowClick?.(row);
369
+ }
370
+ },
371
+ [isInteractiveClick, onRowClick],
372
+ );
373
+ const toggleRow = useCallback(
374
+ (e: React.ChangeEvent<HTMLInputElement>, row: T) => {
375
+ e.stopPropagation();
376
+ const rowId = getRowId ? getRowId(row) : ((row as any).id ?? row);
377
+ const newSelection = new Set(selectedRowIds);
378
+
379
+ if (newSelection.has(rowId)) {
380
+ newSelection.delete(rowId);
381
+ } else {
382
+ if (limitSelect && newSelection.size >= limitSelect) {
383
+ // 🚫 limite atteinte
384
+ return;
385
+ }
386
+ newSelection.add(rowId);
387
+ }
388
+
389
+ setSelectedRowIds(newSelection);
390
+ setIsFooterActionVisible(newSelection.size > 0);
391
+ const selectedItems = data.filter((d) =>
392
+ newSelection.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
393
+ );
394
+ onSelectionChange?.(selectedItems);
395
+ },
396
+ [data, getRowId, limitSelect, onSelectionChange, selectedRowIds],
397
+ );
398
+ const showFooterAction = Boolean(footerAction && isFooterActionVisible);
399
+ const closeFooterAction = useCallback(() => {
400
+ setIsFooterActionVisible(false);
401
+ }, []);
402
+ const selectedRows = data.filter((d) =>
403
+ selectedRowIds.has(getRowId ? getRowId(d) : ((d as any).id ?? d)),
404
+ );
405
+ useEffect(() => {
406
+ if (loading || !autoFocusFirstEditable || data.length === 0) {
407
+ hasAutoFocusedRef.current = false;
408
+ return;
409
+ }
410
+
411
+ if (hasAutoFocusedRef.current) {
412
+ return;
413
+ }
414
+
415
+ const firstEditableColumnIndex = columns.findIndex(
416
+ (column) =>
417
+ column.editable &&
418
+ column.enterToNextColumn &&
419
+ column.type !== "select" &&
420
+ column.type !== "image",
421
+ );
422
+
423
+ if (firstEditableColumnIndex === -1) {
424
+ return;
425
+ }
426
+
427
+ const table =
428
+ document.getElementById(`${id}-mobile`) ?? document.getElementById(id);
429
+ const firstCell = table?.querySelector(
430
+ `[data-row-index="0"][data-column-index="${firstEditableColumnIndex}"]`,
431
+ );
432
+
433
+ if (!(firstCell instanceof HTMLElement)) {
434
+ return;
435
+ }
436
+
437
+ const firstInput = firstCell.querySelector<HTMLElement>(
438
+ 'input:not([disabled]), textarea:not([disabled]), [contenteditable="true"]',
439
+ );
440
+
441
+ if (!firstInput) {
442
+ return;
443
+ }
444
+
445
+ hasAutoFocusedRef.current = true;
446
+ focusEditableElement(firstInput);
447
+ }, [
448
+ autoFocusFirstEditable,
449
+ columns,
450
+ data.length,
451
+ focusEditableElement,
452
+ id,
453
+ loading,
454
+ ]);
455
+ useEffect(() => {
456
+ if (!showFooterAction) {
457
+ setFooterActionOffset(0);
458
+ return;
459
+ }
460
+
461
+ const footerActionElement = footerActionRef.current;
462
+
463
+ if (!footerActionElement) {
464
+ return;
465
+ }
466
+
467
+ const updateFooterActionOffset = () => {
468
+ const { height } = footerActionElement.getBoundingClientRect();
469
+ setFooterActionOffset(Math.ceil(height) + 16);
470
+ };
471
+
472
+ updateFooterActionOffset();
473
+
474
+ if (typeof ResizeObserver === "undefined") {
475
+ return;
476
+ }
477
+
478
+ const resizeObserver = new ResizeObserver(() => {
479
+ updateFooterActionOffset();
480
+ });
481
+
482
+ resizeObserver.observe(footerActionElement);
483
+
484
+ return () => {
485
+ resizeObserver.disconnect();
486
+ };
487
+ }, [showFooterAction]);
488
+ useEffect(() => {
489
+ if (!onLoadMore || !hasMore) return;
490
+
491
+ const observer = new IntersectionObserver(
492
+ (entries) => {
493
+ if (entries[0].isIntersecting) {
494
+ onLoadMore();
495
+ }
496
+ },
497
+ {
498
+ root: null,
499
+ rootMargin: "0px",
500
+ threshold: 1.0,
501
+ },
502
+ );
503
+
504
+ if (loaderRef.current) {
505
+ observer.observe(loaderRef.current);
506
+ }
507
+
508
+ return () => {
509
+ if (loaderRef.current) {
510
+ observer.unobserve(loaderRef.current);
511
+ }
512
+ };
513
+ }, [onLoadMore, hasMore]);
514
+ return (
515
+ <div
516
+ className={`data-table-container ${showFooterAction ? "data-table-container--with-footer-action" : ""}`}
517
+ style={
518
+ showFooterAction
519
+ ? { paddingBottom: `${footerActionOffset}px` }
520
+ : undefined
521
+ }
522
+ >
523
+ {name && <h2 className="mb-2">{name}</h2>}
524
+ {element}
525
+ <DataTableFilters currentFilters={tableState.filters} setCurrentFilters={tableState.setFilters} />
526
+ {showMobileCardView && columns.some((column) => column.filterable || column.sortable) && (
527
+ <div className="data-table-mobile-controls">
528
+ {columns.filter((column) => column.filterable || column.sortable).map((column, idx) => (
529
+ <div key={column.id}>
530
+ <table><thead><tr><DataTableColumn column={column} idx={idx}
531
+ isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
532
+ onToggleFilter={tableState.toggleFilter} onToggleSort={tableState.toggleSort}
533
+ sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
534
+ /></tr></thead></table>
535
+ {tableState.renderFilter(column)}
536
+ </div>
537
+ ))}
538
+ </div>
539
+ )}
540
+ {showMobileCardView && (
541
+ <div
542
+ id={`${id}-mobile`}
543
+ className={`data-table-mobile-cards ${className ? className : ""}`}
544
+ >
545
+ {loading && <Loader display={loading} />}
546
+ {!loading &&
547
+ data.map((row, rowIndex) => {
548
+ const customStyle = (onRowStyle ?? rowStyle)?.(row);
549
+ const customClass =
550
+ (onRowClassName ?? rowClassName)?.(row) ?? "";
551
+ const rowId = getRowId
552
+ ? getRowId(row)
553
+ : ((row as any).id ?? row);
554
+
555
+ return (
556
+ <Card
557
+ key={rowId as React.Key}
558
+ item={{ title: "" }}
559
+ className={`data-table-mobile-card ${customClass}`.trim()}
560
+ handleClick={onRowClick ? () => onRowClick(row) : undefined}
561
+ >
562
+ <div
563
+ className="data-table-mobile-card__content"
564
+ style={customStyle}
565
+ data-row-index={rowIndex}
566
+ onMouseDown={(event) => {
567
+ if (isInteractiveClick(event.target)) {
568
+ event.stopPropagation();
569
+ }
570
+ }}
571
+ >
572
+ {onSelectionChange && (
573
+ <div className="data-table-mobile-card__field data-table-mobile-card__selection">
574
+ <span className="data-table-mobile-card__label">
575
+ Sélection
576
+ </span>
577
+ <Input
578
+ id={`select-card-${rowIndex}`}
579
+ name={`select-card-${rowIndex}`}
580
+ type={TypeInputEnum.CHECKBOX}
581
+ disabled={
582
+ limitSelect !== undefined &&
583
+ selectedRowIds.size >= limitSelect &&
584
+ !selectedRowIds.has(rowId)
585
+ }
586
+ checked={selectedRowIds.has(rowId)}
587
+ onChange={(event) => toggleRow(event, row)}
588
+ />
589
+ </div>
590
+ )}
591
+ {columns.map((column, columnIndex) => (
592
+ <div
593
+ className="data-table-mobile-card__field"
594
+ key={column.id}
595
+ >
596
+ <span className="data-table-mobile-card__label">
597
+ {column.label}
598
+ </span>
599
+ <DataTableRow
600
+ container="card"
601
+ column={column}
602
+ idx={columnIndex}
603
+ row={row}
604
+ rowIndex={rowIndex}
605
+ onEnterNextColumn={focusNextColumn}
606
+ onEditColumnByOverlay={onEditColumnByOverlay}
607
+ />
608
+ </div>
609
+ ))}
610
+ {actions.length > 0 && (
611
+ <div className="data-table-mobile-card__actions card-actions">
612
+ {actions.map((action, actionIndex) => (
613
+ <DataTableAction
614
+ key={action.id}
615
+ action={action}
616
+ row={row}
617
+ rowIndex={rowIndex}
618
+ index={actionIndex}
619
+ onFocusNextRow={focusNextRow}
620
+ />
621
+ ))}
622
+ </div>
623
+ )}
624
+ </div>
625
+ </Card>
626
+ );
627
+ })}
628
+ {!loading && data.length === 0 && (
629
+ <div className="data-table-mobile-cards__empty">
630
+ {emptyMessage ?? "Aucune donnée"}
631
+ </div>
632
+ )}
633
+ </div>
634
+ )}
635
+ <table
636
+ className={`data-table ${className ? className : ""}`}
637
+ id={id}
638
+ style={showMobileCardView ? { display: "none" } : undefined}
639
+ >
640
+ <colgroup>
641
+ {onSelectionChange && <col style={{ width: 40 }} />}
642
+ {columns.map((column) => <col key={column.id} style={{ width: column.width, minWidth: column.minWidth, maxWidth: column.maxWidth }} />)}
643
+ {actions.length > 0 && <col style={{ width: actionsColumnWidth }} />}
644
+ </colgroup>
645
+ <thead>
646
+ {tableState.visibleFilters.size > 0 && <tr className="data-table-filter-row" ref={filterRowRef}>
647
+ {onSelectionChange && <th />}
648
+ {columns.map((column) => <th className="filter-row-cell" key={column.id}
649
+ style={{ position: "sticky", top: 0, zIndex: 14, background: "#f9fbfc",
650
+ width: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
651
+ minWidth: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
652
+ maxWidth: column.width ?? DEFAULT_FILTER_COLUMN_WIDTH,
653
+ ...(column.sticky ? { left: stickyOffsets.get(column.id) } : {}) }}>
654
+ {tableState.renderFilter(column)}
655
+ </th>)}
656
+ {actions.length > 0 && <th />}
657
+ </tr>}
658
+ {hasGroups && <tr ref={groupRowRef} className="data-table-group-row">
659
+ {onSelectionChange && <th />}
660
+ {groupedColumns.map((column) => <th key={column.id} colSpan={flattenColumns([column]).length}
661
+ style={{ top: filterHeight }}>{column.children?.length ? column.label : ""}</th>)}
662
+ {actions.length > 0 && <th />}
663
+ </tr>}
664
+ <tr>
665
+ {onSelectionChange && (
666
+ <th
667
+ className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
668
+ style={{ width: "40px", textAlign: "center", top: filterHeight + groupHeight,
669
+ ...(stickyOffsets.size > 0 ? { left: 0, zIndex: 12 } : {}) }}
670
+ >
671
+ <Input
672
+ id={`select-th-${id}`}
673
+ name={`select-th-${id}`}
674
+ type={TypeInputEnum.CHECKBOX}
675
+ checked={
676
+ data.length > 0 && selectedRowIds.size === data.length
677
+ }
678
+ onChange={toggleSelectAll}
679
+ />
680
+ </th>
681
+ )}
682
+ {columns.map((column, idx) => (
683
+ <DataTableColumn
684
+ key={idx}
685
+ column={column}
686
+ idx={idx}
687
+ onColumnClick={onColumnClick}
688
+ stickyLeft={stickyOffsets.get(column.id)}
689
+ stickyTop={filterHeight + groupHeight}
690
+ isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
691
+ onToggleFilter={tableState.toggleFilter}
692
+ onToggleSort={tableState.toggleSort}
693
+ sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
694
+ />
695
+ ))}
696
+ {actions.length > 0 && (
697
+ <th
698
+ style={{
699
+ width: `${actionsColumnWidth}px`,
700
+ minWidth: `${actionsColumnWidth}px`,
701
+ }}
702
+ align="right"
703
+ key={"actions"}
704
+ ></th>
705
+ )}
706
+ </tr>
707
+ </thead>
708
+ <tbody>
709
+ {loading ? (
710
+ <tr>
711
+ <td
712
+ align="center"
713
+ colSpan={columns.length + (actions.length > 0 ? 1 : 0) + (onSelectionChange ? 1 : 0)}
714
+ >
715
+ <div className="mt-3">
716
+ <Loader display={loading} />
717
+ </div>
718
+ </td>
719
+ </tr>
720
+ ) : (
721
+ <></>
722
+ )}
723
+ {!loading &&
724
+ data.map((row, rowIndex) => {
725
+ const customStyle = (onRowStyle ?? rowStyle)?.(row);
726
+ const customClass = (onRowClassName ?? rowClassName)?.(row) ?? "";
727
+
728
+ return (
729
+ <tr
730
+ key={rowIndex}
731
+ className={`${rowIndex % 2 === 0 ? "even" : "odd"} ${customClass}`.trim()}
732
+ style={customStyle}
733
+ data-row-index={rowIndex}
734
+ onClick={(e) => handleClickRow?.(e, row)}
735
+ >
736
+ {onSelectionChange && (
737
+ <td
738
+ className="select-cell"
739
+ style={{ textAlign: "center" }}
740
+ onClick={(e) => {
741
+ e.stopPropagation();
742
+ setIsFooterActionVisible(true);
743
+ }}
744
+ >
745
+ <Input
746
+ id={`select-row-${rowIndex}`}
747
+ name={`select-row-${rowIndex}`}
748
+ type={TypeInputEnum.CHECKBOX}
749
+ disabled={
750
+ limitSelect !== undefined &&
751
+ selectedRowIds.size >= limitSelect &&
752
+ !selectedRowIds.has(
753
+ getRowId ? getRowId(row) : ((row as any).id ?? row),
754
+ )
755
+ }
756
+ checked={selectedRowIds.has(
757
+ getRowId ? getRowId(row) : ((row as any).id ?? row),
758
+ )}
759
+ onChange={(e) => toggleRow(e, row)}
760
+ />
761
+ </td>
762
+ )}
763
+ {columns.map((column, idx) => (
764
+ <DataTableRow
765
+ column={column}
766
+ idx={idx}
767
+ row={row}
768
+ rowIndex={rowIndex}
769
+ stickyLeft={stickyOffsets.get(column.id)}
770
+ onEditColumnByOverlay={onEditColumnByOverlay}
771
+ key={idx}
772
+ onEnterNextColumn={focusNextColumn}
773
+ />
774
+ ))}
775
+ {actions.length > 0 && (
776
+ <td
777
+ style={{
778
+ width: `${actionsColumnWidth}px`,
779
+ minWidth: `${actionsColumnWidth}px`,
780
+ }}
781
+ align="right"
782
+ onClick={(e) => e.stopPropagation()}
783
+ >
784
+ <div className="d-flex align-items-center justify-content-end">
785
+ {actions.map((action, index) => (
786
+ <DataTableAction
787
+ key={index}
788
+ action={action}
789
+ row={row}
790
+ rowIndex={rowIndex}
791
+ index={index}
792
+ onFocusNextRow={focusNextRow}
793
+ />
794
+ ))}
795
+ </div>
796
+ </td>
797
+ )}
798
+ </tr>
799
+ );
800
+ })}
801
+ {!loading && data.length === 0 && (
802
+ <DataTableLoading
803
+ columns={columns}
804
+ actions={actions}
805
+ emptyMessage={emptyMessage}
806
+ hasSelection={Boolean(onSelectionChange)}
807
+ />
808
+ )}
809
+ </tbody>
810
+ </table>
811
+
812
+ {showFooterAction &&
813
+ typeof document !== "undefined" &&
814
+ createPortal(
815
+ <div
816
+ ref={footerActionRef}
817
+ className="data-table-footer-action"
818
+ data-footer-action="true"
819
+ >
820
+ <div className="data-table-footer-action__content">
821
+ <div
822
+ className="data-table-footer-action__close"
823
+ aria-label="Fermer les actions"
824
+ onClick={closeFooterAction}
825
+ >
826
+ <Icon classIcon={"bi-x"} />
827
+ </div>
828
+ {footerAction?.({
829
+ close: closeFooterAction,
830
+ selectedRows,
831
+ })}
832
+ </div>
833
+ </div>,
834
+ document.body,
835
+ )}
836
+
837
+ {children}
838
+ {hasMore && (
839
+ <div ref={loaderRef} className="infinite-scroll-loader">
840
+ <Loader display={true} />
841
+ </div>
842
+ )}
843
+ </div>
844
+ );
845
+ }