@dbcdk/react-components 0.0.208 → 0.0.209
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/components/overlay/side-panel/SidePanel.cjs +80 -2
- package/dist/components/overlay/side-panel/SidePanel.js +80 -2
- package/dist/components/split-pane/SplitPane.cjs +22 -7
- package/dist/components/split-pane/SplitPane.d.ts +5 -2
- package/dist/components/split-pane/SplitPane.js +22 -7
- package/dist/components/split-pane/SplitPane.module.css +9 -0
- package/dist/components/split-pane/provider/SplitPaneContext.cjs +5 -2
- package/dist/components/split-pane/provider/SplitPaneContext.d.ts +3 -1
- package/dist/components/split-pane/provider/SplitPaneContext.js +5 -2
- package/dist/components/tree-view/TreeNodeRow.cjs +70 -30
- package/dist/components/tree-view/TreeNodeRow.d.ts +6 -1
- package/dist/components/tree-view/TreeNodeRow.js +70 -30
- package/dist/components/tree-view/TreeView.cjs +107 -19
- package/dist/components/tree-view/TreeView.d.ts +1 -1
- package/dist/components/tree-view/TreeView.js +107 -19
- package/dist/components/tree-view/TreeView.module.css +47 -3
- package/dist/components/tree-view/types.d.ts +6 -0
- package/dist/styles/css-helper-classes/typography.css +4 -0
- package/package.json +1 -1
|
@@ -13,6 +13,36 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
13
13
|
|
|
14
14
|
var styles__default = /*#__PURE__*/_interopDefault(styles);
|
|
15
15
|
|
|
16
|
+
function getFocusable(container) {
|
|
17
|
+
var _a, _b;
|
|
18
|
+
const selector = [
|
|
19
|
+
'a[href]:not([aria-disabled="true"])',
|
|
20
|
+
"button:not([disabled])",
|
|
21
|
+
"input:not([disabled])",
|
|
22
|
+
"select:not([disabled])",
|
|
23
|
+
"textarea:not([disabled])",
|
|
24
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
25
|
+
].join(",");
|
|
26
|
+
const focusables = [];
|
|
27
|
+
const addFocusableContent = (root) => {
|
|
28
|
+
const candidates = root.querySelectorAll(selector);
|
|
29
|
+
for (const element of candidates) {
|
|
30
|
+
if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true" || element.getAttribute("aria-disabled") === "true") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!focusables.includes(element)) focusables.push(element);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
addFocusableContent(container);
|
|
37
|
+
for (const trigger of container.querySelectorAll("[aria-controls]")) {
|
|
38
|
+
const controlledIds = (_b = (_a = trigger.getAttribute("aria-controls")) == null ? void 0 : _a.split(/\s+/)) != null ? _b : [];
|
|
39
|
+
for (const controlledId of controlledIds) {
|
|
40
|
+
const controlledContent = document.getElementById(controlledId);
|
|
41
|
+
if (controlledContent) addFocusableContent(controlledContent);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return focusables;
|
|
45
|
+
}
|
|
16
46
|
function SidePanel({
|
|
17
47
|
isOpen,
|
|
18
48
|
onClose,
|
|
@@ -36,23 +66,70 @@ function SidePanel({
|
|
|
36
66
|
const [shouldRender, setShouldRender] = react.useState(isOpen);
|
|
37
67
|
const [isActive, setIsActive] = react.useState(false);
|
|
38
68
|
const panelRef = react.useRef(null);
|
|
69
|
+
const returnFocusRef = react.useRef(null);
|
|
70
|
+
const wasOpenRef = react.useRef(false);
|
|
39
71
|
react.useEffect(() => setMounted(true), []);
|
|
40
72
|
react.useEffect(() => {
|
|
41
73
|
if (isOpen) setShouldRender(true);
|
|
42
74
|
}, [isOpen]);
|
|
43
75
|
react.useEffect(() => {
|
|
44
|
-
if (!isOpen) return;
|
|
76
|
+
if (!isOpen || !shouldRender || !mounted) return;
|
|
77
|
+
if (!wasOpenRef.current) {
|
|
78
|
+
const activeElement = document.activeElement;
|
|
79
|
+
returnFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null;
|
|
80
|
+
wasOpenRef.current = true;
|
|
81
|
+
}
|
|
82
|
+
const focusInitial = () => {
|
|
83
|
+
var _a;
|
|
84
|
+
const panel = panelRef.current;
|
|
85
|
+
if (!panel) return;
|
|
86
|
+
const closeButton = panel.querySelector(
|
|
87
|
+
'[data-cy="side-panel-close-button"]'
|
|
88
|
+
);
|
|
89
|
+
const focusables = getFocusable(panel);
|
|
90
|
+
((_a = closeButton != null ? closeButton : focusables[0]) != null ? _a : panel).focus();
|
|
91
|
+
};
|
|
92
|
+
focusInitial();
|
|
93
|
+
const frame = requestAnimationFrame(focusInitial);
|
|
45
94
|
const handleKeyDown = (e) => {
|
|
46
95
|
if (e.key === "Escape") {
|
|
47
96
|
e.stopPropagation();
|
|
48
97
|
onClose();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (e.key !== "Tab") return;
|
|
101
|
+
const panel = panelRef.current;
|
|
102
|
+
if (!panel) return;
|
|
103
|
+
const focusables = getFocusable(panel);
|
|
104
|
+
if (focusables.length === 0) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
panel.focus();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const first = focusables[0];
|
|
110
|
+
const last = focusables[focusables.length - 1];
|
|
111
|
+
const active = document.activeElement;
|
|
112
|
+
if (e.shiftKey && (active === first || !panel.contains(active))) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
last.focus();
|
|
115
|
+
} else if (!e.shiftKey && (active === last || !panel.contains(active))) {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
first.focus();
|
|
49
118
|
}
|
|
50
119
|
};
|
|
51
120
|
document.addEventListener("keydown", handleKeyDown);
|
|
52
121
|
return () => {
|
|
122
|
+
cancelAnimationFrame(frame);
|
|
53
123
|
document.removeEventListener("keydown", handleKeyDown);
|
|
54
124
|
};
|
|
55
|
-
}, [isOpen, onClose]);
|
|
125
|
+
}, [isOpen, mounted, onClose, shouldRender]);
|
|
126
|
+
react.useEffect(() => {
|
|
127
|
+
var _a;
|
|
128
|
+
if (isOpen || !wasOpenRef.current) return;
|
|
129
|
+
wasOpenRef.current = false;
|
|
130
|
+
(_a = returnFocusRef.current) == null ? void 0 : _a.focus();
|
|
131
|
+
returnFocusRef.current = null;
|
|
132
|
+
}, [isOpen]);
|
|
56
133
|
react.useEffect(() => {
|
|
57
134
|
if (!shouldRender) return;
|
|
58
135
|
if (!isOpen) {
|
|
@@ -103,6 +180,7 @@ function SidePanel({
|
|
|
103
180
|
"data-cy": "details-panel",
|
|
104
181
|
role: "dialog",
|
|
105
182
|
"aria-modal": "true",
|
|
183
|
+
tabIndex: -1,
|
|
106
184
|
children: [
|
|
107
185
|
hasDetails ? /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: styles__default.default.detailsCol, "data-cy": "details-panel-details", children: [
|
|
108
186
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.detailsHeader, children: [
|
|
@@ -7,6 +7,36 @@ import { Headline } from '../../../components/headline/Headline';
|
|
|
7
7
|
import { MOTION_MS } from '../../../styles/animation';
|
|
8
8
|
import styles from './SidePanel.module.css';
|
|
9
9
|
|
|
10
|
+
function getFocusable(container) {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
const selector = [
|
|
13
|
+
'a[href]:not([aria-disabled="true"])',
|
|
14
|
+
"button:not([disabled])",
|
|
15
|
+
"input:not([disabled])",
|
|
16
|
+
"select:not([disabled])",
|
|
17
|
+
"textarea:not([disabled])",
|
|
18
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
19
|
+
].join(",");
|
|
20
|
+
const focusables = [];
|
|
21
|
+
const addFocusableContent = (root) => {
|
|
22
|
+
const candidates = root.querySelectorAll(selector);
|
|
23
|
+
for (const element of candidates) {
|
|
24
|
+
if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true" || element.getAttribute("aria-disabled") === "true") {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!focusables.includes(element)) focusables.push(element);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
addFocusableContent(container);
|
|
31
|
+
for (const trigger of container.querySelectorAll("[aria-controls]")) {
|
|
32
|
+
const controlledIds = (_b = (_a = trigger.getAttribute("aria-controls")) == null ? void 0 : _a.split(/\s+/)) != null ? _b : [];
|
|
33
|
+
for (const controlledId of controlledIds) {
|
|
34
|
+
const controlledContent = document.getElementById(controlledId);
|
|
35
|
+
if (controlledContent) addFocusableContent(controlledContent);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return focusables;
|
|
39
|
+
}
|
|
10
40
|
function SidePanel({
|
|
11
41
|
isOpen,
|
|
12
42
|
onClose,
|
|
@@ -30,23 +60,70 @@ function SidePanel({
|
|
|
30
60
|
const [shouldRender, setShouldRender] = useState(isOpen);
|
|
31
61
|
const [isActive, setIsActive] = useState(false);
|
|
32
62
|
const panelRef = useRef(null);
|
|
63
|
+
const returnFocusRef = useRef(null);
|
|
64
|
+
const wasOpenRef = useRef(false);
|
|
33
65
|
useEffect(() => setMounted(true), []);
|
|
34
66
|
useEffect(() => {
|
|
35
67
|
if (isOpen) setShouldRender(true);
|
|
36
68
|
}, [isOpen]);
|
|
37
69
|
useEffect(() => {
|
|
38
|
-
if (!isOpen) return;
|
|
70
|
+
if (!isOpen || !shouldRender || !mounted) return;
|
|
71
|
+
if (!wasOpenRef.current) {
|
|
72
|
+
const activeElement = document.activeElement;
|
|
73
|
+
returnFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null;
|
|
74
|
+
wasOpenRef.current = true;
|
|
75
|
+
}
|
|
76
|
+
const focusInitial = () => {
|
|
77
|
+
var _a;
|
|
78
|
+
const panel = panelRef.current;
|
|
79
|
+
if (!panel) return;
|
|
80
|
+
const closeButton = panel.querySelector(
|
|
81
|
+
'[data-cy="side-panel-close-button"]'
|
|
82
|
+
);
|
|
83
|
+
const focusables = getFocusable(panel);
|
|
84
|
+
((_a = closeButton != null ? closeButton : focusables[0]) != null ? _a : panel).focus();
|
|
85
|
+
};
|
|
86
|
+
focusInitial();
|
|
87
|
+
const frame = requestAnimationFrame(focusInitial);
|
|
39
88
|
const handleKeyDown = (e) => {
|
|
40
89
|
if (e.key === "Escape") {
|
|
41
90
|
e.stopPropagation();
|
|
42
91
|
onClose();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (e.key !== "Tab") return;
|
|
95
|
+
const panel = panelRef.current;
|
|
96
|
+
if (!panel) return;
|
|
97
|
+
const focusables = getFocusable(panel);
|
|
98
|
+
if (focusables.length === 0) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
panel.focus();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const first = focusables[0];
|
|
104
|
+
const last = focusables[focusables.length - 1];
|
|
105
|
+
const active = document.activeElement;
|
|
106
|
+
if (e.shiftKey && (active === first || !panel.contains(active))) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
last.focus();
|
|
109
|
+
} else if (!e.shiftKey && (active === last || !panel.contains(active))) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
first.focus();
|
|
43
112
|
}
|
|
44
113
|
};
|
|
45
114
|
document.addEventListener("keydown", handleKeyDown);
|
|
46
115
|
return () => {
|
|
116
|
+
cancelAnimationFrame(frame);
|
|
47
117
|
document.removeEventListener("keydown", handleKeyDown);
|
|
48
118
|
};
|
|
49
|
-
}, [isOpen, onClose]);
|
|
119
|
+
}, [isOpen, mounted, onClose, shouldRender]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
var _a;
|
|
122
|
+
if (isOpen || !wasOpenRef.current) return;
|
|
123
|
+
wasOpenRef.current = false;
|
|
124
|
+
(_a = returnFocusRef.current) == null ? void 0 : _a.focus();
|
|
125
|
+
returnFocusRef.current = null;
|
|
126
|
+
}, [isOpen]);
|
|
50
127
|
useEffect(() => {
|
|
51
128
|
if (!shouldRender) return;
|
|
52
129
|
if (!isOpen) {
|
|
@@ -97,6 +174,7 @@ function SidePanel({
|
|
|
97
174
|
"data-cy": "details-panel",
|
|
98
175
|
role: "dialog",
|
|
99
176
|
"aria-modal": "true",
|
|
177
|
+
tabIndex: -1,
|
|
100
178
|
children: [
|
|
101
179
|
hasDetails ? /* @__PURE__ */ jsxs("aside", { className: styles.detailsCol, "data-cy": "details-panel-details", children: [
|
|
102
180
|
/* @__PURE__ */ jsxs("div", { className: styles.detailsHeader, children: [
|
|
@@ -22,7 +22,8 @@ function SplitPane({
|
|
|
22
22
|
showDivider = "hover",
|
|
23
23
|
gutterSize = 16,
|
|
24
24
|
storageKey,
|
|
25
|
-
fillViewport = false
|
|
25
|
+
fillViewport = false,
|
|
26
|
+
secondaryVisible = true
|
|
26
27
|
}) {
|
|
27
28
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
28
29
|
SplitPaneContext.SplitPaneProvider,
|
|
@@ -31,6 +32,7 @@ function SplitPane({
|
|
|
31
32
|
initialPrimarySize,
|
|
32
33
|
minPrimarySize,
|
|
33
34
|
minSecondarySize,
|
|
35
|
+
gutterSize,
|
|
34
36
|
storageKey,
|
|
35
37
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
36
38
|
SplitPaneContainer,
|
|
@@ -38,6 +40,7 @@ function SplitPane({
|
|
|
38
40
|
showDivider,
|
|
39
41
|
gutterSize,
|
|
40
42
|
fillViewport,
|
|
43
|
+
secondaryVisible,
|
|
41
44
|
children
|
|
42
45
|
}
|
|
43
46
|
)
|
|
@@ -48,7 +51,8 @@ function SplitPaneContainer({
|
|
|
48
51
|
children,
|
|
49
52
|
showDivider,
|
|
50
53
|
gutterSize,
|
|
51
|
-
fillViewport
|
|
54
|
+
fillViewport,
|
|
55
|
+
secondaryVisible
|
|
52
56
|
}) {
|
|
53
57
|
const { direction, primarySize, containerRef } = SplitPaneContext.useSplitPaneContext();
|
|
54
58
|
const style = react.useMemo(
|
|
@@ -66,6 +70,7 @@ function SplitPaneContainer({
|
|
|
66
70
|
"data-direction": direction,
|
|
67
71
|
"data-divider": showDivider,
|
|
68
72
|
"data-fill-viewport": fillViewport,
|
|
73
|
+
"data-secondary-visible": secondaryVisible,
|
|
69
74
|
style,
|
|
70
75
|
children
|
|
71
76
|
}
|
|
@@ -77,13 +82,14 @@ function SplitPanePrimary({ children }) {
|
|
|
77
82
|
function SplitPaneSecondary({ children }) {
|
|
78
83
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.secondary, children });
|
|
79
84
|
}
|
|
80
|
-
function SplitPaneGutter() {
|
|
85
|
+
function SplitPaneGutter({ label = "Resize panes" }) {
|
|
81
86
|
const {
|
|
82
87
|
direction,
|
|
83
88
|
primarySize,
|
|
84
89
|
setPrimarySize,
|
|
85
90
|
minPrimarySize,
|
|
86
91
|
minSecondarySize,
|
|
92
|
+
gutterSize,
|
|
87
93
|
containerRef,
|
|
88
94
|
resetDefault
|
|
89
95
|
} = SplitPaneContext.useSplitPaneContext();
|
|
@@ -101,9 +107,10 @@ function SplitPaneGutter() {
|
|
|
101
107
|
if (!el) return { maxPrimary: Infinity, total: 0 };
|
|
102
108
|
const rect = el.getBoundingClientRect();
|
|
103
109
|
const total = direction === "horizontal" ? rect.width : rect.height;
|
|
104
|
-
|
|
110
|
+
if (!Number.isFinite(total) || total <= 0) return { maxPrimary: Infinity, total };
|
|
111
|
+
const maxPrimary2 = Math.max(minPrimarySize, total - minSecondarySize - gutterSize);
|
|
105
112
|
return { maxPrimary: maxPrimary2, total };
|
|
106
|
-
}, [containerRef, direction, minPrimarySize, minSecondarySize]);
|
|
113
|
+
}, [containerRef, direction, gutterSize, minPrimarySize, minSecondarySize]);
|
|
107
114
|
const onPointerDown = react.useCallback(
|
|
108
115
|
(e) => {
|
|
109
116
|
const el = containerRef.current;
|
|
@@ -163,13 +170,21 @@ function SplitPaneGutter() {
|
|
|
163
170
|
const ariaOrientation = direction === "horizontal" ? "vertical" : "horizontal";
|
|
164
171
|
const [maxPrimary, setMaxPrimary] = react.useState(Infinity);
|
|
165
172
|
react.useEffect(() => {
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
const container = containerRef.current;
|
|
174
|
+
if (!container) return;
|
|
175
|
+
const updateMax = () => setMaxPrimary(computeClamp().maxPrimary);
|
|
176
|
+
updateMax();
|
|
177
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
178
|
+
const observer = new ResizeObserver(updateMax);
|
|
179
|
+
observer.observe(container);
|
|
180
|
+
return () => observer.disconnect();
|
|
181
|
+
}, [computeClamp, containerRef]);
|
|
168
182
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.gutter, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
169
183
|
"div",
|
|
170
184
|
{
|
|
171
185
|
className: styles__default.default.resizer,
|
|
172
186
|
role: "separator",
|
|
187
|
+
"aria-label": label,
|
|
173
188
|
"aria-orientation": ariaOrientation,
|
|
174
189
|
"aria-valuemin": Math.round(minPrimarySize),
|
|
175
190
|
"aria-valuemax": Number.isFinite(maxPrimary) ? Math.round(maxPrimary) : void 0,
|
|
@@ -10,12 +10,15 @@ export interface SplitPaneProps {
|
|
|
10
10
|
gutterSize?: number;
|
|
11
11
|
storageKey?: string;
|
|
12
12
|
fillViewport?: boolean;
|
|
13
|
+
secondaryVisible?: boolean;
|
|
13
14
|
}
|
|
14
|
-
export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, fillViewport, }: SplitPaneProps): JSX.Element;
|
|
15
|
+
export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, fillViewport, secondaryVisible, }: SplitPaneProps): JSX.Element;
|
|
15
16
|
export declare function SplitPanePrimary({ children }: {
|
|
16
17
|
children: ReactNode;
|
|
17
18
|
}): JSX.Element;
|
|
18
19
|
export declare function SplitPaneSecondary({ children }: {
|
|
19
20
|
children: ReactNode;
|
|
20
21
|
}): JSX.Element;
|
|
21
|
-
export declare function SplitPaneGutter(
|
|
22
|
+
export declare function SplitPaneGutter({ label }: {
|
|
23
|
+
label?: string;
|
|
24
|
+
}): JSX.Element;
|
|
@@ -16,7 +16,8 @@ function SplitPane({
|
|
|
16
16
|
showDivider = "hover",
|
|
17
17
|
gutterSize = 16,
|
|
18
18
|
storageKey,
|
|
19
|
-
fillViewport = false
|
|
19
|
+
fillViewport = false,
|
|
20
|
+
secondaryVisible = true
|
|
20
21
|
}) {
|
|
21
22
|
return /* @__PURE__ */ jsx(
|
|
22
23
|
SplitPaneProvider,
|
|
@@ -25,6 +26,7 @@ function SplitPane({
|
|
|
25
26
|
initialPrimarySize,
|
|
26
27
|
minPrimarySize,
|
|
27
28
|
minSecondarySize,
|
|
29
|
+
gutterSize,
|
|
28
30
|
storageKey,
|
|
29
31
|
children: /* @__PURE__ */ jsx(
|
|
30
32
|
SplitPaneContainer,
|
|
@@ -32,6 +34,7 @@ function SplitPane({
|
|
|
32
34
|
showDivider,
|
|
33
35
|
gutterSize,
|
|
34
36
|
fillViewport,
|
|
37
|
+
secondaryVisible,
|
|
35
38
|
children
|
|
36
39
|
}
|
|
37
40
|
)
|
|
@@ -42,7 +45,8 @@ function SplitPaneContainer({
|
|
|
42
45
|
children,
|
|
43
46
|
showDivider,
|
|
44
47
|
gutterSize,
|
|
45
|
-
fillViewport
|
|
48
|
+
fillViewport,
|
|
49
|
+
secondaryVisible
|
|
46
50
|
}) {
|
|
47
51
|
const { direction, primarySize, containerRef } = useSplitPaneContext();
|
|
48
52
|
const style = useMemo(
|
|
@@ -60,6 +64,7 @@ function SplitPaneContainer({
|
|
|
60
64
|
"data-direction": direction,
|
|
61
65
|
"data-divider": showDivider,
|
|
62
66
|
"data-fill-viewport": fillViewport,
|
|
67
|
+
"data-secondary-visible": secondaryVisible,
|
|
63
68
|
style,
|
|
64
69
|
children
|
|
65
70
|
}
|
|
@@ -71,13 +76,14 @@ function SplitPanePrimary({ children }) {
|
|
|
71
76
|
function SplitPaneSecondary({ children }) {
|
|
72
77
|
return /* @__PURE__ */ jsx("div", { className: styles.secondary, children });
|
|
73
78
|
}
|
|
74
|
-
function SplitPaneGutter() {
|
|
79
|
+
function SplitPaneGutter({ label = "Resize panes" }) {
|
|
75
80
|
const {
|
|
76
81
|
direction,
|
|
77
82
|
primarySize,
|
|
78
83
|
setPrimarySize,
|
|
79
84
|
minPrimarySize,
|
|
80
85
|
minSecondarySize,
|
|
86
|
+
gutterSize,
|
|
81
87
|
containerRef,
|
|
82
88
|
resetDefault
|
|
83
89
|
} = useSplitPaneContext();
|
|
@@ -95,9 +101,10 @@ function SplitPaneGutter() {
|
|
|
95
101
|
if (!el) return { maxPrimary: Infinity, total: 0 };
|
|
96
102
|
const rect = el.getBoundingClientRect();
|
|
97
103
|
const total = direction === "horizontal" ? rect.width : rect.height;
|
|
98
|
-
|
|
104
|
+
if (!Number.isFinite(total) || total <= 0) return { maxPrimary: Infinity, total };
|
|
105
|
+
const maxPrimary2 = Math.max(minPrimarySize, total - minSecondarySize - gutterSize);
|
|
99
106
|
return { maxPrimary: maxPrimary2, total };
|
|
100
|
-
}, [containerRef, direction, minPrimarySize, minSecondarySize]);
|
|
107
|
+
}, [containerRef, direction, gutterSize, minPrimarySize, minSecondarySize]);
|
|
101
108
|
const onPointerDown = useCallback(
|
|
102
109
|
(e) => {
|
|
103
110
|
const el = containerRef.current;
|
|
@@ -157,13 +164,21 @@ function SplitPaneGutter() {
|
|
|
157
164
|
const ariaOrientation = direction === "horizontal" ? "vertical" : "horizontal";
|
|
158
165
|
const [maxPrimary, setMaxPrimary] = useState(Infinity);
|
|
159
166
|
useEffect(() => {
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
const container = containerRef.current;
|
|
168
|
+
if (!container) return;
|
|
169
|
+
const updateMax = () => setMaxPrimary(computeClamp().maxPrimary);
|
|
170
|
+
updateMax();
|
|
171
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
172
|
+
const observer = new ResizeObserver(updateMax);
|
|
173
|
+
observer.observe(container);
|
|
174
|
+
return () => observer.disconnect();
|
|
175
|
+
}, [computeClamp, containerRef]);
|
|
162
176
|
return /* @__PURE__ */ jsx("div", { className: styles.gutter, children: /* @__PURE__ */ jsx(
|
|
163
177
|
"div",
|
|
164
178
|
{
|
|
165
179
|
className: styles.resizer,
|
|
166
180
|
role: "separator",
|
|
181
|
+
"aria-label": label,
|
|
167
182
|
"aria-orientation": ariaOrientation,
|
|
168
183
|
"aria-valuemin": Math.round(minPrimarySize),
|
|
169
184
|
"aria-valuemax": Number.isFinite(maxPrimary) ? Math.round(maxPrimary) : void 0,
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
max-height: 100%;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
.container[data-secondary-visible='false'] > .primary {
|
|
17
|
+
flex: 1 1 auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.container[data-secondary-visible='false'] > .gutter,
|
|
21
|
+
.container[data-secondary-visible='false'] > .secondary {
|
|
22
|
+
display: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
.container[data-direction='horizontal'] {
|
|
17
26
|
flex-direction: row;
|
|
18
27
|
}
|
|
@@ -44,6 +44,7 @@ function SplitPaneProvider({
|
|
|
44
44
|
initialPrimarySize,
|
|
45
45
|
minPrimarySize,
|
|
46
46
|
minSecondarySize,
|
|
47
|
+
gutterSize,
|
|
47
48
|
storageKey
|
|
48
49
|
}) {
|
|
49
50
|
const containerRef = React.useRef(null);
|
|
@@ -55,8 +56,8 @@ function SplitPaneProvider({
|
|
|
55
56
|
const rect = el.getBoundingClientRect();
|
|
56
57
|
const total = direction === "horizontal" ? rect.width : rect.height;
|
|
57
58
|
if (!Number.isFinite(total) || total <= 0) return null;
|
|
58
|
-
return Math.max(minPrimarySize, total - minSecondarySize);
|
|
59
|
-
}, [direction, minPrimarySize, minSecondarySize]);
|
|
59
|
+
return Math.max(minPrimarySize, total - minSecondarySize - gutterSize);
|
|
60
|
+
}, [direction, gutterSize, minPrimarySize, minSecondarySize]);
|
|
60
61
|
React.useEffect(() => {
|
|
61
62
|
if (!storageKey) return;
|
|
62
63
|
const stored = readStoredSize(storageKey);
|
|
@@ -108,6 +109,7 @@ function SplitPaneProvider({
|
|
|
108
109
|
resetDefault,
|
|
109
110
|
minPrimarySize,
|
|
110
111
|
minSecondarySize,
|
|
112
|
+
gutterSize,
|
|
111
113
|
containerRef,
|
|
112
114
|
storageKey
|
|
113
115
|
}),
|
|
@@ -118,6 +120,7 @@ function SplitPaneProvider({
|
|
|
118
120
|
resetDefault,
|
|
119
121
|
minPrimarySize,
|
|
120
122
|
minSecondarySize,
|
|
123
|
+
gutterSize,
|
|
121
124
|
storageKey
|
|
122
125
|
]
|
|
123
126
|
);
|
|
@@ -6,6 +6,7 @@ export interface SplitPaneContextValue {
|
|
|
6
6
|
setPrimarySize: (newSize: number) => void;
|
|
7
7
|
minPrimarySize: number;
|
|
8
8
|
minSecondarySize: number;
|
|
9
|
+
gutterSize: number;
|
|
9
10
|
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
10
11
|
storageKey?: string;
|
|
11
12
|
resetDefault: () => void;
|
|
@@ -18,7 +19,8 @@ type SplitPaneProviderProps = {
|
|
|
18
19
|
initialPrimarySize: number;
|
|
19
20
|
minPrimarySize: number;
|
|
20
21
|
minSecondarySize: number;
|
|
22
|
+
gutterSize: number;
|
|
21
23
|
storageKey?: string;
|
|
22
24
|
};
|
|
23
|
-
export declare function SplitPaneProvider({ children, direction, initialPrimarySize, minPrimarySize, minSecondarySize, storageKey, }: SplitPaneProviderProps): React.ReactNode;
|
|
25
|
+
export declare function SplitPaneProvider({ children, direction, initialPrimarySize, minPrimarySize, minSecondarySize, gutterSize, storageKey, }: SplitPaneProviderProps): React.ReactNode;
|
|
24
26
|
export {};
|
|
@@ -38,6 +38,7 @@ function SplitPaneProvider({
|
|
|
38
38
|
initialPrimarySize,
|
|
39
39
|
minPrimarySize,
|
|
40
40
|
minSecondarySize,
|
|
41
|
+
gutterSize,
|
|
41
42
|
storageKey
|
|
42
43
|
}) {
|
|
43
44
|
const containerRef = useRef(null);
|
|
@@ -49,8 +50,8 @@ function SplitPaneProvider({
|
|
|
49
50
|
const rect = el.getBoundingClientRect();
|
|
50
51
|
const total = direction === "horizontal" ? rect.width : rect.height;
|
|
51
52
|
if (!Number.isFinite(total) || total <= 0) return null;
|
|
52
|
-
return Math.max(minPrimarySize, total - minSecondarySize);
|
|
53
|
-
}, [direction, minPrimarySize, minSecondarySize]);
|
|
53
|
+
return Math.max(minPrimarySize, total - minSecondarySize - gutterSize);
|
|
54
|
+
}, [direction, gutterSize, minPrimarySize, minSecondarySize]);
|
|
54
55
|
useEffect(() => {
|
|
55
56
|
if (!storageKey) return;
|
|
56
57
|
const stored = readStoredSize(storageKey);
|
|
@@ -102,6 +103,7 @@ function SplitPaneProvider({
|
|
|
102
103
|
resetDefault,
|
|
103
104
|
minPrimarySize,
|
|
104
105
|
minSecondarySize,
|
|
106
|
+
gutterSize,
|
|
105
107
|
containerRef,
|
|
106
108
|
storageKey
|
|
107
109
|
}),
|
|
@@ -112,6 +114,7 @@ function SplitPaneProvider({
|
|
|
112
114
|
resetDefault,
|
|
113
115
|
minPrimarySize,
|
|
114
116
|
minSecondarySize,
|
|
117
|
+
gutterSize,
|
|
115
118
|
storageKey
|
|
116
119
|
]
|
|
117
120
|
);
|
|
@@ -16,12 +16,17 @@ function TreeNodeRow({
|
|
|
16
16
|
wrapLabels,
|
|
17
17
|
isExpanded,
|
|
18
18
|
toggle,
|
|
19
|
+
expand,
|
|
20
|
+
expandOnSelect,
|
|
19
21
|
resolvedChildren,
|
|
20
22
|
loadingIds,
|
|
21
23
|
errorIds,
|
|
22
24
|
requestLoad,
|
|
23
25
|
loadChildren,
|
|
24
26
|
selectedId,
|
|
27
|
+
currentId,
|
|
28
|
+
focusedId,
|
|
29
|
+
onFocusNode,
|
|
25
30
|
onSelectNode,
|
|
26
31
|
renderLabel
|
|
27
32
|
}) {
|
|
@@ -33,6 +38,7 @@ function TreeNodeRow({
|
|
|
33
38
|
const loading = loadingIds.has(node.id);
|
|
34
39
|
const error = errorIds[node.id];
|
|
35
40
|
const selected = selectedId !== void 0 && selectedId === node.id;
|
|
41
|
+
const current = currentId !== void 0 && currentId === node.id;
|
|
36
42
|
react.useEffect(() => {
|
|
37
43
|
if (expanded && children === void 0 && !loading && !error && loadChildren) {
|
|
38
44
|
requestLoad(node);
|
|
@@ -44,38 +50,67 @@ function TreeNodeRow({
|
|
|
44
50
|
role: "treeitem",
|
|
45
51
|
"aria-expanded": expandable ? expanded : void 0,
|
|
46
52
|
"aria-selected": selected,
|
|
53
|
+
"aria-current": current ? "page" : void 0,
|
|
54
|
+
"aria-level": depth + 1,
|
|
55
|
+
"data-tree-node-id": node.id,
|
|
47
56
|
style: { ["--tree-depth"]: depth },
|
|
48
57
|
children: [
|
|
49
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
58
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
className: styles__default.default.row,
|
|
62
|
+
"data-tree-row": "true",
|
|
63
|
+
"data-selected": selected || void 0,
|
|
64
|
+
"data-current": current || void 0,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
67
|
+
"button",
|
|
68
|
+
{
|
|
69
|
+
type: "button",
|
|
70
|
+
className: styles__default.default.toggle,
|
|
71
|
+
"data-tree-toggle": "true",
|
|
72
|
+
onClick: () => expandable && toggle(node.id),
|
|
73
|
+
disabled: !expandable,
|
|
74
|
+
"aria-hidden": !expandable,
|
|
75
|
+
tabIndex: -1,
|
|
76
|
+
"aria-label": expanded ? "Luk relationer" : "Vis relationer",
|
|
77
|
+
children: loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: `spin ${styles__default.default.spinner}`, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
|
|
78
|
+
// A resolved node's chevron means "this has children"; an
|
|
79
|
+
// unresolved lazy node's dimmer chevron means "might have
|
|
80
|
+
// children, click to check" - we genuinely don't know yet,
|
|
81
|
+
// and shouldn't visually assert otherwise.
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: isResolved ? void 0 : styles__default.default.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { "aria-hidden": "true" }) })
|
|
83
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.leafSpacer, "aria-hidden": "true" })
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
87
|
+
"button",
|
|
88
|
+
{
|
|
89
|
+
type: "button",
|
|
90
|
+
className: styles__default.default.labelButton,
|
|
91
|
+
"data-tree-focus-target": "true",
|
|
92
|
+
"data-tree-focus-node-id": node.id,
|
|
93
|
+
tabIndex: focusedId === node.id ? 0 : -1,
|
|
94
|
+
onFocus: () => onFocusNode(node.id),
|
|
95
|
+
onClick: () => {
|
|
96
|
+
if (expandOnSelect && expandable) expand(node.id);
|
|
97
|
+
onSelectNode == null ? void 0 : onSelectNode(node);
|
|
98
|
+
},
|
|
99
|
+
children: [
|
|
100
|
+
node.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.icon, "aria-hidden": "true", children: node.icon }) : null,
|
|
101
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
102
|
+
"span",
|
|
103
|
+
{
|
|
104
|
+
className: [styles__default.default.label, wrapLabels ? styles__default.default.labelWrap : styles__default.default.labelTruncate].filter(Boolean).join(" "),
|
|
105
|
+
children: renderLabel ? renderLabel(node) : node.label
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
),
|
|
79
114
|
error ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.errorRow, children: [
|
|
80
115
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: error }),
|
|
81
116
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: styles__default.default.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
|
|
@@ -88,12 +123,17 @@ function TreeNodeRow({
|
|
|
88
123
|
wrapLabels,
|
|
89
124
|
isExpanded,
|
|
90
125
|
toggle,
|
|
126
|
+
expand,
|
|
127
|
+
expandOnSelect,
|
|
91
128
|
resolvedChildren,
|
|
92
129
|
loadingIds,
|
|
93
130
|
errorIds,
|
|
94
131
|
requestLoad,
|
|
95
132
|
loadChildren,
|
|
96
133
|
selectedId,
|
|
134
|
+
currentId,
|
|
135
|
+
focusedId,
|
|
136
|
+
onFocusNode,
|
|
97
137
|
onSelectNode,
|
|
98
138
|
renderLabel
|
|
99
139
|
},
|
|
@@ -6,14 +6,19 @@ type TreeNodeRowProps<T> = {
|
|
|
6
6
|
wrapLabels: boolean;
|
|
7
7
|
isExpanded: (id: string) => boolean;
|
|
8
8
|
toggle: (id: string) => void;
|
|
9
|
+
expand: (id: string) => void;
|
|
10
|
+
expandOnSelect: boolean;
|
|
9
11
|
resolvedChildren: Record<string, TreeNode<T>[]>;
|
|
10
12
|
loadingIds: Set<string>;
|
|
11
13
|
errorIds: Record<string, ReactNode>;
|
|
12
14
|
requestLoad: (node: TreeNode<T>) => void;
|
|
13
15
|
loadChildren?: LoadTreeChildren<T>;
|
|
14
16
|
selectedId?: string;
|
|
17
|
+
currentId?: string;
|
|
18
|
+
focusedId?: string;
|
|
19
|
+
onFocusNode: (id: string) => void;
|
|
15
20
|
onSelectNode?: (node: TreeNode<T>) => void;
|
|
16
21
|
renderLabel?: (node: TreeNode<T>) => ReactNode;
|
|
17
22
|
};
|
|
18
|
-
export declare function TreeNodeRow<T>({ node, depth, wrapLabels, isExpanded, toggle, resolvedChildren, loadingIds, errorIds, requestLoad, loadChildren, selectedId, onSelectNode, renderLabel, }: TreeNodeRowProps<T>): JSX.Element;
|
|
23
|
+
export declare function TreeNodeRow<T>({ node, depth, wrapLabels, isExpanded, toggle, expand, expandOnSelect, resolvedChildren, loadingIds, errorIds, requestLoad, loadChildren, selectedId, currentId, focusedId, onFocusNode, onSelectNode, renderLabel, }: TreeNodeRowProps<T>): JSX.Element;
|
|
19
24
|
export {};
|
|
@@ -10,12 +10,17 @@ function TreeNodeRow({
|
|
|
10
10
|
wrapLabels,
|
|
11
11
|
isExpanded,
|
|
12
12
|
toggle,
|
|
13
|
+
expand,
|
|
14
|
+
expandOnSelect,
|
|
13
15
|
resolvedChildren,
|
|
14
16
|
loadingIds,
|
|
15
17
|
errorIds,
|
|
16
18
|
requestLoad,
|
|
17
19
|
loadChildren,
|
|
18
20
|
selectedId,
|
|
21
|
+
currentId,
|
|
22
|
+
focusedId,
|
|
23
|
+
onFocusNode,
|
|
19
24
|
onSelectNode,
|
|
20
25
|
renderLabel
|
|
21
26
|
}) {
|
|
@@ -27,6 +32,7 @@ function TreeNodeRow({
|
|
|
27
32
|
const loading = loadingIds.has(node.id);
|
|
28
33
|
const error = errorIds[node.id];
|
|
29
34
|
const selected = selectedId !== void 0 && selectedId === node.id;
|
|
35
|
+
const current = currentId !== void 0 && currentId === node.id;
|
|
30
36
|
useEffect(() => {
|
|
31
37
|
if (expanded && children === void 0 && !loading && !error && loadChildren) {
|
|
32
38
|
requestLoad(node);
|
|
@@ -38,38 +44,67 @@ function TreeNodeRow({
|
|
|
38
44
|
role: "treeitem",
|
|
39
45
|
"aria-expanded": expandable ? expanded : void 0,
|
|
40
46
|
"aria-selected": selected,
|
|
47
|
+
"aria-current": current ? "page" : void 0,
|
|
48
|
+
"aria-level": depth + 1,
|
|
49
|
+
"data-tree-node-id": node.id,
|
|
41
50
|
style: { ["--tree-depth"]: depth },
|
|
42
51
|
children: [
|
|
43
|
-
/* @__PURE__ */ jsxs(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
/* @__PURE__ */ jsxs(
|
|
53
|
+
"div",
|
|
54
|
+
{
|
|
55
|
+
className: styles.row,
|
|
56
|
+
"data-tree-row": "true",
|
|
57
|
+
"data-selected": selected || void 0,
|
|
58
|
+
"data-current": current || void 0,
|
|
59
|
+
children: [
|
|
60
|
+
/* @__PURE__ */ jsx(
|
|
61
|
+
"button",
|
|
62
|
+
{
|
|
63
|
+
type: "button",
|
|
64
|
+
className: styles.toggle,
|
|
65
|
+
"data-tree-toggle": "true",
|
|
66
|
+
onClick: () => expandable && toggle(node.id),
|
|
67
|
+
disabled: !expandable,
|
|
68
|
+
"aria-hidden": !expandable,
|
|
69
|
+
tabIndex: -1,
|
|
70
|
+
"aria-label": expanded ? "Luk relationer" : "Vis relationer",
|
|
71
|
+
children: loading ? /* @__PURE__ */ jsx("span", { className: `spin ${styles.spinner}`, children: /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true" }) }) : expandable ? (
|
|
72
|
+
// A resolved node's chevron means "this has children"; an
|
|
73
|
+
// unresolved lazy node's dimmer chevron means "might have
|
|
74
|
+
// children, click to check" - we genuinely don't know yet,
|
|
75
|
+
// and shouldn't visually assert otherwise.
|
|
76
|
+
/* @__PURE__ */ jsx("span", { className: isResolved ? void 0 : styles.toggleUnresolved, children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }) })
|
|
77
|
+
) : /* @__PURE__ */ jsx("span", { className: styles.leafSpacer, "aria-hidden": "true" })
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsxs(
|
|
81
|
+
"button",
|
|
82
|
+
{
|
|
83
|
+
type: "button",
|
|
84
|
+
className: styles.labelButton,
|
|
85
|
+
"data-tree-focus-target": "true",
|
|
86
|
+
"data-tree-focus-node-id": node.id,
|
|
87
|
+
tabIndex: focusedId === node.id ? 0 : -1,
|
|
88
|
+
onFocus: () => onFocusNode(node.id),
|
|
89
|
+
onClick: () => {
|
|
90
|
+
if (expandOnSelect && expandable) expand(node.id);
|
|
91
|
+
onSelectNode == null ? void 0 : onSelectNode(node);
|
|
92
|
+
},
|
|
93
|
+
children: [
|
|
94
|
+
node.icon ? /* @__PURE__ */ jsx("span", { className: styles.icon, "aria-hidden": "true", children: node.icon }) : null,
|
|
95
|
+
/* @__PURE__ */ jsx(
|
|
96
|
+
"span",
|
|
97
|
+
{
|
|
98
|
+
className: [styles.label, wrapLabels ? styles.labelWrap : styles.labelTruncate].filter(Boolean).join(" "),
|
|
99
|
+
children: renderLabel ? renderLabel(node) : node.label
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
),
|
|
73
108
|
error ? /* @__PURE__ */ jsxs("div", { className: styles.errorRow, children: [
|
|
74
109
|
/* @__PURE__ */ jsx("span", { children: error }),
|
|
75
110
|
/* @__PURE__ */ jsx("button", { type: "button", className: styles.retryButton, onClick: () => requestLoad(node), children: "Pr\xF8v igen" })
|
|
@@ -82,12 +117,17 @@ function TreeNodeRow({
|
|
|
82
117
|
wrapLabels,
|
|
83
118
|
isExpanded,
|
|
84
119
|
toggle,
|
|
120
|
+
expand,
|
|
121
|
+
expandOnSelect,
|
|
85
122
|
resolvedChildren,
|
|
86
123
|
loadingIds,
|
|
87
124
|
errorIds,
|
|
88
125
|
requestLoad,
|
|
89
126
|
loadChildren,
|
|
90
127
|
selectedId,
|
|
128
|
+
currentId,
|
|
129
|
+
focusedId,
|
|
130
|
+
onFocusNode,
|
|
91
131
|
onSelectNode,
|
|
92
132
|
renderLabel
|
|
93
133
|
},
|
|
@@ -14,19 +14,29 @@ var styles__default = /*#__PURE__*/_interopDefault(styles);
|
|
|
14
14
|
|
|
15
15
|
function TreeView({
|
|
16
16
|
data,
|
|
17
|
+
size = "sm",
|
|
17
18
|
loadChildren,
|
|
18
19
|
defaultExpandedIds,
|
|
19
20
|
expandedIds,
|
|
20
21
|
onExpandedIdsChange,
|
|
21
22
|
selectedId,
|
|
23
|
+
currentId,
|
|
22
24
|
onSelectNode,
|
|
25
|
+
expandOnSelect = false,
|
|
23
26
|
renderLabel,
|
|
24
27
|
wrapLabels = true,
|
|
25
28
|
emptyMessage = "Intet at vise.",
|
|
26
29
|
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
27
30
|
className,
|
|
31
|
+
"aria-label": ariaLabel,
|
|
28
32
|
...rest
|
|
29
33
|
}) {
|
|
34
|
+
const [focusedId, setFocusedId] = react.useState(
|
|
35
|
+
() => {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
return (_b = currentId != null ? currentId : selectedId) != null ? _b : (_a = data[0]) == null ? void 0 : _a.id;
|
|
38
|
+
}
|
|
39
|
+
);
|
|
30
40
|
const { isExpanded, toggle, expand } = useTreeExpansion.useTreeExpansion(
|
|
31
41
|
defaultExpandedIds,
|
|
32
42
|
expandedIds,
|
|
@@ -60,28 +70,106 @@ function TreeView({
|
|
|
60
70
|
},
|
|
61
71
|
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
62
72
|
);
|
|
73
|
+
const handleTreeKeyDown = react.useCallback((event) => {
|
|
74
|
+
var _a, _b, _c, _d;
|
|
75
|
+
const target = event.target;
|
|
76
|
+
const focusedLabel = target.closest('[data-tree-focus-target="true"]');
|
|
77
|
+
if (!focusedLabel) return;
|
|
78
|
+
const visibleItems = Array.from(
|
|
79
|
+
event.currentTarget.querySelectorAll('[data-tree-focus-target="true"]')
|
|
80
|
+
);
|
|
81
|
+
const treeItem = focusedLabel.closest('[role="treeitem"]');
|
|
82
|
+
if (!treeItem) return;
|
|
83
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
84
|
+
const index = visibleItems.indexOf(focusedLabel);
|
|
85
|
+
const nextIndex = Math.max(
|
|
86
|
+
0,
|
|
87
|
+
Math.min(index + (event.key === "ArrowDown" ? 1 : -1), visibleItems.length - 1)
|
|
88
|
+
);
|
|
89
|
+
if (nextIndex !== index) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
(_a = visibleItems[nextIndex]) == null ? void 0 : _a.focus();
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (event.key === "ArrowRight") {
|
|
96
|
+
const expanded = treeItem.getAttribute("aria-expanded") === "true";
|
|
97
|
+
if (treeItem.getAttribute("aria-expanded") === "false") {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
(_b = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _b.click();
|
|
100
|
+
} else if (expanded) {
|
|
101
|
+
const firstChild = treeItem.querySelector(
|
|
102
|
+
':scope > [role="group"] [data-tree-focus-target="true"]'
|
|
103
|
+
);
|
|
104
|
+
if (firstChild) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
firstChild.focus();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (event.key === "ArrowLeft") {
|
|
112
|
+
if (treeItem.getAttribute("aria-expanded") === "true") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
(_c = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _c.click();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const parentTreeItem = (_d = treeItem.parentElement) == null ? void 0 : _d.closest('[role="treeitem"]');
|
|
118
|
+
const parentLabel = parentTreeItem == null ? void 0 : parentTreeItem.querySelector(
|
|
119
|
+
':scope > [data-tree-row] [data-tree-focus-target="true"]'
|
|
120
|
+
);
|
|
121
|
+
if (parentLabel) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
parentLabel.focus();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
63
127
|
if (data.length === 0) {
|
|
64
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
128
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
className: [styles__default.default.emptyState, className].filter(Boolean).join(" "),
|
|
132
|
+
"aria-label": ariaLabel,
|
|
133
|
+
...rest,
|
|
134
|
+
children: emptyMessage
|
|
135
|
+
}
|
|
136
|
+
);
|
|
65
137
|
}
|
|
66
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
67
|
-
|
|
138
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
139
|
+
"div",
|
|
68
140
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
role: "tree",
|
|
142
|
+
"data-size": size,
|
|
143
|
+
"aria-label": ariaLabel,
|
|
144
|
+
className: [styles__default.default.tree, className].filter(Boolean).join(" "),
|
|
145
|
+
onKeyDown: handleTreeKeyDown,
|
|
146
|
+
...rest,
|
|
147
|
+
children: data.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
148
|
+
TreeNodeRow.TreeNodeRow,
|
|
149
|
+
{
|
|
150
|
+
node,
|
|
151
|
+
depth: 0,
|
|
152
|
+
wrapLabels,
|
|
153
|
+
isExpanded,
|
|
154
|
+
toggle,
|
|
155
|
+
expand,
|
|
156
|
+
expandOnSelect,
|
|
157
|
+
resolvedChildren,
|
|
158
|
+
loadingIds,
|
|
159
|
+
errorIds,
|
|
160
|
+
requestLoad,
|
|
161
|
+
loadChildren,
|
|
162
|
+
selectedId,
|
|
163
|
+
currentId,
|
|
164
|
+
focusedId,
|
|
165
|
+
onFocusNode: setFocusedId,
|
|
166
|
+
onSelectNode,
|
|
167
|
+
renderLabel
|
|
168
|
+
},
|
|
169
|
+
node.id
|
|
170
|
+
))
|
|
171
|
+
}
|
|
172
|
+
);
|
|
85
173
|
}
|
|
86
174
|
|
|
87
175
|
Object.defineProperty(exports, "collectExpandableIds", {
|
|
@@ -17,4 +17,4 @@ export { collectExpandableIds, findAncestorIds } from './utils';
|
|
|
17
17
|
* useTreeExpansion), and it can be left uncontrolled or owned by the
|
|
18
18
|
* caller via `expandedIds`/`onExpandedIdsChange`.
|
|
19
19
|
*/
|
|
20
|
-
export declare function TreeView<T = unknown>({ data, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, onSelectNode, renderLabel, wrapLabels, emptyMessage, loadErrorMessage, className, ...rest }: TreeViewProps<T>): JSX.Element;
|
|
20
|
+
export declare function TreeView<T = unknown>({ data, size, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, currentId, onSelectNode, expandOnSelect, renderLabel, wrapLabels, emptyMessage, loadErrorMessage, className, 'aria-label': ariaLabel, ...rest }: TreeViewProps<T>): JSX.Element;
|
|
@@ -8,19 +8,29 @@ export { collectExpandableIds, findAncestorIds } from './utils';
|
|
|
8
8
|
|
|
9
9
|
function TreeView({
|
|
10
10
|
data,
|
|
11
|
+
size = "sm",
|
|
11
12
|
loadChildren,
|
|
12
13
|
defaultExpandedIds,
|
|
13
14
|
expandedIds,
|
|
14
15
|
onExpandedIdsChange,
|
|
15
16
|
selectedId,
|
|
17
|
+
currentId,
|
|
16
18
|
onSelectNode,
|
|
19
|
+
expandOnSelect = false,
|
|
17
20
|
renderLabel,
|
|
18
21
|
wrapLabels = true,
|
|
19
22
|
emptyMessage = "Intet at vise.",
|
|
20
23
|
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
21
24
|
className,
|
|
25
|
+
"aria-label": ariaLabel,
|
|
22
26
|
...rest
|
|
23
27
|
}) {
|
|
28
|
+
const [focusedId, setFocusedId] = useState(
|
|
29
|
+
() => {
|
|
30
|
+
var _a, _b;
|
|
31
|
+
return (_b = currentId != null ? currentId : selectedId) != null ? _b : (_a = data[0]) == null ? void 0 : _a.id;
|
|
32
|
+
}
|
|
33
|
+
);
|
|
24
34
|
const { isExpanded, toggle, expand } = useTreeExpansion(
|
|
25
35
|
defaultExpandedIds,
|
|
26
36
|
expandedIds,
|
|
@@ -54,28 +64,106 @@ function TreeView({
|
|
|
54
64
|
},
|
|
55
65
|
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
56
66
|
);
|
|
67
|
+
const handleTreeKeyDown = useCallback((event) => {
|
|
68
|
+
var _a, _b, _c, _d;
|
|
69
|
+
const target = event.target;
|
|
70
|
+
const focusedLabel = target.closest('[data-tree-focus-target="true"]');
|
|
71
|
+
if (!focusedLabel) return;
|
|
72
|
+
const visibleItems = Array.from(
|
|
73
|
+
event.currentTarget.querySelectorAll('[data-tree-focus-target="true"]')
|
|
74
|
+
);
|
|
75
|
+
const treeItem = focusedLabel.closest('[role="treeitem"]');
|
|
76
|
+
if (!treeItem) return;
|
|
77
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
78
|
+
const index = visibleItems.indexOf(focusedLabel);
|
|
79
|
+
const nextIndex = Math.max(
|
|
80
|
+
0,
|
|
81
|
+
Math.min(index + (event.key === "ArrowDown" ? 1 : -1), visibleItems.length - 1)
|
|
82
|
+
);
|
|
83
|
+
if (nextIndex !== index) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
(_a = visibleItems[nextIndex]) == null ? void 0 : _a.focus();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (event.key === "ArrowRight") {
|
|
90
|
+
const expanded = treeItem.getAttribute("aria-expanded") === "true";
|
|
91
|
+
if (treeItem.getAttribute("aria-expanded") === "false") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
(_b = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _b.click();
|
|
94
|
+
} else if (expanded) {
|
|
95
|
+
const firstChild = treeItem.querySelector(
|
|
96
|
+
':scope > [role="group"] [data-tree-focus-target="true"]'
|
|
97
|
+
);
|
|
98
|
+
if (firstChild) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
firstChild.focus();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (event.key === "ArrowLeft") {
|
|
106
|
+
if (treeItem.getAttribute("aria-expanded") === "true") {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
(_c = treeItem.querySelector('[data-tree-toggle="true"]')) == null ? void 0 : _c.click();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const parentTreeItem = (_d = treeItem.parentElement) == null ? void 0 : _d.closest('[role="treeitem"]');
|
|
112
|
+
const parentLabel = parentTreeItem == null ? void 0 : parentTreeItem.querySelector(
|
|
113
|
+
':scope > [data-tree-row] [data-tree-focus-target="true"]'
|
|
114
|
+
);
|
|
115
|
+
if (parentLabel) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
parentLabel.focus();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, []);
|
|
57
121
|
if (data.length === 0) {
|
|
58
|
-
return /* @__PURE__ */ jsx(
|
|
122
|
+
return /* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
className: [styles.emptyState, className].filter(Boolean).join(" "),
|
|
126
|
+
"aria-label": ariaLabel,
|
|
127
|
+
...rest,
|
|
128
|
+
children: emptyMessage
|
|
129
|
+
}
|
|
130
|
+
);
|
|
59
131
|
}
|
|
60
|
-
return /* @__PURE__ */ jsx(
|
|
61
|
-
|
|
132
|
+
return /* @__PURE__ */ jsx(
|
|
133
|
+
"div",
|
|
62
134
|
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
135
|
+
role: "tree",
|
|
136
|
+
"data-size": size,
|
|
137
|
+
"aria-label": ariaLabel,
|
|
138
|
+
className: [styles.tree, className].filter(Boolean).join(" "),
|
|
139
|
+
onKeyDown: handleTreeKeyDown,
|
|
140
|
+
...rest,
|
|
141
|
+
children: data.map((node) => /* @__PURE__ */ jsx(
|
|
142
|
+
TreeNodeRow,
|
|
143
|
+
{
|
|
144
|
+
node,
|
|
145
|
+
depth: 0,
|
|
146
|
+
wrapLabels,
|
|
147
|
+
isExpanded,
|
|
148
|
+
toggle,
|
|
149
|
+
expand,
|
|
150
|
+
expandOnSelect,
|
|
151
|
+
resolvedChildren,
|
|
152
|
+
loadingIds,
|
|
153
|
+
errorIds,
|
|
154
|
+
requestLoad,
|
|
155
|
+
loadChildren,
|
|
156
|
+
selectedId,
|
|
157
|
+
currentId,
|
|
158
|
+
focusedId,
|
|
159
|
+
onFocusNode: setFocusedId,
|
|
160
|
+
onSelectNode,
|
|
161
|
+
renderLabel
|
|
162
|
+
},
|
|
163
|
+
node.id
|
|
164
|
+
))
|
|
165
|
+
}
|
|
166
|
+
);
|
|
79
167
|
}
|
|
80
168
|
|
|
81
169
|
export { TreeView };
|
|
@@ -21,8 +21,44 @@
|
|
|
21
21
|
border-radius: var(--border-radius-default);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
.tree[data-size='md'] .row {
|
|
25
|
+
min-block-size: calc(var(--spacing-md) + var(--spacing-sm));
|
|
26
|
+
padding-inline-start: 0;
|
|
27
|
+
padding-inline-end: calc(var(--spacing-xs) - var(--spacing-2xs));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tree[data-size='md'] .toggle,
|
|
31
|
+
.tree[data-size='md'] .labelButton {
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
min-block-size: calc(var(--spacing-md) + var(--spacing-sm));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tree[data-size='md'] .toggle {
|
|
37
|
+
justify-content: center;
|
|
38
|
+
align-self: stretch;
|
|
39
|
+
--tree-indent: calc(
|
|
40
|
+
var(--tree-depth) * (var(--spacing-md) + 2 * var(--spacing-2xs)) + var(--spacing-xs) -
|
|
41
|
+
var(--spacing-2xs)
|
|
42
|
+
);
|
|
43
|
+
inline-size: calc(var(--tree-indent) + var(--spacing-lg) + var(--spacing-sm));
|
|
44
|
+
padding-inline-start: var(--tree-indent);
|
|
45
|
+
padding-inline-end: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.tree[data-size='md'] .errorRow {
|
|
49
|
+
padding-inline-start: calc(
|
|
50
|
+
(var(--tree-depth) + 1) * (var(--spacing-md) + 2 * var(--spacing-2xs)) + var(--spacing-xs) -
|
|
51
|
+
var(--spacing-2xs)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
.row[data-selected='true'] {
|
|
25
|
-
background:
|
|
56
|
+
background: var(--color-status-info-bg);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.row[data-current='true'] {
|
|
60
|
+
background: var(--color-bg-surface-subtle);
|
|
61
|
+
font-weight: 600;
|
|
26
62
|
}
|
|
27
63
|
|
|
28
64
|
.toggle,
|
|
@@ -82,13 +118,21 @@
|
|
|
82
118
|
padding-inline: var(--spacing-xs);
|
|
83
119
|
}
|
|
84
120
|
|
|
85
|
-
.row:hover
|
|
86
|
-
|
|
121
|
+
.row:hover {
|
|
122
|
+
background: var(--color-bg-hover-subtle);
|
|
123
|
+
}
|
|
124
|
+
|
|
87
125
|
.toggle:focus-visible,
|
|
88
126
|
.labelButton:focus-visible {
|
|
89
127
|
background: var(--color-bg-hover-subtle);
|
|
90
128
|
}
|
|
91
129
|
|
|
130
|
+
.toggle:not(:disabled):hover,
|
|
131
|
+
.toggle:not(:disabled):focus-visible {
|
|
132
|
+
background: var(--color-bg-pressed-subtle);
|
|
133
|
+
color: var(--color-fg-default);
|
|
134
|
+
}
|
|
135
|
+
|
|
92
136
|
.toggle:focus-visible,
|
|
93
137
|
.labelButton:focus-visible {
|
|
94
138
|
outline: none;
|
|
@@ -23,6 +23,7 @@ export type TreeNode<T = unknown> = {
|
|
|
23
23
|
/** Fetches one node's children. Called once per node, the first time it's expanded (and again on retry after a failed load). Presence of this prop on TreeView is what switches it into lazy mode. */
|
|
24
24
|
export type LoadTreeChildren<T = unknown> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
|
|
25
25
|
export type TreeViewProps<T = unknown> = {
|
|
26
|
+
size?: 'sm' | 'md';
|
|
26
27
|
/** Top-level nodes. In eager mode every node's full subtree must already be present via `children`. In lazy mode only the nodes you already know about need to be here - deeper levels arrive via `loadChildren`. */
|
|
27
28
|
data: TreeNode<T>[];
|
|
28
29
|
/** Omit for eager mode. Provide for lazy mode. */
|
|
@@ -33,7 +34,11 @@ export type TreeViewProps<T = unknown> = {
|
|
|
33
34
|
expandedIds?: string[];
|
|
34
35
|
onExpandedIdsChange?: (ids: string[]) => void;
|
|
35
36
|
selectedId?: string;
|
|
37
|
+
/** Marks the page's current record independently from the preview selection. */
|
|
38
|
+
currentId?: string;
|
|
36
39
|
onSelectNode?: (node: TreeNode<T>) => void;
|
|
40
|
+
/** Expand branches when their labels are activated, without collapsing already-open branches. */
|
|
41
|
+
expandOnSelect?: boolean;
|
|
37
42
|
/** Customize how a node's row content renders; defaults to `node.icon` + `node.label`. */
|
|
38
43
|
renderLabel?: (node: TreeNode<T>) => ReactNode;
|
|
39
44
|
/** Allows node labels to wrap onto multiple lines. Set to false to truncate them to one line. */
|
|
@@ -42,5 +47,6 @@ export type TreeViewProps<T = unknown> = {
|
|
|
42
47
|
/** Formats the error shown under a node whose `loadChildren` call rejected. */
|
|
43
48
|
loadErrorMessage?: (error: unknown, node: TreeNode<T>) => ReactNode;
|
|
44
49
|
className?: string;
|
|
50
|
+
'aria-label'?: string;
|
|
45
51
|
'data-testid'?: string;
|
|
46
52
|
};
|