@navikt/ds-react 8.10.0 → 8.10.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/cjs/data/table/column-header/DataTableColumnHeader.d.ts +1 -2
- package/cjs/data/table/column-header/DataTableColumnHeader.js +13 -11
- package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
- package/cjs/data/table/column-header/useTableColumnResize.d.ts +5 -3
- package/cjs/data/table/column-header/useTableColumnResize.js +128 -53
- package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
- package/cjs/data/table/helpers/collectTableRowEntries.d.ts +16 -0
- package/cjs/data/table/helpers/collectTableRowEntries.js +27 -0
- package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -0
- package/cjs/data/table/helpers/table-keyboard.js +3 -3
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/hooks/useTableExpansion.d.ts +9 -6
- package/cjs/data/table/hooks/useTableExpansion.js +36 -15
- package/cjs/data/table/hooks/useTableExpansion.js.map +1 -1
- package/cjs/data/table/hooks/useTableItems.d.ts +29 -0
- package/cjs/data/table/hooks/useTableItems.js +63 -0
- package/cjs/data/table/hooks/useTableItems.js.map +1 -0
- package/cjs/data/table/hooks/useTableKeyboardNav.js +3 -3
- package/cjs/data/table/hooks/useTableKeyboardNav.js.map +1 -1
- package/cjs/data/table/root/DataTableAuto.d.ts +18 -0
- package/cjs/data/table/root/DataTableAuto.js +71 -29
- package/cjs/data/table/root/DataTableAuto.js.map +1 -1
- package/cjs/data/table/root/DataTableRoot.context.d.ts +5 -3
- package/cjs/data/table/root/DataTableRoot.context.js.map +1 -1
- package/cjs/data/table/root/DataTableRoot.js +7 -4
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/data/table/tr/DataTableTr.js +30 -32
- package/cjs/data/table/tr/DataTableTr.js.map +1 -1
- package/cjs/modal/Modal.js +3 -3
- package/cjs/modal/Modal.js.map +1 -1
- package/cjs/modal/types.d.ts +1 -0
- package/esm/data/table/column-header/DataTableColumnHeader.d.ts +1 -2
- package/esm/data/table/column-header/DataTableColumnHeader.js +14 -12
- package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
- package/esm/data/table/column-header/useTableColumnResize.d.ts +5 -3
- package/esm/data/table/column-header/useTableColumnResize.js +129 -54
- package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
- package/esm/data/table/helpers/collectTableRowEntries.d.ts +16 -0
- package/esm/data/table/helpers/collectTableRowEntries.js +25 -0
- package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -0
- package/esm/data/table/helpers/table-keyboard.js +3 -3
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/hooks/useTableExpansion.d.ts +9 -6
- package/esm/data/table/hooks/useTableExpansion.js +36 -16
- package/esm/data/table/hooks/useTableExpansion.js.map +1 -1
- package/esm/data/table/hooks/useTableItems.d.ts +29 -0
- package/esm/data/table/hooks/useTableItems.js +58 -0
- package/esm/data/table/hooks/useTableItems.js.map +1 -0
- package/esm/data/table/hooks/useTableKeyboardNav.js +3 -3
- package/esm/data/table/hooks/useTableKeyboardNav.js.map +1 -1
- package/esm/data/table/root/DataTableAuto.d.ts +18 -0
- package/esm/data/table/root/DataTableAuto.js +72 -30
- package/esm/data/table/root/DataTableAuto.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.context.d.ts +5 -3
- package/esm/data/table/root/DataTableRoot.context.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.js +7 -4
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/data/table/tr/DataTableTr.js +32 -34
- package/esm/data/table/tr/DataTableTr.js.map +1 -1
- package/esm/modal/Modal.js +3 -3
- package/esm/modal/Modal.js.map +1 -1
- package/esm/modal/types.d.ts +1 -0
- package/package.json +7 -7
- package/src/data/table/column-header/DataTableColumnHeader.tsx +22 -14
- package/src/data/table/column-header/useTableColumnResize.ts +152 -79
- package/src/data/table/helpers/collectTableRowEntries.ts +58 -0
- package/src/data/table/helpers/table-keyboard.ts +4 -4
- package/src/data/table/hooks/__tests__/useTableExpansion.test.tsx +115 -0
- package/src/data/table/hooks/__tests__/useTableItems.test.ts +131 -0
- package/src/data/table/hooks/useTableExpansion.tsx +63 -22
- package/src/data/table/hooks/useTableItems.ts +123 -0
- package/src/data/table/hooks/useTableKeyboardNav.ts +3 -3
- package/src/data/table/root/DataTableAuto.test.tsx +118 -0
- package/src/data/table/root/DataTableAuto.tsx +159 -49
- package/src/data/table/root/DataTableRoot.context.ts +4 -2
- package/src/data/table/root/DataTableRoot.tsx +20 -13
- package/src/data/table/tr/DataTableTr.tsx +48 -47
- package/src/modal/Modal.tsx +12 -6
- package/src/modal/types.ts +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type TableRowEntryId = string | number;
|
|
2
|
+
|
|
3
|
+
type CollectTableRowEntriesArgs<T> = {
|
|
4
|
+
items: T[];
|
|
5
|
+
getRowId?: (rowData: T, index: number) => TableRowEntryId;
|
|
6
|
+
getSubRows?: (rowData: T) => T[];
|
|
7
|
+
isSubRowExpandable?: (rowData: T) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface ItemDetail<T> {
|
|
11
|
+
id: string | number;
|
|
12
|
+
level: number;
|
|
13
|
+
parent: null | T;
|
|
14
|
+
children: readonly T[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function collectTableRowEntries<T>({
|
|
18
|
+
items,
|
|
19
|
+
getRowId,
|
|
20
|
+
getSubRows,
|
|
21
|
+
isSubRowExpandable,
|
|
22
|
+
}: CollectTableRowEntriesArgs<T>): Map<T, ItemDetail<T>> {
|
|
23
|
+
const itemDetailsMap = new Map<T, ItemDetail<T>>();
|
|
24
|
+
|
|
25
|
+
const traverseRows = (
|
|
26
|
+
rows: T[],
|
|
27
|
+
level: number,
|
|
28
|
+
parent: T | null,
|
|
29
|
+
parentId?: TableRowEntryId,
|
|
30
|
+
) => {
|
|
31
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
32
|
+
const rowData = rows[rowIndex];
|
|
33
|
+
const rowId =
|
|
34
|
+
getRowId?.(rowData, rowIndex) ??
|
|
35
|
+
(parentId == null ? rowIndex : `${parentId}-${rowIndex}`);
|
|
36
|
+
const isRowExpandable = isSubRowExpandable?.(rowData) ?? true;
|
|
37
|
+
const children = (isRowExpandable ? getSubRows?.(rowData) : []) ?? [];
|
|
38
|
+
|
|
39
|
+
itemDetailsMap.set(rowData, {
|
|
40
|
+
id: rowId,
|
|
41
|
+
level,
|
|
42
|
+
parent,
|
|
43
|
+
children,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (children.length > 0) {
|
|
47
|
+
traverseRows(children, level + 1, rowData, rowId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
traverseRows(items, 0, null);
|
|
53
|
+
|
|
54
|
+
return itemDetailsMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { collectTableRowEntries };
|
|
58
|
+
export type { CollectTableRowEntriesArgs, TableRowEntryId, ItemDetail };
|
|
@@ -55,15 +55,15 @@ function getNavigationAction(event: KeyboardEvent): NavigationAction | null {
|
|
|
55
55
|
*/
|
|
56
56
|
function shouldBlockNavigation(event: KeyboardEvent): boolean {
|
|
57
57
|
const key = event.key;
|
|
58
|
-
if (!(key in keyToCoord)) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
58
|
const el = event.target as HTMLElement | null;
|
|
63
59
|
if (!el) {
|
|
64
60
|
return false;
|
|
65
61
|
}
|
|
66
62
|
|
|
63
|
+
if (el.dataset.disableKeyboardNav === "true") {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
67
|
if (el.isContentEditable) {
|
|
68
68
|
return true;
|
|
69
69
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
DataTableExpansionProvider,
|
|
6
|
+
useDataTableExpansion,
|
|
7
|
+
} from "../useTableExpansion";
|
|
8
|
+
import { useTableItems } from "../useTableItems";
|
|
9
|
+
|
|
10
|
+
type TestRow = {
|
|
11
|
+
id: number;
|
|
12
|
+
subRows?: TestRow[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function createWrapper(
|
|
16
|
+
options: {
|
|
17
|
+
onDetailsPanelChange?: (ids: (string | number)[]) => void;
|
|
18
|
+
isDetailsPanelExpandable?: (row: TestRow) => boolean;
|
|
19
|
+
} = {},
|
|
20
|
+
) {
|
|
21
|
+
const rows: TestRow[] = [{ id: 1, subRows: [{ id: 10 }] }, { id: 2 }];
|
|
22
|
+
|
|
23
|
+
return function Wrapper({ children }: { children: React.ReactNode }) {
|
|
24
|
+
const tableItems = useTableItems({
|
|
25
|
+
items: rows,
|
|
26
|
+
getRowId: (row) => row.id,
|
|
27
|
+
getSubRows: (row) => row.subRows ?? [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DataTableExpansionProvider<TestRow>
|
|
32
|
+
itemDetails={tableItems.itemDetails}
|
|
33
|
+
getDetailsPanelContent={(row) => row.id}
|
|
34
|
+
isDetailsPanelExpandable={options.isDetailsPanelExpandable}
|
|
35
|
+
onDetailsPanelChange={options.onDetailsPanelChange}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</DataTableExpansionProvider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("useTableExpansion", () => {
|
|
44
|
+
test("does not allow toggling rows that are not expandable", () => {
|
|
45
|
+
const onDetailsPanelChange = vi.fn();
|
|
46
|
+
|
|
47
|
+
const { result } = renderHook(() => useDataTableExpansion(), {
|
|
48
|
+
wrapper: createWrapper({
|
|
49
|
+
onDetailsPanelChange,
|
|
50
|
+
isDetailsPanelExpandable: (row) => row.id === 1,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.current.isDetailsPanelExpandable(1)).toBe(true);
|
|
55
|
+
expect(result.current.isDetailsPanelExpandable(2)).toBe(false);
|
|
56
|
+
expect(result.current.isDetailsPanelExpandable(10)).toBe(false);
|
|
57
|
+
|
|
58
|
+
act(() => {
|
|
59
|
+
result.current.toggleExpansion(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.current.isExpanded(2)).toBe(false);
|
|
63
|
+
expect(onDetailsPanelChange).not.toHaveBeenCalled();
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
result.current.toggleExpansion(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.current.isExpanded(1)).toBe(true);
|
|
70
|
+
expect(onDetailsPanelChange).toHaveBeenCalledWith([1]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("expand all only expands expandable rows", () => {
|
|
74
|
+
const onDetailsPanelChange = vi.fn();
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useDataTableExpansion(), {
|
|
77
|
+
wrapper: createWrapper({
|
|
78
|
+
onDetailsPanelChange,
|
|
79
|
+
isDetailsPanelExpandable: (row) => row.id === 1,
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
act(() => {
|
|
84
|
+
result.current.toggleAll();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.current.isExpanded(1)).toBe(true);
|
|
88
|
+
expect(result.current.isExpanded(2)).toBe(false);
|
|
89
|
+
expect(result.current.isAllExpanded).toBe(true);
|
|
90
|
+
expect(onDetailsPanelChange).toHaveBeenCalledWith([1]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("expand all only targets top-level table items", () => {
|
|
94
|
+
const onDetailsPanelChange = vi.fn();
|
|
95
|
+
|
|
96
|
+
const { result } = renderHook(() => useDataTableExpansion(), {
|
|
97
|
+
wrapper: createWrapper({
|
|
98
|
+
onDetailsPanelChange,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.current.isDetailsPanelExpandable(1)).toBe(true);
|
|
103
|
+
expect(result.current.isDetailsPanelExpandable(2)).toBe(true);
|
|
104
|
+
expect(result.current.isDetailsPanelExpandable(10)).toBe(false);
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
result.current.toggleAll();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result.current.isExpanded(1)).toBe(true);
|
|
111
|
+
expect(result.current.isExpanded(2)).toBe(true);
|
|
112
|
+
expect(result.current.isExpanded(10)).toBe(false);
|
|
113
|
+
expect(onDetailsPanelChange).toHaveBeenCalledWith([1, 2]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { useTableItems } from "../useTableItems";
|
|
4
|
+
|
|
5
|
+
type TestRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
subRows?: TestRow[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FallbackTestRow = {
|
|
12
|
+
label: string;
|
|
13
|
+
subRows?: FallbackTestRow[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const plainRows: TestRow[] = [
|
|
17
|
+
{ id: "a", name: "Alpha" },
|
|
18
|
+
{ id: "b", name: "Beta" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const nestedRows: TestRow[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "a",
|
|
24
|
+
name: "Alpha",
|
|
25
|
+
subRows: [
|
|
26
|
+
{ id: "a1", name: "Alpha child 1" },
|
|
27
|
+
{
|
|
28
|
+
id: "a2",
|
|
29
|
+
name: "Alpha child 2",
|
|
30
|
+
subRows: [{ id: "a2a", name: "Alpha grandchild" }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "b",
|
|
36
|
+
name: "Beta",
|
|
37
|
+
subRows: [{ id: "b1", name: "Beta child 1" }],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const fallbackRows: FallbackTestRow[] = [
|
|
42
|
+
{
|
|
43
|
+
label: "Parent",
|
|
44
|
+
subRows: [{ label: "Child" }],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const getSubRows = (row: TestRow) => row.subRows ?? [];
|
|
49
|
+
|
|
50
|
+
const getVisibleIds = (rows: TestRow[]) => rows.map((row) => row.id);
|
|
51
|
+
|
|
52
|
+
describe("useTableItems", () => {
|
|
53
|
+
test("builds row details for plain rows without nesting", () => {
|
|
54
|
+
const { result } = renderHook(() =>
|
|
55
|
+
useTableItems({
|
|
56
|
+
items: plainRows,
|
|
57
|
+
getRowId: (row) => row.id,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(getVisibleIds(result.current.items)).toEqual(["a", "b"]);
|
|
62
|
+
expect(result.current.itemDetails.get(plainRows[0])).toMatchObject({
|
|
63
|
+
id: "a",
|
|
64
|
+
level: 0,
|
|
65
|
+
parent: null,
|
|
66
|
+
children: [],
|
|
67
|
+
});
|
|
68
|
+
expect(result.current.itemDetails.get(plainRows[1])).toMatchObject({
|
|
69
|
+
id: "b",
|
|
70
|
+
level: 0,
|
|
71
|
+
parent: null,
|
|
72
|
+
children: [],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("shows direct child rows when a parent row is expanded", () => {
|
|
77
|
+
const { result } = renderHook(() =>
|
|
78
|
+
useTableItems({
|
|
79
|
+
items: nestedRows,
|
|
80
|
+
getRowId: (row) => row.id,
|
|
81
|
+
getSubRows,
|
|
82
|
+
defaultExpandedSubRowIds: ["a"],
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(getVisibleIds(result.current.items)).toEqual(["a", "a1", "a2", "b"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("uses the same fallback root id to reveal child rows when getRowId is omitted", () => {
|
|
90
|
+
const { result } = renderHook(() =>
|
|
91
|
+
useTableItems({
|
|
92
|
+
items: fallbackRows,
|
|
93
|
+
getSubRows: (row) => row.subRows ?? [],
|
|
94
|
+
defaultExpandedSubRowIds: [0],
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.current.items.map((row) => row.label)).toEqual([
|
|
99
|
+
"Parent",
|
|
100
|
+
"Child",
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("updates visible rows in depth-first order for controlled expanded ids", () => {
|
|
105
|
+
const { result, rerender } = renderHook(
|
|
106
|
+
({ expandedIds }) =>
|
|
107
|
+
useTableItems({
|
|
108
|
+
items: nestedRows,
|
|
109
|
+
getRowId: (row) => row.id,
|
|
110
|
+
getSubRows,
|
|
111
|
+
expandedSubRowIds: expandedIds,
|
|
112
|
+
}),
|
|
113
|
+
{
|
|
114
|
+
initialProps: { expandedIds: [] as (string | number)[] },
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(getVisibleIds(result.current.items)).toEqual(["a", "b"]);
|
|
119
|
+
|
|
120
|
+
rerender({ expandedIds: ["a", "a2", "b"] });
|
|
121
|
+
|
|
122
|
+
expect(getVisibleIds(result.current.items)).toEqual([
|
|
123
|
+
"a",
|
|
124
|
+
"a1",
|
|
125
|
+
"a2",
|
|
126
|
+
"a2a",
|
|
127
|
+
"b",
|
|
128
|
+
"b1",
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React, { useCallback } from "react";
|
|
2
2
|
import { createStrictContext } from "../../../utils/helpers";
|
|
3
3
|
import { useControllableState } from "../../../utils/hooks";
|
|
4
|
+
import type { ItemDetail } from "./useTableItems";
|
|
4
5
|
|
|
5
6
|
type DataTableExpansionContextT = {
|
|
6
|
-
expandedIds: (string | number)[];
|
|
7
7
|
isExpanded: (id: string | number) => boolean;
|
|
8
|
+
isDetailsPanelExpandable: (id: string | number) => boolean;
|
|
8
9
|
toggleExpansion: (id: string | number) => void;
|
|
9
10
|
toggleAll: () => void;
|
|
10
11
|
isAllExpanded: boolean;
|
|
11
12
|
getDetailsPanelContent?: (row: unknown) => React.ReactNode;
|
|
12
13
|
getDetailsPanelHeight?: (row: unknown) => number | "auto";
|
|
13
|
-
showExpandAll
|
|
14
|
-
|
|
14
|
+
showExpandAll: boolean;
|
|
15
|
+
enableDetailsPanel: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const {
|
|
@@ -27,57 +28,93 @@ type TableExpansionOptions<T> = {
|
|
|
27
28
|
detailsPanelRowIds?: (string | number)[];
|
|
28
29
|
defaultDetailsPanelRowIds?: (string | number)[];
|
|
29
30
|
onDetailsPanelChange?: (ids: (string | number)[]) => void;
|
|
30
|
-
|
|
31
|
+
itemDetails: Map<T, ItemDetail<T>>;
|
|
31
32
|
getDetailsPanelContent?: (row: T) => React.ReactNode;
|
|
33
|
+
isDetailsPanelExpandable?: (rowData: T) => boolean;
|
|
32
34
|
getDetailsPanelHeight?: (row: T) => number | "auto";
|
|
33
35
|
showExpandAll?: boolean;
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
function getDataTableExpansionId(tableId: string, rowId: string | number) {
|
|
39
|
+
return `${tableId}-expansion-${rowId}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
function DataTableExpansionProvider<T>({
|
|
37
43
|
children,
|
|
38
44
|
detailsPanelRowIds,
|
|
39
45
|
defaultDetailsPanelRowIds = [],
|
|
40
46
|
onDetailsPanelChange,
|
|
41
|
-
|
|
47
|
+
itemDetails,
|
|
42
48
|
getDetailsPanelContent,
|
|
49
|
+
isDetailsPanelExpandable,
|
|
43
50
|
getDetailsPanelHeight,
|
|
44
51
|
showExpandAll = false,
|
|
45
52
|
}: TableExpansionOptions<T> & { children: React.ReactNode }) {
|
|
46
53
|
const [expandedIds, setExpandedIds] = useControllableState({
|
|
47
54
|
value: detailsPanelRowIds,
|
|
48
55
|
defaultValue: defaultDetailsPanelRowIds,
|
|
56
|
+
onChange: onDetailsPanelChange,
|
|
49
57
|
});
|
|
50
58
|
|
|
59
|
+
const expandableIds = React.useMemo(() => {
|
|
60
|
+
if (!getDetailsPanelContent) {
|
|
61
|
+
return new Set<string | number>();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ids = new Set<string | number>();
|
|
65
|
+
|
|
66
|
+
for (const [rowData, { id, level }] of itemDetails.entries()) {
|
|
67
|
+
/* We only allow Master - Details pattern on top level rows */
|
|
68
|
+
if (level > 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isDetailsPanelExpandable || isDetailsPanelExpandable(rowData)) {
|
|
73
|
+
ids.add(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ids;
|
|
78
|
+
}, [getDetailsPanelContent, isDetailsPanelExpandable, itemDetails]);
|
|
79
|
+
|
|
80
|
+
const isDetailsPanelExpandableById = useCallback(
|
|
81
|
+
(id: string | number) => expandableIds.has(id),
|
|
82
|
+
[expandableIds],
|
|
83
|
+
);
|
|
84
|
+
|
|
51
85
|
const isExpanded = useCallback(
|
|
52
|
-
(id: string | number) =>
|
|
53
|
-
|
|
86
|
+
(id: string | number) =>
|
|
87
|
+
isDetailsPanelExpandableById(id) && expandedIds.includes(id),
|
|
88
|
+
[expandedIds, isDetailsPanelExpandableById],
|
|
54
89
|
);
|
|
55
90
|
|
|
56
91
|
const toggleExpansion = useCallback(
|
|
57
92
|
(id: string | number) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
if (!isDetailsPanelExpandableById(id)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setExpandedIds((currentExpandedIds) =>
|
|
98
|
+
currentExpandedIds.includes(id)
|
|
99
|
+
? currentExpandedIds.filter((expandedId) => expandedId !== id)
|
|
100
|
+
: [...currentExpandedIds, id],
|
|
101
|
+
);
|
|
63
102
|
},
|
|
64
|
-
[
|
|
103
|
+
[isDetailsPanelExpandableById, setExpandedIds],
|
|
65
104
|
);
|
|
66
105
|
|
|
67
106
|
const isAllExpanded =
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
expandableIds.size > 0 &&
|
|
108
|
+
Array.from(expandableIds).every((key) => expandedIds.includes(key));
|
|
70
109
|
|
|
71
110
|
const toggleAll = useCallback(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onDetailsPanelChange?.(next);
|
|
75
|
-
}, [isAllExpanded, allRowKeys, setExpandedIds, onDetailsPanelChange]);
|
|
111
|
+
setExpandedIds(isAllExpanded ? [] : Array.from(expandableIds));
|
|
112
|
+
}, [expandableIds, isAllExpanded, setExpandedIds]);
|
|
76
113
|
|
|
77
114
|
return (
|
|
78
115
|
<DataTableExpansionContextProvider
|
|
79
|
-
expandedIds={expandedIds}
|
|
80
116
|
isExpanded={isExpanded}
|
|
117
|
+
isDetailsPanelExpandable={isDetailsPanelExpandableById}
|
|
81
118
|
toggleExpansion={toggleExpansion}
|
|
82
119
|
toggleAll={toggleAll}
|
|
83
120
|
isAllExpanded={isAllExpanded}
|
|
@@ -90,11 +127,15 @@ function DataTableExpansionProvider<T>({
|
|
|
90
127
|
getDetailsPanelHeight as ((row: unknown) => number | "auto") | undefined
|
|
91
128
|
}
|
|
92
129
|
showExpandAll={showExpandAll}
|
|
93
|
-
|
|
130
|
+
enableDetailsPanel={!!getDetailsPanelContent}
|
|
94
131
|
>
|
|
95
132
|
{children}
|
|
96
133
|
</DataTableExpansionContextProvider>
|
|
97
134
|
);
|
|
98
135
|
}
|
|
99
136
|
|
|
100
|
-
export {
|
|
137
|
+
export {
|
|
138
|
+
DataTableExpansionProvider,
|
|
139
|
+
getDataTableExpansionId,
|
|
140
|
+
useDataTableExpansion,
|
|
141
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { createStrictContext } from "../../../utils/helpers";
|
|
3
|
+
import { useControllableState } from "../../../utils/hooks";
|
|
4
|
+
import {
|
|
5
|
+
type ItemDetail,
|
|
6
|
+
collectTableRowEntries,
|
|
7
|
+
} from "../helpers/collectTableRowEntries";
|
|
8
|
+
|
|
9
|
+
type UseTableItemsArgs<T> = {
|
|
10
|
+
items: T[];
|
|
11
|
+
getRowId?: (rowData: T, index: number) => string | number;
|
|
12
|
+
/**
|
|
13
|
+
* Master - Detail pattern props
|
|
14
|
+
*/
|
|
15
|
+
/* expandedDetailsPanelIds?: (string | number)[];
|
|
16
|
+
defaultExpandedDetailsPanelIds?: (string | number)[];
|
|
17
|
+
isDetailsPanelExpandable?: (rowData: T) => boolean;
|
|
18
|
+
onDetailsPanelChange?: (ids: (string | number)[]) => void;
|
|
19
|
+
|
|
20
|
+
getDetailsPanelHeight?: (row: T) => number | "auto";
|
|
21
|
+
getDetailsPanelContent?: (row: T) => React.ReactNode; */
|
|
22
|
+
/**
|
|
23
|
+
* Expanded/Nested rows pattern props
|
|
24
|
+
*/
|
|
25
|
+
getSubRows?: (rowData: T) => T[];
|
|
26
|
+
expandedSubRowIds?: (string | number)[];
|
|
27
|
+
defaultExpandedSubRowIds?: (string | number)[];
|
|
28
|
+
isSubRowExpandable?: (rowData: T) => boolean;
|
|
29
|
+
onExpandedSubRowIdsChange?: (ids: (string | number)[]) => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type useTableItemsReturn<T> = {
|
|
33
|
+
items: T[];
|
|
34
|
+
itemDetails: Map<T, ItemDetail<T>>;
|
|
35
|
+
onExpandedSubRowIdsChange: (id: string | number) => void;
|
|
36
|
+
isSubRowExpanded: (id: string | number) => boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function useTableItems<T>(args: UseTableItemsArgs<T>): useTableItemsReturn<T> {
|
|
40
|
+
const {
|
|
41
|
+
items,
|
|
42
|
+
expandedSubRowIds,
|
|
43
|
+
defaultExpandedSubRowIds,
|
|
44
|
+
getSubRows,
|
|
45
|
+
getRowId,
|
|
46
|
+
onExpandedSubRowIdsChange,
|
|
47
|
+
isSubRowExpandable,
|
|
48
|
+
} = args;
|
|
49
|
+
|
|
50
|
+
const [nestedSubRowsExpandedIds, setNestedSubRowsExpandedIds] =
|
|
51
|
+
useControllableState({
|
|
52
|
+
value: expandedSubRowIds,
|
|
53
|
+
defaultValue: defaultExpandedSubRowIds ?? [],
|
|
54
|
+
onChange: onExpandedSubRowIdsChange,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const expandedIdsSet = useMemo(
|
|
58
|
+
() => new Set(nestedSubRowsExpandedIds),
|
|
59
|
+
[nestedSubRowsExpandedIds],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const { itemDetails, visibleItems } = useMemo(() => {
|
|
63
|
+
const rowEntriesMap = collectTableRowEntries({
|
|
64
|
+
items,
|
|
65
|
+
getRowId,
|
|
66
|
+
getSubRows,
|
|
67
|
+
isSubRowExpandable,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const localVisibleItems: T[] = [];
|
|
71
|
+
const addVisibleRows = (rowData: T) => {
|
|
72
|
+
localVisibleItems.push(rowData);
|
|
73
|
+
|
|
74
|
+
const details = rowEntriesMap.get(rowData);
|
|
75
|
+
|
|
76
|
+
if (!details || !expandedIdsSet.has(details.id)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const childRow of details.children) {
|
|
81
|
+
addVisibleRows(childRow);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
for (const rowData of items) {
|
|
86
|
+
addVisibleRows(rowData);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
visibleItems: localVisibleItems,
|
|
91
|
+
itemDetails: rowEntriesMap,
|
|
92
|
+
};
|
|
93
|
+
}, [getSubRows, items, getRowId, isSubRowExpandable, expandedIdsSet]);
|
|
94
|
+
|
|
95
|
+
const handleExpandedSubRowIdChange = useCallback(
|
|
96
|
+
(id: string | number) => {
|
|
97
|
+
setNestedSubRowsExpandedIds((prev) =>
|
|
98
|
+
prev.includes(id)
|
|
99
|
+
? prev.filter((expandedId) => expandedId !== id)
|
|
100
|
+
: [...prev, id],
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
[setNestedSubRowsExpandedIds],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
items: visibleItems,
|
|
108
|
+
itemDetails,
|
|
109
|
+
onExpandedSubRowIdsChange: handleExpandedSubRowIdChange,
|
|
110
|
+
isSubRowExpanded: (id: string | number) => expandedIdsSet.has(id),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { Provider: TableItemsProvider, useContext: useTableItemsContext } =
|
|
115
|
+
/* TODO: Can we type this better? */
|
|
116
|
+
createStrictContext<useTableItemsReturn<any>>({
|
|
117
|
+
name: "TableItemsContext",
|
|
118
|
+
errorMessage:
|
|
119
|
+
"useTableItemsContext must be used within a TableItemsProvider",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export { useTableItems, TableItemsProvider, useTableItemsContext };
|
|
123
|
+
export type { ItemDetail };
|
|
@@ -114,12 +114,12 @@ function useTableKeyboardNav({
|
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
const action = getNavigationAction(event);
|
|
118
|
+
if (!action) {
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
if (!action) {
|
|
122
|
+
if (shouldBlockNavigation(event)) {
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
|