@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.
Files changed (79) hide show
  1. package/cjs/data/table/column-header/DataTableColumnHeader.d.ts +1 -2
  2. package/cjs/data/table/column-header/DataTableColumnHeader.js +13 -11
  3. package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  4. package/cjs/data/table/column-header/useTableColumnResize.d.ts +5 -3
  5. package/cjs/data/table/column-header/useTableColumnResize.js +128 -53
  6. package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
  7. package/cjs/data/table/helpers/collectTableRowEntries.d.ts +16 -0
  8. package/cjs/data/table/helpers/collectTableRowEntries.js +27 -0
  9. package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -0
  10. package/cjs/data/table/helpers/table-keyboard.js +3 -3
  11. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  12. package/cjs/data/table/hooks/useTableExpansion.d.ts +9 -6
  13. package/cjs/data/table/hooks/useTableExpansion.js +36 -15
  14. package/cjs/data/table/hooks/useTableExpansion.js.map +1 -1
  15. package/cjs/data/table/hooks/useTableItems.d.ts +29 -0
  16. package/cjs/data/table/hooks/useTableItems.js +63 -0
  17. package/cjs/data/table/hooks/useTableItems.js.map +1 -0
  18. package/cjs/data/table/hooks/useTableKeyboardNav.js +3 -3
  19. package/cjs/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  20. package/cjs/data/table/root/DataTableAuto.d.ts +18 -0
  21. package/cjs/data/table/root/DataTableAuto.js +71 -29
  22. package/cjs/data/table/root/DataTableAuto.js.map +1 -1
  23. package/cjs/data/table/root/DataTableRoot.context.d.ts +5 -3
  24. package/cjs/data/table/root/DataTableRoot.context.js.map +1 -1
  25. package/cjs/data/table/root/DataTableRoot.js +7 -4
  26. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  27. package/cjs/data/table/tr/DataTableTr.js +30 -32
  28. package/cjs/data/table/tr/DataTableTr.js.map +1 -1
  29. package/cjs/modal/Modal.js +3 -3
  30. package/cjs/modal/Modal.js.map +1 -1
  31. package/cjs/modal/types.d.ts +1 -0
  32. package/esm/data/table/column-header/DataTableColumnHeader.d.ts +1 -2
  33. package/esm/data/table/column-header/DataTableColumnHeader.js +14 -12
  34. package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  35. package/esm/data/table/column-header/useTableColumnResize.d.ts +5 -3
  36. package/esm/data/table/column-header/useTableColumnResize.js +129 -54
  37. package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
  38. package/esm/data/table/helpers/collectTableRowEntries.d.ts +16 -0
  39. package/esm/data/table/helpers/collectTableRowEntries.js +25 -0
  40. package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -0
  41. package/esm/data/table/helpers/table-keyboard.js +3 -3
  42. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  43. package/esm/data/table/hooks/useTableExpansion.d.ts +9 -6
  44. package/esm/data/table/hooks/useTableExpansion.js +36 -16
  45. package/esm/data/table/hooks/useTableExpansion.js.map +1 -1
  46. package/esm/data/table/hooks/useTableItems.d.ts +29 -0
  47. package/esm/data/table/hooks/useTableItems.js +58 -0
  48. package/esm/data/table/hooks/useTableItems.js.map +1 -0
  49. package/esm/data/table/hooks/useTableKeyboardNav.js +3 -3
  50. package/esm/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  51. package/esm/data/table/root/DataTableAuto.d.ts +18 -0
  52. package/esm/data/table/root/DataTableAuto.js +72 -30
  53. package/esm/data/table/root/DataTableAuto.js.map +1 -1
  54. package/esm/data/table/root/DataTableRoot.context.d.ts +5 -3
  55. package/esm/data/table/root/DataTableRoot.context.js.map +1 -1
  56. package/esm/data/table/root/DataTableRoot.js +7 -4
  57. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  58. package/esm/data/table/tr/DataTableTr.js +32 -34
  59. package/esm/data/table/tr/DataTableTr.js.map +1 -1
  60. package/esm/modal/Modal.js +3 -3
  61. package/esm/modal/Modal.js.map +1 -1
  62. package/esm/modal/types.d.ts +1 -0
  63. package/package.json +7 -7
  64. package/src/data/table/column-header/DataTableColumnHeader.tsx +22 -14
  65. package/src/data/table/column-header/useTableColumnResize.ts +152 -79
  66. package/src/data/table/helpers/collectTableRowEntries.ts +58 -0
  67. package/src/data/table/helpers/table-keyboard.ts +4 -4
  68. package/src/data/table/hooks/__tests__/useTableExpansion.test.tsx +115 -0
  69. package/src/data/table/hooks/__tests__/useTableItems.test.ts +131 -0
  70. package/src/data/table/hooks/useTableExpansion.tsx +63 -22
  71. package/src/data/table/hooks/useTableItems.ts +123 -0
  72. package/src/data/table/hooks/useTableKeyboardNav.ts +3 -3
  73. package/src/data/table/root/DataTableAuto.test.tsx +118 -0
  74. package/src/data/table/root/DataTableAuto.tsx +159 -49
  75. package/src/data/table/root/DataTableRoot.context.ts +4 -2
  76. package/src/data/table/root/DataTableRoot.tsx +20 -13
  77. package/src/data/table/tr/DataTableTr.tsx +48 -47
  78. package/src/modal/Modal.tsx +12 -6
  79. 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?: boolean;
14
- enableExpansion: boolean;
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
- allRowKeys: (string | number)[];
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
- allRowKeys,
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) => expandedIds.includes(id),
53
- [expandedIds],
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
- const next = expandedIds.includes(id)
59
- ? expandedIds.filter((eid) => eid !== id)
60
- : [...expandedIds, id];
61
- setExpandedIds(next);
62
- onDetailsPanelChange?.(next);
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
- [expandedIds, setExpandedIds, onDetailsPanelChange],
103
+ [isDetailsPanelExpandableById, setExpandedIds],
65
104
  );
66
105
 
67
106
  const isAllExpanded =
68
- allRowKeys.length > 0 &&
69
- allRowKeys.every((key) => expandedIds.includes(key));
107
+ expandableIds.size > 0 &&
108
+ Array.from(expandableIds).every((key) => expandedIds.includes(key));
70
109
 
71
110
  const toggleAll = useCallback(() => {
72
- const next = isAllExpanded ? [] : [...allRowKeys];
73
- setExpandedIds(next);
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
- enableExpansion={!!getDetailsPanelContent}
130
+ enableDetailsPanel={!!getDetailsPanelContent}
94
131
  >
95
132
  {children}
96
133
  </DataTableExpansionContextProvider>
97
134
  );
98
135
  }
99
136
 
100
- export { DataTableExpansionProvider, useDataTableExpansion };
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
- if (shouldBlockNavigation(event)) {
117
+ const action = getNavigationAction(event);
118
+ if (!action) {
118
119
  return;
119
120
  }
120
121
 
121
- const action = getNavigationAction(event);
122
- if (!action) {
122
+ if (shouldBlockNavigation(event)) {
123
123
  return;
124
124
  }
125
125