kelt-ui-kit-react 2.1.0 → 2.1.2

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 (32) hide show
  1. package/dist/button/buttonImport/ButtonImport.d.ts +13 -0
  2. package/dist/button/buttonImport/ButtonImport.view.d.ts +1 -0
  3. package/dist/dataTable/dataTable.interface.d.ts +2 -0
  4. package/dist/dataTable/dataTable.utils.d.ts +3 -0
  5. package/dist/dataTable/dataTableActions/DataTableActionMenu.d.ts +9 -0
  6. package/dist/dataTable/dataTableColumn/DataTableColumn.d.ts +3 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +2622 -2419
  9. package/dist/style.css +1 -1
  10. package/package.json +1 -1
  11. package/src/App.css +953 -161
  12. package/src/App.menu.tsx +8 -0
  13. package/src/button/buttonFile/ButtonFile.tsx +4 -1
  14. package/src/button/buttonImport/ButtonImport.tsx +99 -0
  15. package/src/button/buttonImport/ButtonImport.view.tsx +17 -0
  16. package/src/button/buttonImport/buttonImport.css +38 -0
  17. package/src/dataTable/DataTable.tsx +24 -25
  18. package/src/dataTable/DataTable.view.tsx +13 -0
  19. package/src/dataTable/dataTable.css +51 -3
  20. package/src/dataTable/dataTable.interface.ts +2 -0
  21. package/src/dataTable/dataTable.utils.ts +15 -2
  22. package/src/dataTable/dataTableActions/DataTableAction.tsx +2 -1
  23. package/src/dataTable/dataTableActions/DataTableActionMenu.tsx +95 -0
  24. package/src/dataTable/dataTableColumn/DataTableColumn.tsx +68 -0
  25. package/src/dataTable/dataTableRow/DataTableRow.tsx +3 -1
  26. package/src/docs/catalog.ts +1 -0
  27. package/src/index.ts +1 -0
  28. package/src/select/Select.tsx +1 -1
  29. package/src/select/select.css +28 -22
  30. package/src/tabs/tabs.css +7 -1
  31. package/tests/dataTable.test.mjs +9 -0
  32. package/tests/documentation.test.mjs +1 -1
package/src/App.menu.tsx CHANGED
@@ -6,6 +6,7 @@ import { AddressSearchView } from "./addressSearch/AddressSearch.view";
6
6
  import { BadgeView } from "./badge/Badge.view";
7
7
  import { ButtonView } from "./button/Button.view";
8
8
  import { ButtonFileView } from "./button/buttonFile/ButtonFile.view";
9
+ import { ButtonImportView } from "./button/buttonImport/ButtonImport.view";
9
10
  import { CardView } from "./card/Card.view";
10
11
  import { CarouselView } from "./carousel/Carousel.view";
11
12
  import { DamierView } from "./damier/Damier.view";
@@ -251,6 +252,13 @@ export const MenuList: MenuProps[] = [
251
252
  children: <NavLink to="/address-search">Address search</NavLink>,
252
253
  element: <AddressSearchView />,
253
254
  },
255
+ {
256
+ id: 31,
257
+ name: "Button Import",
258
+ link: "/button-import",
259
+ children: <NavLink to="/button-import">Button Import</NavLink>,
260
+ element: <ButtonImportView />,
261
+ },
254
262
  ];
255
263
 
256
264
  export const AppMenu = (): JSX.Element => {
@@ -31,6 +31,9 @@ export const ButtonFile = ({
31
31
 
32
32
  reader.readAsArrayBuffer(file);
33
33
  }
34
+
35
+ // Permet de resélectionner le même fichier (sinon onChange ne se déclenche pas)
36
+ e.target.value = "";
34
37
  };
35
38
 
36
39
  const handleButtonClick = () => {
@@ -43,7 +46,7 @@ export const ButtonFile = ({
43
46
  type="file"
44
47
  ref={fileInputRef}
45
48
  onChange={handleFileChange}
46
- className="hidden"
49
+ style={{ display: "none" }}
47
50
  accept={accept}
48
51
  />
49
52
  <Button
@@ -0,0 +1,99 @@
1
+ import { ChangeEvent, useRef, useState } from "react";
2
+ import { Icon } from "../../icon/Icon";
3
+ import { IconSizeEnum } from "../../icon/iconSize.enum";
4
+ import { Modal } from "../../modal/Modal";
5
+ import { Button } from "../Button";
6
+ import "./buttonImport.css";
7
+
8
+ interface ButtonImportProps {
9
+ onValidate: (file: File) => void | Promise<void>;
10
+ accept?: string;
11
+ buttonText?: string;
12
+ buttonClassName?: string;
13
+ buttonClassIcon?: string;
14
+ title?: string;
15
+ description?: string;
16
+ classIcon?: string;
17
+ validateText?: string;
18
+ }
19
+
20
+ export const ButtonImport = ({
21
+ onValidate,
22
+ accept = "*",
23
+ buttonText = "Importer",
24
+ buttonClassName,
25
+ buttonClassIcon = "",
26
+ title = "Importer un fichier",
27
+ description = "Sélectionner un fichier à importer.",
28
+ classIcon = "bi-file-earmark-arrow-up",
29
+ validateText = "Valider",
30
+ }: ButtonImportProps) => {
31
+ const fileInputRef = useRef<HTMLInputElement>(null);
32
+ const [isOpen, setIsOpen] = useState<boolean>(false);
33
+ const [file, setFile] = useState<File | null>(null);
34
+ const [loading, setLoading] = useState<boolean>(false);
35
+
36
+ const handleClose = () => {
37
+ if (loading) return;
38
+ setIsOpen(false);
39
+ setFile(null);
40
+ };
41
+
42
+ const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
43
+ setFile(e.target.files?.[0] ?? null);
44
+ // Permet de resélectionner le même fichier
45
+ e.target.value = "";
46
+ };
47
+
48
+ const handleValidate = async () => {
49
+ if (!file) return;
50
+ setLoading(true);
51
+ try {
52
+ await onValidate(file);
53
+ setIsOpen(false);
54
+ setFile(null);
55
+ } catch {
56
+ // L'appelant gère l'erreur, la modal reste ouverte pour réessayer
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <>
64
+ <Button
65
+ className={buttonClassName}
66
+ classIcon={buttonClassIcon}
67
+ title={buttonText}
68
+ onClick={() => setIsOpen(true)}
69
+ />
70
+ <Modal isOpen={isOpen} onClose={handleClose} title={title}>
71
+ <input
72
+ type="file"
73
+ ref={fileInputRef}
74
+ onChange={handleFileChange}
75
+ style={{ display: "none" }}
76
+ accept={accept}
77
+ />
78
+ <div
79
+ className="button-import-zone"
80
+ onClick={() => fileInputRef.current?.click()}
81
+ >
82
+ <div>{description}</div>
83
+ <Icon classIcon={classIcon} size={IconSizeEnum.LARGE} />
84
+ <div className="button-import-file mt-2">
85
+ {file ? file.name : "Aucun fichier sélectionné"}
86
+ </div>
87
+ </div>
88
+ <div className="d-flex justify-content-end mt-2">
89
+
90
+ <Button
91
+ title={validateText}
92
+ disabled={!file || loading}
93
+ onClick={handleValidate}
94
+ />
95
+ </div>
96
+ </Modal>
97
+ </>
98
+ );
99
+ };
@@ -0,0 +1,17 @@
1
+ import { ButtonImport } from "./ButtonImport";
2
+
3
+ export const ButtonImportView = () => {
4
+ return (
5
+ <div className="d-flex">
6
+ <ButtonImport
7
+ accept=".csv,text/csv"
8
+ buttonText="Importer CSV"
9
+ title="Import des stocks"
10
+ description="Sélectionner un fichier CSV afin de mettre à jour les stocks."
11
+ onValidate={(file: File) => {
12
+ console.log("Fichier validé:", file.name, file.size);
13
+ }}
14
+ />
15
+ </div>
16
+ );
17
+ };
@@ -0,0 +1,38 @@
1
+ .button-import-zone {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 100%;
6
+ flex-direction: column;
7
+ text-align: center;
8
+ padding: 1rem 0;
9
+ cursor: pointer;
10
+
11
+ > div {
12
+ margin-bottom: 1rem;
13
+ color: #8f8888;
14
+ }
15
+
16
+ .bi {
17
+ border-radius: 50%;
18
+ border: 1px solid #d5d5d5;
19
+ padding: 2rem;
20
+ color: #d5d5d5;
21
+ width: 5rem;
22
+ height: 5rem;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ overflow: hidden;
27
+ }
28
+
29
+ &:hover .bi {
30
+ border-color: #8f8888;
31
+ color: #8f8888;
32
+ }
33
+
34
+ .button-import-file {
35
+ font-weight: 600;
36
+ word-break: break-all;
37
+ }
38
+ }
@@ -12,17 +12,17 @@ import {
12
12
  dataTableColumnsInterface,
13
13
  } from "./dataTable.interface";
14
14
  import { DataTableAction } from "./dataTableActions/DataTableAction";
15
+ import { DataTableActionMenu } from "./dataTableActions/DataTableActionMenu";
15
16
  import { DataTableColumn } from "./dataTableColumn/DataTableColumn";
16
17
  import { DataTableLoading } from "./dataTableLoading/DataTableLoading";
17
18
  import { DataTableRow } from "./dataTableRow/DataTableRow";
18
19
  import { DataTableFilters } from "./dataTableFilters/DataTableFilters";
19
20
  import { useDataTableState } from "./useDataTableState";
20
- import { DataTableFilterState, DataTableSortState, flattenColumns, getActionsWidth } from "./dataTable.utils";
21
+ import { DataTableFilterState, DataTableSortState, flattenColumns, getActionsWidth, isMenuAction } from "./dataTable.utils";
21
22
  export type {
22
23
  DataTableDateRangeFilterValue, DataTableFilterState, DataTableSortDirection,
23
24
  DataTableSortState, FilterValue,
24
25
  } from "./dataTable.utils";
25
- const DEFAULT_FILTER_COLUMN_WIDTH = 100;
26
26
 
27
27
  export type DataTableProps<T> = {
28
28
  id: string;
@@ -93,9 +93,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
93
93
  const tableState = useDataTableState(id, groupedColumns, sourceData, onFiltersChange, onSortChange);
94
94
  const { flatColumns: columns, sortedData: data } = tableState;
95
95
  const actionsColumnWidth = getActionsWidth(actions, explicitActionsWidth);
96
- const filterRowRef = useRef<HTMLTableRowElement>(null);
96
+ const inlineActions = useMemo(() => actions.filter((action) => !isMenuAction(action)), [actions]);
97
+ const menuActions = useMemo(() => actions.filter(isMenuAction), [actions]);
97
98
  const groupRowRef = useRef<HTMLTableRowElement>(null);
98
- const [filterHeight, setFilterHeight] = useState(0);
99
99
  const [groupHeight, setGroupHeight] = useState(0);
100
100
  const hasGroups = groupedColumns.some((column) => column.children?.length);
101
101
  const stickyOffsets = useMemo(() => {
@@ -112,7 +112,6 @@ export function DataTable<T>(props: DataTableProps<T>) {
112
112
 
113
113
  useLayoutEffect(() => {
114
114
  const measure = () => {
115
- setFilterHeight(filterRowRef.current?.getBoundingClientRect().height ?? 0);
116
115
  setGroupHeight(groupRowRef.current?.getBoundingClientRect().height ?? 0);
117
116
  };
118
117
  measure();
@@ -121,10 +120,9 @@ export function DataTable<T>(props: DataTableProps<T>) {
121
120
  return () => window.removeEventListener("resize", measure);
122
121
  }
123
122
  const observer = new ResizeObserver(measure);
124
- if (filterRowRef.current) observer.observe(filterRowRef.current);
125
123
  if (groupRowRef.current) observer.observe(groupRowRef.current);
126
124
  return () => observer.disconnect();
127
- }, [tableState.visibleFilters.size, hasGroups, groupedColumns]);
125
+ }, [hasGroups, groupedColumns]);
128
126
 
129
127
  const isMobile = useIsMobile();
130
128
  const showMobileCardView = mobileCardView && isMobile;
@@ -531,8 +529,8 @@ export function DataTable<T>(props: DataTableProps<T>) {
531
529
  isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
532
530
  onToggleFilter={tableState.toggleFilter} onToggleSort={tableState.toggleSort}
533
531
  sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
532
+ filterControl={tableState.renderFilter(column)}
534
533
  /></tr></thead></table>
535
- {tableState.renderFilter(column)}
536
534
  </div>
537
535
  ))}
538
536
  </div>
@@ -609,7 +607,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
609
607
  ))}
610
608
  {actions.length > 0 && (
611
609
  <div className="data-table-mobile-card__actions card-actions">
612
- {actions.map((action, actionIndex) => (
610
+ {inlineActions.map((action, actionIndex) => (
613
611
  <DataTableAction
614
612
  key={action.id}
615
613
  action={action}
@@ -619,6 +617,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
619
617
  onFocusNextRow={focusNextRow}
620
618
  />
621
619
  ))}
620
+ <DataTableActionMenu
621
+ actions={menuActions}
622
+ row={row}
623
+ rowIndex={rowIndex}
624
+ onFocusNextRow={focusNextRow}
625
+ />
622
626
  </div>
623
627
  )}
624
628
  </div>
@@ -643,29 +647,17 @@ export function DataTable<T>(props: DataTableProps<T>) {
643
647
  {actions.length > 0 && <col style={{ width: actionsColumnWidth }} />}
644
648
  </colgroup>
645
649
  <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
650
  {hasGroups && <tr ref={groupRowRef} className="data-table-group-row">
659
651
  {onSelectionChange && <th />}
660
652
  {groupedColumns.map((column) => <th key={column.id} colSpan={flattenColumns([column]).length}
661
- style={{ top: filterHeight }}>{column.children?.length ? column.label : ""}</th>)}
653
+ style={{ top: 0 }}>{column.children?.length ? column.label : ""}</th>)}
662
654
  {actions.length > 0 && <th />}
663
655
  </tr>}
664
656
  <tr>
665
657
  {onSelectionChange && (
666
658
  <th
667
659
  className={`select-cell ${hideToggleAll ? "hidden" : ""}`}
668
- style={{ width: "40px", textAlign: "center", top: filterHeight + groupHeight,
660
+ style={{ width: "40px", textAlign: "center", top: groupHeight,
669
661
  ...(stickyOffsets.size > 0 ? { left: 0, zIndex: 12 } : {}) }}
670
662
  >
671
663
  <Input
@@ -686,11 +678,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
686
678
  idx={idx}
687
679
  onColumnClick={onColumnClick}
688
680
  stickyLeft={stickyOffsets.get(column.id)}
689
- stickyTop={filterHeight + groupHeight}
681
+ stickyTop={groupHeight}
690
682
  isFilterVisible={tableState.visibleFilters.has(column.name ?? column.id)}
691
683
  onToggleFilter={tableState.toggleFilter}
692
684
  onToggleSort={tableState.toggleSort}
693
685
  sortDirection={tableState.sort?.key === (column.name ?? column.id) ? tableState.sort.direction : null}
686
+ filterControl={tableState.renderFilter(column)}
694
687
  />
695
688
  ))}
696
689
  {actions.length > 0 && (
@@ -782,7 +775,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
782
775
  onClick={(e) => e.stopPropagation()}
783
776
  >
784
777
  <div className="d-flex align-items-center justify-content-end">
785
- {actions.map((action, index) => (
778
+ {inlineActions.map((action, index) => (
786
779
  <DataTableAction
787
780
  key={index}
788
781
  action={action}
@@ -792,6 +785,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
792
785
  onFocusNextRow={focusNextRow}
793
786
  />
794
787
  ))}
788
+ <DataTableActionMenu
789
+ actions={menuActions}
790
+ row={row}
791
+ rowIndex={rowIndex}
792
+ onFocusNextRow={focusNextRow}
793
+ />
795
794
  </div>
796
795
  </td>
797
796
  )}
@@ -64,10 +64,23 @@ export const DataTableView = () => {
64
64
  console.log("Edit action clicked for row:", row);
65
65
  },
66
66
  },
67
+ {
68
+ id: "duplicate",
69
+ label: "Dupliquer",
70
+ type: "button",
71
+ classIcon: "bi-copy",
72
+ display: "menu",
73
+ onClick: (row: any) => {
74
+ console.log("Duplicate action clicked for row:", row);
75
+ },
76
+ },
67
77
  {
68
78
  id: "delete",
69
79
  label: "Delete",
70
80
  type: "button",
81
+ classIcon: "bi-trash",
82
+ display: "menu",
83
+ shouldDisplay: (row) => row.id !== 1,
71
84
  onClick: (row: any) => {
72
85
  console.log("Delete action clicked for row:", row);
73
86
  },
@@ -134,7 +134,12 @@
134
134
  background-color: #fff;
135
135
  color: var(--tertiary-color);
136
136
  min-width: 100px;
137
- padding: .5rem;
137
+ padding: .5rem 0;
138
+
139
+ &:first-child,
140
+ &:last-child {
141
+ padding: .5rem;
142
+ }
138
143
 
139
144
  .data-table-column-filter,
140
145
  .data-table-column-sort {
@@ -152,6 +157,7 @@
152
157
 
153
158
  &:hover,
154
159
  &:focus-within {
160
+
155
161
  .data-table-column-filter,
156
162
  .data-table-column-sort {
157
163
  visibility: visible;
@@ -302,32 +308,74 @@
302
308
  }
303
309
  }
304
310
  }
311
+
305
312
  .data-table-action-icon {
306
313
  cursor: pointer;
307
314
  display: flex;
308
315
  align-items: center;
309
316
  }
317
+
318
+ .data-table-action-menu-toggle {
319
+ cursor: pointer;
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ width: 32px;
324
+ height: 32px;
325
+ border-radius: 4px;
326
+
327
+ &:hover,
328
+ &[aria-expanded="true"] {
329
+ background: rgba(0, 0, 0, 0.06);
330
+ }
331
+ }
332
+
333
+ .data-table-action-menu {
334
+ min-width: 150px;
335
+ max-width: 300px;
336
+ padding: 0.25rem;
337
+
338
+ button {
339
+ font-size: 0.85rem;
340
+ width: 100%;
341
+ padding: 1rem 0rem !important;
342
+ }
343
+ }
344
+
310
345
  .data-table-column-heading {
311
346
  display: flex;
312
347
  align-items: center;
313
348
  gap: 4px;
314
349
 
315
- > .flex-1 {
350
+ >.flex-1 {
316
351
  flex: 1;
317
352
  }
318
353
  }
354
+
355
+ .data-table-filter-popover {
356
+ z-index: 1200;
357
+ padding: 8px;
358
+ box-sizing: border-box;
359
+ border: 1px solid #dce3e7;
360
+ border-radius: 7px;
361
+ background: #fff;
362
+ box-shadow: 0 6px 18px rgba(20, 40, 50, 0.16);
363
+ }
364
+
319
365
  .data-table-group-row th {
320
366
  height: 40px;
321
367
  box-sizing: border-box;
322
368
  }
369
+
323
370
  .data-table-action-link {
324
371
  min-width: 0;
325
372
  background: transparent;
326
373
  color: var(--primary-color);
327
374
  text-decoration: underline;
328
375
  }
376
+
329
377
  .data-table-mobile-controls {
330
378
  display: grid;
331
379
  gap: 12px;
332
380
  margin-bottom: 16px;
333
- }
381
+ }
@@ -63,4 +63,6 @@ export interface dataTableActionsInterface<T> {
63
63
  width?: string | number;
64
64
  minWidth?: string | number;
65
65
  focusNextRow?: boolean;
66
+ /** "inline" (défaut) : affichée dans la ligne ; "menu" : regroupée dans le menu « ⋮ » en fin de ligne */
67
+ display?: "inline" | "menu";
66
68
  }
@@ -19,10 +19,23 @@ export function getFilterKey<T>(column: dataTableColumnsInterface<T>) {
19
19
  return column.name ?? column.id;
20
20
  }
21
21
 
22
+ export const ACTION_MENU_WIDTH = 40;
23
+
24
+ export function isMenuAction<T>(action: dataTableActionsInterface<T>) {
25
+ return action.display === "menu";
26
+ }
27
+
28
+ export function isActionDisplayed<T>(action: dataTableActionsInterface<T>, row: T) {
29
+ return action.visible !== false && (action.shouldDisplay ? action.shouldDisplay(row) : true);
30
+ }
31
+
22
32
  export function getActionsWidth<T>(actions: dataTableActionsInterface<T>[], explicitWidth?: number) {
23
33
  if (explicitWidth !== undefined) return explicitWidth;
24
- if (!actions.some((action) => typeof action.width === "number")) return 200;
25
- return actions.reduce((total, action) => total + (typeof action.width === "number" ? action.width : 0), 0);
34
+ const inlineActions = actions.filter((action) => !isMenuAction(action));
35
+ const menuWidth = inlineActions.length < actions.length ? ACTION_MENU_WIDTH : 0;
36
+ if (inlineActions.length === 0 && menuWidth > 0) return menuWidth;
37
+ if (!inlineActions.some((action) => typeof action.width === "number")) return 200;
38
+ return inlineActions.reduce((total, action) => total + (typeof action.width === "number" ? action.width : 0), menuWidth);
26
39
  }
27
40
 
28
41
  export function sortRows<T>(data: T[], columns: dataTableColumnsInterface<T>[], sort: DataTableSortState | null): T[] {
@@ -1,6 +1,7 @@
1
1
  import { Button } from "../../button/Button";
2
2
  import { Icon } from "../../icon/Icon";
3
3
  import { dataTableActionsInterface } from "../dataTable.interface";
4
+ import { isActionDisplayed } from "../dataTable.utils";
4
5
  type DataTableActionProps = {
5
6
  action: dataTableActionsInterface<any>;
6
7
  row: any;
@@ -16,7 +17,7 @@ export const DataTableAction = ({
16
17
  onFocusNextRow,
17
18
  }: DataTableActionProps) => {
18
19
  let title = action.label;
19
- const display = action.visible !== false && (action.shouldDisplay ? action.shouldDisplay(row) : true);
20
+ const display = isActionDisplayed(action, row);
20
21
  if (!display) return null;
21
22
  if (action.labelButton) {
22
23
  title = action.labelButton ? action.labelButton(row) : action.label;
@@ -0,0 +1,95 @@
1
+ import { useMemo, useRef, useState } from "react";
2
+ import { Button } from "../../button/Button";
3
+ import { Icon } from "../../icon/Icon";
4
+ import { IconSizeEnum } from "../../icon/iconSize.enum";
5
+ import { MenuInterface } from "../../menus/menu/menu.interface";
6
+ import { Menus } from "../../menus/Menus";
7
+ import { OverlayPanel } from "../../overlayPanel/OverlayPanel";
8
+ import { dataTableActionsInterface } from "../dataTable.interface";
9
+ import { isActionDisplayed } from "../dataTable.utils";
10
+
11
+ type DataTableActionMenuProps = {
12
+ actions: dataTableActionsInterface<any>[];
13
+ row: any;
14
+ rowIndex?: number;
15
+ onFocusNextRow?: (rowIndex: number) => void;
16
+ };
17
+
18
+ export const DataTableActionMenu = ({
19
+ actions,
20
+ row,
21
+ rowIndex,
22
+ onFocusNextRow,
23
+ }: DataTableActionMenuProps) => {
24
+ const refButton = useRef<HTMLDivElement>(null);
25
+ const [open, setOpen] = useState(false);
26
+ const visibleActions = actions.filter((action) => isActionDisplayed(action, row));
27
+
28
+ const items: MenuInterface[] = useMemo(
29
+ () =>
30
+ visibleActions.map((action, index) => ({
31
+ id: index,
32
+ icon: action.classIcon,
33
+ name: action.label,
34
+ disabled: action.disabled,
35
+ children: (
36
+ <Button
37
+ key={action.id}
38
+ classIcon={action.classIcon}
39
+ sizeIcon={IconSizeEnum.LARGE}
40
+ disabled={action.disabled}
41
+ title={action.labelButton ? action.labelButton(row) : action.label}
42
+ onClick={(e) => {
43
+ e.stopPropagation();
44
+ if (action.disabled) return;
45
+ setOpen(false);
46
+ action.onClick(row, rowIndex);
47
+ if (action.focusNextRow && rowIndex !== undefined) {
48
+ onFocusNextRow?.(rowIndex);
49
+ }
50
+ }}
51
+ />
52
+ ),
53
+ })),
54
+ [visibleActions, row, rowIndex, onFocusNextRow],
55
+ );
56
+
57
+ if (visibleActions.length === 0) return null;
58
+
59
+ return (
60
+ <>
61
+ <div
62
+ ref={refButton}
63
+ className="data-table-action-menu-toggle"
64
+ title="Actions"
65
+ role="button"
66
+ tabIndex={0}
67
+ aria-haspopup="menu"
68
+ aria-expanded={open}
69
+ onClick={(e) => {
70
+ e.stopPropagation();
71
+ setOpen(!open);
72
+ }}
73
+ onKeyDown={(e) => {
74
+ if (e.key === "Enter" || e.key === " ") {
75
+ e.preventDefault();
76
+ e.stopPropagation();
77
+ setOpen(!open);
78
+ }
79
+ }}
80
+ >
81
+ <Icon size={IconSizeEnum.MEDIUM} classIcon="bi-three-dots-vertical" />
82
+ </div>
83
+ {open && refButton.current && (
84
+ <OverlayPanel
85
+ show={open}
86
+ position="bottomRight"
87
+ referenceElement={refButton.current}
88
+ closeOverlay={() => setOpen(false)}
89
+ >
90
+ <Menus className="data-table-action-menu" items={items} />
91
+ </OverlayPanel>
92
+ )}
93
+ </>
94
+ );
95
+ };