kelt-ui-kit-react 2.1.1 → 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.
- package/dist/button/buttonImport/ButtonImport.d.ts +13 -0
- package/dist/button/buttonImport/ButtonImport.view.d.ts +1 -0
- package/dist/dataTable/dataTable.interface.d.ts +2 -0
- package/dist/dataTable/dataTable.utils.d.ts +3 -0
- package/dist/dataTable/dataTableActions/DataTableActionMenu.d.ts +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2916 -2751
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/App.css +953 -161
- package/src/App.menu.tsx +8 -0
- package/src/button/buttonFile/ButtonFile.tsx +4 -1
- package/src/button/buttonImport/ButtonImport.tsx +99 -0
- package/src/button/buttonImport/ButtonImport.view.tsx +17 -0
- package/src/button/buttonImport/buttonImport.css +38 -0
- package/src/dataTable/DataTable.tsx +18 -3
- package/src/dataTable/DataTable.view.tsx +13 -0
- package/src/dataTable/dataTable.css +42 -3
- package/src/dataTable/dataTable.interface.ts +2 -0
- package/src/dataTable/dataTable.utils.ts +15 -2
- package/src/dataTable/dataTableActions/DataTableAction.tsx +2 -1
- package/src/dataTable/dataTableActions/DataTableActionMenu.tsx +95 -0
- package/src/docs/catalog.ts +1 -0
- package/src/index.ts +1 -0
- package/src/tabs/tabs.css +7 -1
- package/tests/dataTable.test.mjs +9 -0
- 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
|
-
|
|
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,12 +12,13 @@ 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,
|
|
@@ -92,6 +93,8 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
92
93
|
const tableState = useDataTableState(id, groupedColumns, sourceData, onFiltersChange, onSortChange);
|
|
93
94
|
const { flatColumns: columns, sortedData: data } = tableState;
|
|
94
95
|
const actionsColumnWidth = getActionsWidth(actions, explicitActionsWidth);
|
|
96
|
+
const inlineActions = useMemo(() => actions.filter((action) => !isMenuAction(action)), [actions]);
|
|
97
|
+
const menuActions = useMemo(() => actions.filter(isMenuAction), [actions]);
|
|
95
98
|
const groupRowRef = useRef<HTMLTableRowElement>(null);
|
|
96
99
|
const [groupHeight, setGroupHeight] = useState(0);
|
|
97
100
|
const hasGroups = groupedColumns.some((column) => column.children?.length);
|
|
@@ -604,7 +607,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
604
607
|
))}
|
|
605
608
|
{actions.length > 0 && (
|
|
606
609
|
<div className="data-table-mobile-card__actions card-actions">
|
|
607
|
-
{
|
|
610
|
+
{inlineActions.map((action, actionIndex) => (
|
|
608
611
|
<DataTableAction
|
|
609
612
|
key={action.id}
|
|
610
613
|
action={action}
|
|
@@ -614,6 +617,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
614
617
|
onFocusNextRow={focusNextRow}
|
|
615
618
|
/>
|
|
616
619
|
))}
|
|
620
|
+
<DataTableActionMenu
|
|
621
|
+
actions={menuActions}
|
|
622
|
+
row={row}
|
|
623
|
+
rowIndex={rowIndex}
|
|
624
|
+
onFocusNextRow={focusNextRow}
|
|
625
|
+
/>
|
|
617
626
|
</div>
|
|
618
627
|
)}
|
|
619
628
|
</div>
|
|
@@ -766,7 +775,7 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
766
775
|
onClick={(e) => e.stopPropagation()}
|
|
767
776
|
>
|
|
768
777
|
<div className="d-flex align-items-center justify-content-end">
|
|
769
|
-
{
|
|
778
|
+
{inlineActions.map((action, index) => (
|
|
770
779
|
<DataTableAction
|
|
771
780
|
key={index}
|
|
772
781
|
action={action}
|
|
@@ -776,6 +785,12 @@ export function DataTable<T>(props: DataTableProps<T>) {
|
|
|
776
785
|
onFocusNextRow={focusNextRow}
|
|
777
786
|
/>
|
|
778
787
|
))}
|
|
788
|
+
<DataTableActionMenu
|
|
789
|
+
actions={menuActions}
|
|
790
|
+
row={row}
|
|
791
|
+
rowIndex={rowIndex}
|
|
792
|
+
onFocusNextRow={focusNextRow}
|
|
793
|
+
/>
|
|
779
794
|
</div>
|
|
780
795
|
</td>
|
|
781
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,20 +308,50 @@
|
|
|
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
|
-
|
|
350
|
+
>.flex-1 {
|
|
316
351
|
flex: 1;
|
|
317
352
|
}
|
|
318
353
|
}
|
|
354
|
+
|
|
319
355
|
.data-table-filter-popover {
|
|
320
356
|
z-index: 1200;
|
|
321
357
|
padding: 8px;
|
|
@@ -325,18 +361,21 @@
|
|
|
325
361
|
background: #fff;
|
|
326
362
|
box-shadow: 0 6px 18px rgba(20, 40, 50, 0.16);
|
|
327
363
|
}
|
|
364
|
+
|
|
328
365
|
.data-table-group-row th {
|
|
329
366
|
height: 40px;
|
|
330
367
|
box-sizing: border-box;
|
|
331
368
|
}
|
|
369
|
+
|
|
332
370
|
.data-table-action-link {
|
|
333
371
|
min-width: 0;
|
|
334
372
|
background: transparent;
|
|
335
373
|
color: var(--primary-color);
|
|
336
374
|
text-decoration: underline;
|
|
337
375
|
}
|
|
376
|
+
|
|
338
377
|
.data-table-mobile-controls {
|
|
339
378
|
display: grid;
|
|
340
379
|
gap: 12px;
|
|
341
380
|
margin-bottom: 16px;
|
|
342
|
-
}
|
|
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
|
-
|
|
25
|
-
|
|
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 =
|
|
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
|
+
};
|
package/src/docs/catalog.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const catalog: Record<string, { category: Category; description: string;
|
|
|
5
5
|
"/buttons": { category: "Actions", icon: "cursor", description: "Des actions claires, avec icones, badges et menus contextuels." },
|
|
6
6
|
"/action": { category: "Actions", icon: "lightning", description: "Une action compacte et ses interactions." },
|
|
7
7
|
"/button-file": { category: "Actions", icon: "upload", description: "Selectionner un fichier depuis un bouton dedie." },
|
|
8
|
+
"/button-import": { category: "Actions", icon: "file-earmark-arrow-up", description: "Ouvrir une modal pour choisir un fichier puis valider l'import." },
|
|
8
9
|
"/form": { category: "Formulaires", icon: "ui-checks", description: "Composer des formulaires dynamiques, sections et champs responsives." },
|
|
9
10
|
"/address-search": { category: "Formulaires", icon: "geo-alt", description: "Rechercher une adresse et exploiter ses donnees geographiques." },
|
|
10
11
|
"/select": { category: "Formulaires", icon: "chevron-expand", description: "Choisir une valeur parmi des options." },
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { Badge } from "./badge/Badge";
|
|
|
26
26
|
export { Button } from "./button/Button";
|
|
27
27
|
export { ButtonActions } from "./button/buttonActions/ButtonActions";
|
|
28
28
|
export { ButtonFile } from "./button/buttonFile/ButtonFile";
|
|
29
|
+
export { ButtonImport } from "./button/buttonImport/ButtonImport";
|
|
29
30
|
export { ButtonFilters } from "./button/buttonFilters/ButtonFilters";
|
|
30
31
|
export { Card } from "./card/Card";
|
|
31
32
|
export { Carousel } from "./carousel/Carousel";
|
package/src/tabs/tabs.css
CHANGED
package/tests/dataTable.test.mjs
CHANGED
|
@@ -50,6 +50,15 @@ test("action widths preserve Hub sums and Kelt explicit column widths", () => {
|
|
|
50
50
|
assert.equal(utils.getActionsWidth([]), 200);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test("menu actions are grouped behind a single three-dots toggle", () => {
|
|
54
|
+
assert.equal(utils.getActionsWidth([{ width: 48 }, { width: 100, display: "menu" }]), 48 + utils.ACTION_MENU_WIDTH);
|
|
55
|
+
assert.equal(utils.getActionsWidth([{ display: "menu" }, { display: "menu" }]), utils.ACTION_MENU_WIDTH);
|
|
56
|
+
assert.equal(utils.getActionsWidth([{ type: "button" }, { display: "menu" }]), 200);
|
|
57
|
+
assert.equal(utils.isActionDisplayed({ visible: false }, {}), false);
|
|
58
|
+
assert.equal(utils.isActionDisplayed({ shouldDisplay: (row) => row.id === 2 }, { id: 1 }), false);
|
|
59
|
+
assert.equal(utils.isActionDisplayed({}, { id: 1 }), true);
|
|
60
|
+
});
|
|
61
|
+
|
|
53
62
|
test("sorting is stable, immutable, and supports Hub names and dates", () => {
|
|
54
63
|
const rows = [{ name: "Dossier 10", date: "2026-10-12" }, { name: "Dossier 2", date: "2026-01-02" }];
|
|
55
64
|
const columns = [{ id: "name", name: "dossier", type: "text" }, { id: "date", type: "date" }];
|
|
@@ -27,7 +27,7 @@ test("every registered demo has generated API documentation and its exact source
|
|
|
27
27
|
ts.forEachChild(node, visit);
|
|
28
28
|
}
|
|
29
29
|
visit(source);
|
|
30
|
-
assert.equal(routes.length,
|
|
30
|
+
assert.equal(routes.length, 31);
|
|
31
31
|
assert.deepEqual(Object.keys(docs).sort(), routes.sort());
|
|
32
32
|
for (const route of routes) {
|
|
33
33
|
assert.ok(docs[route].components.length > 0, route);
|