@dbcdk/react-components 0.0.38 → 0.0.39
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Checkbox } from '../../../components/forms/checkbox/Checkbox';
|
|
3
3
|
import { RadioButton } from '../../../components/forms/radio-buttons/RadioButton';
|
|
4
|
-
import {
|
|
4
|
+
import { SeverityBorderColor } from '../../../constants/severity';
|
|
5
5
|
import { useTableRowInteractions } from '../hooks/useTableRowInteractions';
|
|
6
6
|
import { cx } from '../table.classes';
|
|
7
7
|
import styles from '../Table.module.css';
|
|
@@ -21,7 +21,7 @@ export function TableRow({ row, rowId, columns, selectedRows, hasSelection, sele
|
|
|
21
21
|
});
|
|
22
22
|
return (_jsxs("tr", { className: cx(styles.row, onRowClick && styles.clickableRow, isSelected && styles.selectedRow, rowSeverity && styles.severity), style: {
|
|
23
23
|
['--row-severity-color']: rowSeverity
|
|
24
|
-
?
|
|
24
|
+
? SeverityBorderColor[rowSeverity]
|
|
25
25
|
: undefined,
|
|
26
26
|
}, tabIndex: onRowClick ? 0 : -1, onKeyDown: handleRowKeyDown, onMouseEnter: () => onRowMouseEnter === null || onRowMouseEnter === void 0 ? void 0 : onRowMouseEnter(row), onClick: handleRowClick, children: [hasSelection ? (_jsx("td", { className: cx(styles.cell, styles.selectionCell), "data-selection-control": "true", children: _jsx("div", { className: styles.selectionHitArea, "data-selection-control": "true", onClick: e => {
|
|
27
27
|
if (e.target !== e.currentTarget)
|
|
@@ -6,6 +6,14 @@ export const SeverityBgColor = {
|
|
|
6
6
|
info: 'var(--color-status-info)',
|
|
7
7
|
warning: 'var(--color-status-warning)',
|
|
8
8
|
};
|
|
9
|
+
export const SeverityBorderColor = {
|
|
10
|
+
neutral: 'var(--color-neutral-strong)',
|
|
11
|
+
brand: 'var(--color-brand)',
|
|
12
|
+
success: 'var(--color-status-success-border)',
|
|
13
|
+
error: 'var(--color-status-error-border)',
|
|
14
|
+
info: 'var(--color-status-info-border)',
|
|
15
|
+
warning: 'var(--color-status-warning-border)',
|
|
16
|
+
};
|
|
9
17
|
export const SeverityTextColor = {
|
|
10
18
|
neutral: 'var(--color-neutral-strong-fg)',
|
|
11
19
|
brand: 'var(--color-fg-on-brand)',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type Id = string | number;
|
|
2
2
|
interface Props<T> {
|
|
3
|
-
storageKey
|
|
3
|
+
storageKey?: string;
|
|
4
4
|
items: T[];
|
|
5
5
|
getId: (item: T) => Id;
|
|
6
6
|
initialSelectedIds?: Set<Id>;
|
|
@@ -8,15 +8,17 @@ interface Props<T> {
|
|
|
8
8
|
selectedIds: Set<Id>;
|
|
9
9
|
selectedItems: T[];
|
|
10
10
|
}) => void;
|
|
11
|
-
totalItems?: number;
|
|
12
11
|
selectionMode?: 'single' | 'multiple';
|
|
12
|
+
pruneToItems?: boolean;
|
|
13
|
+
storage?: 'local' | 'session';
|
|
13
14
|
}
|
|
14
|
-
export declare function useTableSelection<T>({ storageKey, items, getId, initialSelectedIds,
|
|
15
|
+
export declare function useTableSelection<T>({ storageKey, items, getId, initialSelectedIds, onSelectionChange, selectionMode, pruneToItems, storage, }: Props<T>): {
|
|
15
16
|
selectedIds: Set<Id>;
|
|
16
17
|
selectedItems: T[];
|
|
17
18
|
selectedItemMap: Map<Id, T>;
|
|
18
19
|
toggleItem: (item: T) => void;
|
|
19
20
|
toggleId: (id: Id, selected?: boolean) => void;
|
|
21
|
+
selectOnly: (id: Id) => void;
|
|
20
22
|
clearSelection: () => void;
|
|
21
23
|
allSelected: boolean;
|
|
22
24
|
anySelected: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
const EMPTY_IDS = new Set();
|
|
3
4
|
function safeParseIds(raw) {
|
|
4
5
|
if (!raw)
|
|
5
6
|
return null;
|
|
@@ -7,8 +8,7 @@ function safeParseIds(raw) {
|
|
|
7
8
|
const parsed = JSON.parse(raw);
|
|
8
9
|
if (!Array.isArray(parsed))
|
|
9
10
|
return null;
|
|
10
|
-
|
|
11
|
-
return parsed.filter(v => typeof v === 'string' || typeof v === 'number');
|
|
11
|
+
return parsed.filter((v) => typeof v === 'string' || typeof v === 'number');
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return null;
|
|
@@ -17,84 +17,91 @@ function safeParseIds(raw) {
|
|
|
17
17
|
function serializeIds(ids) {
|
|
18
18
|
return JSON.stringify(Array.from(ids));
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function areSetsEqual(a, b) {
|
|
21
|
+
if (a.size !== b.size)
|
|
22
|
+
return false;
|
|
23
|
+
for (const value of a) {
|
|
24
|
+
if (!b.has(value))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
export function useTableSelection({ storageKey, items, getId, initialSelectedIds, onSelectionChange, selectionMode = 'single', pruneToItems = false, storage = 'session', }) {
|
|
30
|
+
const resolvedInitialSelectedIds = initialSelectedIds !== null && initialSelectedIds !== void 0 ? initialSelectedIds : EMPTY_IDS;
|
|
31
|
+
const [selectedIds, setSelectedIds] = useState(resolvedInitialSelectedIds);
|
|
22
32
|
const [hydrated, setHydrated] = useState(false);
|
|
23
|
-
// Used to avoid unnecessary writes / loops (and helps with storage-event sync)
|
|
24
33
|
const lastWrittenRef = useRef(null);
|
|
25
|
-
// Fast lookup of current items by id
|
|
26
34
|
const itemsById = useMemo(() => {
|
|
27
|
-
const
|
|
28
|
-
for (const item of items)
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const map = new Map();
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
map.set(getId(item), item);
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
31
40
|
}, [items, getId]);
|
|
32
|
-
// Hydrate from localStorage on mount / when storageKey changes
|
|
33
41
|
useEffect(() => {
|
|
34
42
|
if (typeof window === 'undefined')
|
|
35
43
|
return;
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
setSelectedIds(new Set(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const storageApi = storage === 'local' ? window.localStorage : window.sessionStorage;
|
|
45
|
+
if (!storageKey) {
|
|
46
|
+
setSelectedIds(prev => areSetsEqual(prev, resolvedInitialSelectedIds) ? prev : new Set(resolvedInitialSelectedIds));
|
|
47
|
+
setHydrated(true);
|
|
48
|
+
lastWrittenRef.current = null;
|
|
49
|
+
return;
|
|
42
50
|
}
|
|
43
|
-
|
|
51
|
+
const parsed = safeParseIds(storageApi.getItem(storageKey));
|
|
52
|
+
const next = new Set(parsed !== null && parsed !== void 0 ? parsed : Array.from(resolvedInitialSelectedIds));
|
|
53
|
+
setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
|
|
54
|
+
lastWrittenRef.current = serializeIds(next);
|
|
44
55
|
setHydrated(true);
|
|
45
|
-
|
|
46
|
-
}, [storageKey]);
|
|
47
|
-
// Keep selection in sync across tabs/windows (optional but usually desired)
|
|
56
|
+
}, [storage, storageKey, resolvedInitialSelectedIds]);
|
|
48
57
|
useEffect(() => {
|
|
49
|
-
if (
|
|
58
|
+
if (!pruneToItems)
|
|
50
59
|
return;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const next = new Set(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return () => window.removeEventListener('storage', onStorage);
|
|
66
|
-
// Note: selectedIds is intentionally included to compare current vs next
|
|
67
|
-
}, [storageKey, initialSelectedIds, selectedIds]);
|
|
68
|
-
// Derive selectedItemMap from selectedIds + itemsById (never out of sync)
|
|
60
|
+
const visibleIds = new Set(Array.from(itemsById.keys()));
|
|
61
|
+
setSelectedIds(prev => {
|
|
62
|
+
if (prev.size === 0)
|
|
63
|
+
return prev;
|
|
64
|
+
if (visibleIds.size === 0)
|
|
65
|
+
return prev;
|
|
66
|
+
const next = new Set();
|
|
67
|
+
for (const id of prev) {
|
|
68
|
+
if (visibleIds.has(id))
|
|
69
|
+
next.add(id);
|
|
70
|
+
}
|
|
71
|
+
return areSetsEqual(prev, next) ? prev : next;
|
|
72
|
+
});
|
|
73
|
+
}, [pruneToItems, itemsById]);
|
|
69
74
|
const selectedItemMap = useMemo(() => {
|
|
70
|
-
const
|
|
75
|
+
const map = new Map();
|
|
71
76
|
for (const id of selectedIds) {
|
|
72
77
|
const item = itemsById.get(id);
|
|
73
78
|
if (item !== undefined)
|
|
74
|
-
|
|
79
|
+
map.set(id, item);
|
|
75
80
|
}
|
|
76
|
-
return
|
|
81
|
+
return map;
|
|
77
82
|
}, [selectedIds, itemsById]);
|
|
78
83
|
const selectedItems = useMemo(() => Array.from(selectedItemMap.values()), [selectedItemMap]);
|
|
79
84
|
const allSelected = useMemo(() => {
|
|
80
|
-
if (
|
|
85
|
+
if (items.length === 0)
|
|
81
86
|
return false;
|
|
82
|
-
return
|
|
83
|
-
}, [selectedIds,
|
|
87
|
+
return items.every(item => selectedIds.has(getId(item)));
|
|
88
|
+
}, [items, selectedIds, getId]);
|
|
84
89
|
const anySelected = useMemo(() => selectedIds.size > 0, [selectedIds]);
|
|
85
|
-
// Persist + notify (but only after hydration so we don’t clobber stored values)
|
|
86
90
|
useEffect(() => {
|
|
87
91
|
if (!hydrated)
|
|
88
92
|
return;
|
|
89
93
|
if (typeof window === 'undefined')
|
|
90
94
|
return;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
lastWrittenRef.current
|
|
95
|
+
if (storageKey) {
|
|
96
|
+
const storageApi = storage === 'local' ? window.localStorage : window.sessionStorage;
|
|
97
|
+
const nextStr = serializeIds(selectedIds);
|
|
98
|
+
if (lastWrittenRef.current !== nextStr) {
|
|
99
|
+
storageApi.setItem(storageKey, nextStr);
|
|
100
|
+
lastWrittenRef.current = nextStr;
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
103
|
onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange({ selectedIds, selectedItems });
|
|
97
|
-
}, [hydrated, selectedIds, selectedItems,
|
|
104
|
+
}, [hydrated, onSelectionChange, selectedIds, selectedItems, storage, storageKey]);
|
|
98
105
|
const toggleId = useCallback((id, selected) => {
|
|
99
106
|
setSelectedIds(prev => {
|
|
100
107
|
const next = new Set(prev);
|
|
@@ -104,19 +111,26 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
|
|
|
104
111
|
next.clear();
|
|
105
112
|
if (shouldSelect)
|
|
106
113
|
next.add(id);
|
|
107
|
-
return next;
|
|
114
|
+
return areSetsEqual(prev, next) ? prev : next;
|
|
108
115
|
}
|
|
109
|
-
// multiple
|
|
110
116
|
if (shouldSelect)
|
|
111
117
|
next.add(id);
|
|
112
118
|
else
|
|
113
119
|
next.delete(id);
|
|
114
|
-
return next;
|
|
120
|
+
return areSetsEqual(prev, next) ? prev : next;
|
|
115
121
|
});
|
|
116
122
|
}, [selectionMode]);
|
|
117
|
-
const toggleItem = useCallback((item) =>
|
|
123
|
+
const toggleItem = useCallback((item) => {
|
|
124
|
+
toggleId(getId(item));
|
|
125
|
+
}, [toggleId, getId]);
|
|
126
|
+
const selectOnly = useCallback((id) => {
|
|
127
|
+
setSelectedIds(prev => {
|
|
128
|
+
const next = new Set([id]);
|
|
129
|
+
return areSetsEqual(prev, next) ? prev : next;
|
|
130
|
+
});
|
|
131
|
+
}, []);
|
|
118
132
|
const clearSelection = useCallback(() => {
|
|
119
|
-
setSelectedIds(new Set());
|
|
133
|
+
setSelectedIds(prev => (prev.size === 0 ? prev : new Set()));
|
|
120
134
|
}, []);
|
|
121
135
|
const toggleAll = useCallback((selected) => {
|
|
122
136
|
if (!selected) {
|
|
@@ -124,15 +138,16 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
|
|
|
124
138
|
return;
|
|
125
139
|
}
|
|
126
140
|
if (selectionMode === 'single') {
|
|
127
|
-
// Choose the first item (or nothing if empty)
|
|
128
141
|
const first = items[0];
|
|
129
|
-
|
|
142
|
+
const next = first ? new Set([getId(first)]) : new Set();
|
|
143
|
+
setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
|
|
130
144
|
return;
|
|
131
145
|
}
|
|
132
146
|
const next = new Set();
|
|
133
|
-
for (const item of items)
|
|
147
|
+
for (const item of items) {
|
|
134
148
|
next.add(getId(item));
|
|
135
|
-
|
|
149
|
+
}
|
|
150
|
+
setSelectedIds(prev => (areSetsEqual(prev, next) ? prev : next));
|
|
136
151
|
}, [clearSelection, getId, items, selectionMode]);
|
|
137
152
|
return {
|
|
138
153
|
selectedIds,
|
|
@@ -140,6 +155,7 @@ export function useTableSelection({ storageKey, items, getId, initialSelectedIds
|
|
|
140
155
|
selectedItemMap,
|
|
141
156
|
toggleItem,
|
|
142
157
|
toggleId,
|
|
158
|
+
selectOnly,
|
|
143
159
|
clearSelection,
|
|
144
160
|
allSelected,
|
|
145
161
|
anySelected,
|