react-simple-dock 0.2.2 → 0.2.5
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/README.md +37 -2
- package/index.css +18 -10
- package/index.js +158 -55
- package/index.test.d.ts +1 -0
- package/index.test.js +42 -0
- package/package.json +1 -1
- package/types.d.ts +5 -0
- package/utils.d.ts +4 -1
- package/utils.js +150 -3
- package/utils.test.d.ts +1 -0
- package/utils.test.js +65 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/pret-simple-dock/)
|
|
4
4
|
[](https://www.npmjs.com/package/react-simple-dock)
|
|
5
|
-
[](https://github.com/percevalw/react-simple-dock/actions/workflows/test.yml)
|
|
6
6
|
|
|
7
7
|
A set of React components to create a dockable interface, allowing to arrange and resize tabs.
|
|
8
8
|
|
|
@@ -69,6 +69,41 @@ const App = () => (
|
|
|
69
69
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(<App />);
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Dynamic tab placement
|
|
73
|
+
|
|
74
|
+
When children are added/removed dynamically, `Layout` now preserves user-arranged layout and applies `place` only
|
|
75
|
+
when a panel appears for the first time in the current layout.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<Layout
|
|
79
|
+
defaultConfig={{
|
|
80
|
+
kind: "row",
|
|
81
|
+
children: [
|
|
82
|
+
{ kind: "leaf", tabs: ["Panel 1", "@anchor-1"] },
|
|
83
|
+
{ kind: "leaf", tabs: ["Panel 2"] },
|
|
84
|
+
],
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<Panel key="Panel 1">...</Panel>
|
|
88
|
+
<Panel key="Panel 2">...</Panel>
|
|
89
|
+
|
|
90
|
+
{/* Insert into the same tab list */}
|
|
91
|
+
<Panel key="Panel 3" place={{ where: "after-tab", of: "Panel 1" }}>
|
|
92
|
+
...
|
|
93
|
+
</Panel>
|
|
94
|
+
|
|
95
|
+
{/* Insert as a sibling leaf around the target leaf */}
|
|
96
|
+
<Panel key="Panel 4" place={{ where: "right", of: "@anchor-1" }}>
|
|
97
|
+
...
|
|
98
|
+
</Panel>
|
|
99
|
+
</Layout>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `where` supports: `before-tab`, `after-tab`, `first-tab`, `last-tab`, `left`, `right`, `top`, `bottom`.
|
|
103
|
+
- `of` references panel keys (or anchor tabs in `defaultConfig`, such as `@anchor-1`).
|
|
104
|
+
- If `of` is missing, placement falls back to the first tab currently present in the layout.
|
|
105
|
+
- Tabs prefixed with `@` are treated as virtual anchors: preserved for placement, but never rendered as visible tabs or leaves.
|
|
106
|
+
|
|
72
107
|
## Development
|
|
73
108
|
|
|
74
109
|
### Installation
|
|
@@ -111,7 +146,7 @@ pip install pret
|
|
|
111
146
|
If you have changed the signature of the components, you will need to update the python stubs.
|
|
112
147
|
|
|
113
148
|
```bash
|
|
114
|
-
pret stub . SimpleDock
|
|
149
|
+
pret stub . SimpleDock pret_simple_dock/__init__.py
|
|
115
150
|
```
|
|
116
151
|
|
|
117
152
|
To build the python library and make it available in your environment:
|
package/index.css
CHANGED
|
@@ -35,7 +35,6 @@ body[data-jp-theme-light] {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.resize-border {
|
|
38
|
-
position: absolute;
|
|
39
38
|
background-color: var(--sd-background-color, #fff);
|
|
40
39
|
opacity: 0;
|
|
41
40
|
z-index: 10;
|
|
@@ -43,21 +42,19 @@ body[data-jp-theme-light] {
|
|
|
43
42
|
|
|
44
43
|
.resize-border:hover {
|
|
45
44
|
opacity: 0.8;
|
|
45
|
+
border: 1px solid var(--sd-background-color, #fff);
|
|
46
|
+
box-sizing: border-box;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
.resize-border.bottom {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
right: 0;
|
|
52
|
-
height: calc((var(--sd-grid-gap)));
|
|
50
|
+
height: 100%;
|
|
51
|
+
width: 100%;
|
|
53
52
|
cursor: row-resize;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
.resize-border.right {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
right: calc(0px - var(--sd-grid-gap));
|
|
60
|
-
width: calc((var(--sd-grid-gap)));
|
|
56
|
+
height: 100%;
|
|
57
|
+
width: 100%;
|
|
61
58
|
cursor: col-resize;
|
|
62
59
|
}
|
|
63
60
|
|
|
@@ -91,6 +88,7 @@ body[data-jp-theme-light] {
|
|
|
91
88
|
height: 100%;
|
|
92
89
|
width: 100%;
|
|
93
90
|
background: var(--sd-background-color);
|
|
91
|
+
overflow-x: hidden;
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
.panel.leaf {
|
|
@@ -100,7 +98,6 @@ body[data-jp-theme-light] {
|
|
|
100
98
|
.row > .panel-content,
|
|
101
99
|
.column > .panel-content {
|
|
102
100
|
display: grid;
|
|
103
|
-
grid-gap: var(--sd-grid-gap);
|
|
104
101
|
position: absolute;
|
|
105
102
|
left: 0;
|
|
106
103
|
top: 0;
|
|
@@ -125,12 +122,23 @@ body[data-jp-theme-light] {
|
|
|
125
122
|
overflow: auto;
|
|
126
123
|
}
|
|
127
124
|
|
|
125
|
+
.leaf > div:nth-child(1) {
|
|
126
|
+
padding-right: 0px;
|
|
127
|
+
}
|
|
128
|
+
|
|
128
129
|
/* TAB HEADER */
|
|
129
130
|
|
|
130
131
|
.tab-header {
|
|
131
132
|
display: flex;
|
|
132
133
|
flex-direction: row;
|
|
133
134
|
height: var(--sd-header-height);
|
|
135
|
+
width: 100%;
|
|
136
|
+
overflow: auto;
|
|
137
|
+
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
|
138
|
+
scrollbar-width: none; /* Firefox, Safari 18.2+, Chromium 121+ */
|
|
139
|
+
}
|
|
140
|
+
.tab-header::-webkit-scrollbar {
|
|
141
|
+
display: none; /* Older Safari and Chromium */
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
.tab-header-border {
|
package/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { DndProvider, useDrag, useDrop, useDragDropManager } from "react-dnd";
|
|
4
4
|
import "./index.css";
|
|
5
|
-
import { filterPanels, movePanel } from "./utils";
|
|
5
|
+
import { filterPanels, getLayoutTabs, insertPanel, isAnchorTab, movePanel } from "./utils";
|
|
6
6
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
7
7
|
function useForceUpdate() {
|
|
8
8
|
const [value, setValue] = useState(0); // integer state
|
|
@@ -14,9 +14,13 @@ const isMobileDevice = () => {
|
|
|
14
14
|
return /Mobi|Android|iPhone|iPad|iPod|Windows Phone|IEMobile|Opera Mini/i.test(navigator.userAgent || "");
|
|
15
15
|
};
|
|
16
16
|
const getPanelElementMaxHeaderHeight = (config, panelElements) => {
|
|
17
|
-
// If we have a leaf, then the header height is the
|
|
17
|
+
// If we have a leaf, then the header height is the tab header height.
|
|
18
18
|
if (config.kind === "leaf") {
|
|
19
|
-
|
|
19
|
+
const element = panelElements.get(config);
|
|
20
|
+
if (!element) {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
return element.children[0]?.offsetHeight || 0;
|
|
20
24
|
}
|
|
21
25
|
// If we have a row, then the header height is the max of each child's max header height
|
|
22
26
|
else if (config.kind === "row") {
|
|
@@ -47,7 +51,42 @@ function normalizeConfig(config, siblingsCount, depth = 0, default_kind = depth
|
|
|
47
51
|
const children = container.children.map((child) => normalizeConfig(child, container.children.length, depth + 1, kind === "row" ? "column" : "row"));
|
|
48
52
|
return { kind, children, size, nesting: container.nesting };
|
|
49
53
|
}
|
|
50
|
-
const
|
|
54
|
+
const getPanelName = (panel, index) => panel.props.name || (panel.key !== null ? panel.key.toString().slice(2) : `unnamed-${index}`);
|
|
55
|
+
const getRenderableTabs = (tabs) => tabs.filter((tab) => !isAnchorTab(tab));
|
|
56
|
+
const isRenderableConfig = (config) => {
|
|
57
|
+
if (config.kind === "leaf") {
|
|
58
|
+
return getRenderableTabs(config.tabs).length > 0;
|
|
59
|
+
}
|
|
60
|
+
return config.children.some((child) => isRenderableConfig(child));
|
|
61
|
+
};
|
|
62
|
+
const getFirstRenderableTab = (config) => {
|
|
63
|
+
if (config.kind === "leaf") {
|
|
64
|
+
return getRenderableTabs(config.tabs)[0] || null;
|
|
65
|
+
}
|
|
66
|
+
const firstRenderableChild = config.children.find((child) => isRenderableConfig(child));
|
|
67
|
+
if (!firstRenderableChild) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return getFirstRenderableTab(firstRenderableChild);
|
|
71
|
+
};
|
|
72
|
+
const getLastRenderableTab = (config) => {
|
|
73
|
+
if (config.kind === "leaf") {
|
|
74
|
+
const tabs = getRenderableTabs(config.tabs);
|
|
75
|
+
return tabs[tabs.length - 1] || null;
|
|
76
|
+
}
|
|
77
|
+
const children = config.children.filter((child) => isRenderableConfig(child));
|
|
78
|
+
if (!children.length) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return getLastRenderableTab(children[children.length - 1]);
|
|
82
|
+
};
|
|
83
|
+
const getRenderableTabCount = (config) => {
|
|
84
|
+
if (config.kind === "leaf") {
|
|
85
|
+
return getRenderableTabs(config.tabs).length;
|
|
86
|
+
}
|
|
87
|
+
return config.children.reduce((count, child) => count + getRenderableTabCount(child), 0);
|
|
88
|
+
};
|
|
89
|
+
const TabHandle = ({ name, visible, onClick, children, }) => {
|
|
51
90
|
const getItem = () => ({
|
|
52
91
|
name,
|
|
53
92
|
handleElement: targetRef.current,
|
|
@@ -62,13 +101,15 @@ const TabHandle = ({ name, index, visible, onClick, children, }) => {
|
|
|
62
101
|
return (_jsx("div", { className: `tab-handle tab-handle__${visible ? "visible" : "hidden"}`, ref: (element) => {
|
|
63
102
|
dragRef(element);
|
|
64
103
|
targetRef.current = element;
|
|
65
|
-
}, onClick: () => onClick(
|
|
104
|
+
}, onClick: () => onClick(name), children: children }));
|
|
66
105
|
};
|
|
67
106
|
const TabHeader = ({ config, onClick, leaves, }) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
const renderableTabs = getRenderableTabs(config.tabs);
|
|
108
|
+
const selectedTab = config.tabs[config.tabIndex];
|
|
109
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "tab-header", children: [renderableTabs
|
|
110
|
+
.map((tab) => [
|
|
111
|
+
_jsx("div", { className: "tab-placeholder", style: { width: "0px" }, "data-panel-name": tab }, `${tab}-placeholder`),
|
|
112
|
+
_jsx(TabHandle, { name: tab, visible: selectedTab === tab, onClick: onClick, children: leaves[tab]?.header || tab }, tab),
|
|
72
113
|
])
|
|
73
114
|
.flat(), _jsx("div", { className: "tab-placeholder" })] }), _jsx("div", { className: "tab-header-border" }), _jsx("div", { className: "tab-header-bottom" })] }));
|
|
74
115
|
};
|
|
@@ -76,6 +117,12 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
76
117
|
const startEvent = useRef(null);
|
|
77
118
|
const savedSizes = useRef([]);
|
|
78
119
|
const forceUpdate = useForceUpdate();
|
|
120
|
+
const renderableChildren = config.kind === "leaf"
|
|
121
|
+
? []
|
|
122
|
+
: config.children
|
|
123
|
+
.filter((child) => isRenderableConfig(child))
|
|
124
|
+
.map((child, childIndex) => ({ child, childIndex }));
|
|
125
|
+
console.log("RENDERABLE CHILDREN", renderableChildren);
|
|
79
126
|
useEffect(() => {
|
|
80
127
|
const handleMouseUp = (e) => {
|
|
81
128
|
document.removeEventListener("mousemove", startEvent.current?.handleMouseMove);
|
|
@@ -105,17 +152,21 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
105
152
|
const { clientX, clientY } = e;
|
|
106
153
|
// get Panel top left corner
|
|
107
154
|
const { top, left } = (startEvent.current?.target).parentElement.getBoundingClientRect();
|
|
155
|
+
const parent = (startEvent.current?.target).parentElement;
|
|
156
|
+
const containerWidth = parent.offsetWidth;
|
|
157
|
+
const containerHeight = parent.offsetHeight;
|
|
108
158
|
let ratio;
|
|
109
159
|
if (startEvent.current.side === "bottom") {
|
|
110
|
-
ratio = (clientY - top) /
|
|
160
|
+
ratio = containerHeight > 0 ? (clientY - top) / containerHeight : 0;
|
|
111
161
|
}
|
|
112
162
|
else if (startEvent.current.side === "right") {
|
|
113
|
-
ratio = (clientX - left) /
|
|
163
|
+
ratio = containerWidth > 0 ? (clientX - left) / containerWidth : 0;
|
|
114
164
|
}
|
|
115
165
|
else {
|
|
116
166
|
return;
|
|
117
167
|
}
|
|
118
|
-
|
|
168
|
+
const boundedRatio = Math.max(0, Math.min(1, ratio));
|
|
169
|
+
onResize && onResize(boundedRatio, index, startEvent.current.target);
|
|
119
170
|
}, [onResize, index]);
|
|
120
171
|
const handleMouseDown = (e, side) => {
|
|
121
172
|
if (startEvent.current) {
|
|
@@ -126,8 +177,6 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
126
177
|
e.preventDefault();
|
|
127
178
|
saveSizes && saveSizes();
|
|
128
179
|
startEvent.current = {
|
|
129
|
-
width: e.clientX - left,
|
|
130
|
-
height: e.clientY - top,
|
|
131
180
|
target: e.currentTarget,
|
|
132
181
|
side: side,
|
|
133
182
|
handleMouseMove,
|
|
@@ -138,53 +187,82 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
138
187
|
if (config.kind === "row") {
|
|
139
188
|
// return {gridTemplateColumns: config.children.map(c => `calc((${100}% - var(--grid-gap) * ${config.children.length - 1}) * ${c.size / 100})`).join(" ")};
|
|
140
189
|
return {
|
|
141
|
-
gridTemplateColumns:
|
|
190
|
+
gridTemplateColumns: renderableChildren.map(({ child, childIndex }) => (childIndex == 0 ? `${child.size}fr` : `7px ${child.size}fr`)).join(" "),
|
|
142
191
|
};
|
|
143
192
|
}
|
|
144
193
|
if (config.kind === "column") {
|
|
145
194
|
// return {gridTemplateRows: config.children.map(c => `calc((${100}% - var(--grid-gap) * ${config.children.length - 1}) * ${c.size / 100})`).join(" ")};
|
|
146
195
|
return {
|
|
147
|
-
gridTemplateRows:
|
|
196
|
+
gridTemplateRows: renderableChildren.map(({ child, childIndex }) => (childIndex == 0 ? `${child.size}fr` : `7px ${child.size}fr`)).join(" "),
|
|
148
197
|
};
|
|
149
198
|
}
|
|
150
199
|
};
|
|
151
200
|
const handleResize = useCallback((ratio, idx, target) => {
|
|
152
201
|
if (config.kind === "leaf")
|
|
153
202
|
return;
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
const current = renderableChildren[idx];
|
|
204
|
+
const next = renderableChildren[idx + 1];
|
|
205
|
+
if (!current || !next) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
156
208
|
const total = savedSizes.current.reduce((a, b) => a + b, 0);
|
|
209
|
+
if (total <= 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sizeBefore = savedSizes.current.slice(0, idx).reduce((a, b) => a + b, 0);
|
|
213
|
+
const pairTotal = savedSizes.current[idx] + savedSizes.current[idx + 1];
|
|
214
|
+
let size = ratio * total - sizeBefore;
|
|
215
|
+
size = Math.max(0, Math.min(pairTotal, size));
|
|
216
|
+
let nextSize = pairTotal - size;
|
|
157
217
|
if (config.kind === "column") {
|
|
158
|
-
const headerHeightBefore = getPanelElementMaxHeaderHeight(
|
|
159
|
-
const headerHeightAfter = getPanelElementMaxHeaderHeight(
|
|
218
|
+
const headerHeightBefore = getPanelElementMaxHeaderHeight(current.child, panelElements);
|
|
219
|
+
const headerHeightAfter = getPanelElementMaxHeaderHeight(next.child, panelElements);
|
|
160
220
|
const parentHeight = panelContentRef.current.offsetHeight;
|
|
221
|
+
if (parentHeight <= 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
161
224
|
if ((size * parentHeight) / total < headerHeightBefore) {
|
|
162
225
|
size = (headerHeightBefore / parentHeight) * total;
|
|
163
|
-
|
|
226
|
+
size = Math.max(0, Math.min(pairTotal, size));
|
|
227
|
+
nextSize = pairTotal - size;
|
|
164
228
|
}
|
|
165
229
|
else if ((nextSize * parentHeight) / total < headerHeightAfter) {
|
|
166
|
-
nextSize = (headerHeightAfter / parentHeight) * total;
|
|
167
|
-
size =
|
|
230
|
+
nextSize = Math.max(0, Math.min(pairTotal, (headerHeightAfter / parentHeight) * total));
|
|
231
|
+
size = pairTotal - nextSize;
|
|
168
232
|
}
|
|
169
233
|
}
|
|
170
|
-
|
|
171
|
-
|
|
234
|
+
current.child.size = size;
|
|
235
|
+
next.child.size = nextSize;
|
|
172
236
|
Object.assign(panelContentRef.current.style, makeStyle());
|
|
173
|
-
}, [config]);
|
|
237
|
+
}, [config, renderableChildren]);
|
|
174
238
|
const handleSaveSizes = useCallback(() => {
|
|
175
239
|
if (config.kind === "leaf")
|
|
176
240
|
return;
|
|
177
|
-
savedSizes.current =
|
|
178
|
-
}, [config]);
|
|
179
|
-
const handleHeaderClick = (
|
|
241
|
+
savedSizes.current = renderableChildren.map(({ child }) => child.size);
|
|
242
|
+
}, [config, renderableChildren]);
|
|
243
|
+
const handleHeaderClick = (name) => {
|
|
180
244
|
if (config.kind === "leaf") {
|
|
245
|
+
const tabIndex = config.tabs.findIndex((tab) => tab === name);
|
|
246
|
+
if (tabIndex === -1) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
181
249
|
forceUpdate();
|
|
182
|
-
config.tabIndex =
|
|
250
|
+
config.tabIndex = tabIndex;
|
|
183
251
|
}
|
|
184
252
|
};
|
|
253
|
+
const activeTab = config.kind === "leaf"
|
|
254
|
+
? (() => {
|
|
255
|
+
const selected = config.tabs[config.tabIndex];
|
|
256
|
+
if (selected && !isAnchorTab(selected) && leaves[selected]) {
|
|
257
|
+
return selected;
|
|
258
|
+
}
|
|
259
|
+
return getRenderableTabs(config.tabs).find((tab) => leaves[tab]) || null;
|
|
260
|
+
})()
|
|
261
|
+
: null;
|
|
262
|
+
const leafContent = activeTab ? (_jsx("div", { children: leaves[activeTab].element }, activeTab)) : (_jsx("div", { style: { width: "100%", height: "100%" } }));
|
|
185
263
|
const panelContentRef = useRef(null);
|
|
186
264
|
const panelRef = useRef(null);
|
|
187
|
-
return (_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (
|
|
265
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${config.kind} panel`, style: style, ref: panelRef, children: [config.kind === "leaf" && getRenderableTabs(config.tabs).length > 0 ? (_jsx(TabHeader, { config: config, leaves: leaves, onClick: handleHeaderClick })) : null, _jsx("div", { className: "panel-content", ref: panelContentRef, style: makeStyle(), children: config.kind === "leaf" ? (leafContent) : (renderableChildren.map(({ child, childIndex }, i) => (_jsx(NestedPanel, { config: child, leaves: leaves, saveSizes: handleSaveSizes, index: i, onResize: handleResize, isLast: i === renderableChildren.length - 1, direction: config.kind, panelElements: panelElements }, childIndex)))) })] }), !isLast && direction === "column" && (_jsx("div", { className: "resize-border bottom", onMouseDown: (e) => handleMouseDown(e, "bottom") })), !isLast && direction === "row" && (_jsx("div", { className: "resize-border right", onMouseDown: (e) => handleMouseDown(e, "right") }))] }));
|
|
188
266
|
});
|
|
189
267
|
const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
190
268
|
const [, set] = useState();
|
|
@@ -245,7 +323,8 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
245
323
|
zones.push({ rect, config, index, element });
|
|
246
324
|
};
|
|
247
325
|
if (config.kind === "leaf") {
|
|
248
|
-
|
|
326
|
+
const renderableTabs = getRenderableTabs(config.tabs);
|
|
327
|
+
if (!(renderableTabs.length === 1 && renderableTabs[0] === name)) {
|
|
249
328
|
pushZone("TOP", left, top, width, height / 2);
|
|
250
329
|
pushZone("BOTTOM", left, top + height / 2, width, height / 2);
|
|
251
330
|
pushZone("LEFT", left, top, width / 2, height);
|
|
@@ -260,24 +339,32 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
260
339
|
pushZone("TAB", left, top, width, element.children[0].offsetHeight);
|
|
261
340
|
}
|
|
262
341
|
else {
|
|
263
|
-
const
|
|
264
|
-
|
|
342
|
+
const renderableChildren = config.children.filter((child) => isRenderableConfig(child));
|
|
343
|
+
if (!renderableChildren.length) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const firstChild = renderableChildren[0];
|
|
347
|
+
const lastChild = renderableChildren[renderableChildren.length - 1];
|
|
348
|
+
const firstTab = getFirstRenderableTab(firstChild);
|
|
349
|
+
const lastTab = getLastRenderableTab(lastChild);
|
|
350
|
+
const firstTabCount = getRenderableTabCount(firstChild);
|
|
351
|
+
const lastTabCount = getRenderableTabCount(lastChild);
|
|
265
352
|
if (config.kind === "row" || config === rootConfig) {
|
|
266
|
-
const zoneWidth = width / (config.kind === "row" ?
|
|
353
|
+
const zoneWidth = width / (config.kind === "row" ? renderableChildren.length + 1 : 2);
|
|
267
354
|
// check that the dragged item is not the last item in the row and a single tab
|
|
268
|
-
if (config === rootConfig ||
|
|
355
|
+
if (config === rootConfig || lastTabCount > 1 || lastTab !== name) {
|
|
269
356
|
pushZone("RIGHT", left + width - zoneWidth, top, zoneWidth, height);
|
|
270
357
|
}
|
|
271
|
-
if (config === rootConfig ||
|
|
358
|
+
if (config === rootConfig || firstTabCount > 1 || firstTab !== name) {
|
|
272
359
|
pushZone("LEFT", left, top, zoneWidth, height);
|
|
273
360
|
}
|
|
274
361
|
}
|
|
275
362
|
if (config.kind === "column" || config === rootConfig) {
|
|
276
|
-
const zoneHeight = height / (config.kind === "column" ?
|
|
277
|
-
if (config === rootConfig ||
|
|
363
|
+
const zoneHeight = height / (config.kind === "column" ? renderableChildren.length + 1 : 2);
|
|
364
|
+
if (config === rootConfig || lastTabCount > 1 || lastTab !== name) {
|
|
278
365
|
pushZone("BOTTOM", left, top + height - zoneHeight, width, zoneHeight);
|
|
279
366
|
}
|
|
280
|
-
if (config === rootConfig ||
|
|
367
|
+
if (config === rootConfig || firstTabCount > 1 || firstTab !== name) {
|
|
281
368
|
pushZone("TOP", left, top, width, zoneHeight);
|
|
282
369
|
}
|
|
283
370
|
}
|
|
@@ -409,11 +496,19 @@ export const Panel = (props) => {
|
|
|
409
496
|
*/
|
|
410
497
|
export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOnMobile = true, }) {
|
|
411
498
|
const children_array = React.Children.toArray(children);
|
|
412
|
-
const
|
|
413
|
-
|
|
499
|
+
const panelDefinitions = children_array.map((panel, i) => ({
|
|
500
|
+
name: getPanelName(panel, i),
|
|
501
|
+
header: panel.props.header,
|
|
502
|
+
element: panel.props.children,
|
|
503
|
+
place: panel.props.place,
|
|
504
|
+
}));
|
|
505
|
+
const panelNames = panelDefinitions.map((panel) => panel.name);
|
|
506
|
+
const placementByName = new Map(panelDefinitions.map((panel) => [panel.name, panel.place]));
|
|
507
|
+
const namedChildren = Object.fromEntries(panelDefinitions.map((panel) => [
|
|
508
|
+
panel.name,
|
|
414
509
|
{
|
|
415
|
-
element:
|
|
416
|
-
header:
|
|
510
|
+
element: panel.element,
|
|
511
|
+
header: panel.header,
|
|
417
512
|
},
|
|
418
513
|
]));
|
|
419
514
|
const panelElements = useRef(new Map());
|
|
@@ -423,9 +518,9 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
423
518
|
: {
|
|
424
519
|
kind: "row",
|
|
425
520
|
size: 1,
|
|
426
|
-
children:
|
|
521
|
+
children: panelDefinitions.map((panel) => ({
|
|
427
522
|
kind: "leaf",
|
|
428
|
-
tabs: [
|
|
523
|
+
tabs: [panel.name],
|
|
429
524
|
tabIndex: 0,
|
|
430
525
|
size: 100 / children_array.length,
|
|
431
526
|
})),
|
|
@@ -433,7 +528,7 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
433
528
|
if (collapseTabsOnMobile && isMobileDevice()) {
|
|
434
529
|
// If collapseTabsOnMobile is a list, use the names in the list as the first tabs
|
|
435
530
|
// then complete with the rest of the named children
|
|
436
|
-
const actualTabs =
|
|
531
|
+
const actualTabs = panelNames;
|
|
437
532
|
const tabs = [
|
|
438
533
|
...(Array.isArray(collapseTabsOnMobile)
|
|
439
534
|
? collapseTabsOnMobile.filter((name) => actualTabs.includes(name))
|
|
@@ -451,18 +546,26 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
451
546
|
return base;
|
|
452
547
|
});
|
|
453
548
|
let config = rootConfig;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
549
|
+
let nextConfig = rootConfig;
|
|
550
|
+
if (nextConfig.kind !== "leaf" || nextConfig.tabs.length > 0) {
|
|
551
|
+
nextConfig = filterPanels(panelNames, nextConfig) || { kind: "leaf", tabs: [], tabIndex: 0, size: 100 };
|
|
552
|
+
}
|
|
553
|
+
const tabs = getLayoutTabs(nextConfig);
|
|
554
|
+
const missingPanels = panelNames.filter((name) => !tabs.includes(name));
|
|
555
|
+
if (missingPanels.length > 0) {
|
|
556
|
+
missingPanels.forEach((name) => {
|
|
557
|
+
nextConfig = insertPanel(placementByName.get(name), name, nextConfig);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (nextConfig !== rootConfig) {
|
|
561
|
+
config = nextConfig;
|
|
562
|
+
setRootConfig(nextConfig);
|
|
460
563
|
}
|
|
461
564
|
const handleDrop = (zone, name) => {
|
|
462
565
|
const newConfig = movePanel(zone, name, rootConfig);
|
|
463
566
|
setRootConfig(newConfig);
|
|
464
567
|
};
|
|
465
|
-
const container = (_jsxs("div", { className: "container", children: [_jsx(NestedPanel, { leaves: namedChildren, config: config, panelElements: panelElements.current }), _jsx(Overlay, { panelElements: panelElements, onDrop: handleDrop, rootConfig: config })] }));
|
|
568
|
+
const container = (_jsxs("div", { className: "container", children: [isRenderableConfig(config) ? (_jsx(NestedPanel, { leaves: namedChildren, config: config, panelElements: panelElements.current })) : (_jsx("div", { style: { width: "100%", height: "100%" } })), _jsx(Overlay, { panelElements: panelElements, onDrop: handleDrop, rootConfig: config })] }));
|
|
466
569
|
if (wrapDnd) {
|
|
467
570
|
return _jsx(DndProvider, { backend: HTML5Backend, children: container });
|
|
468
571
|
}
|
package/index.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/index.test.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Layout, Panel } from "./index";
|
|
4
|
+
jest.mock("react-dnd", () => {
|
|
5
|
+
const React = require("react");
|
|
6
|
+
return {
|
|
7
|
+
DndProvider: ({ children }) => _jsx(_Fragment, { children: children }),
|
|
8
|
+
useDrag: () => [{}, () => null],
|
|
9
|
+
useDrop: () => [{}, () => null],
|
|
10
|
+
useDragDropManager: () => ({
|
|
11
|
+
getMonitor: () => ({
|
|
12
|
+
subscribeToStateChange: () => () => null,
|
|
13
|
+
subscribeToOffsetChange: () => () => null,
|
|
14
|
+
getItemType: () => null,
|
|
15
|
+
isDragging: () => false,
|
|
16
|
+
getItem: () => null,
|
|
17
|
+
getClientOffset: () => null,
|
|
18
|
+
}),
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
jest.mock("react-dnd-html5-backend", () => ({
|
|
23
|
+
HTML5Backend: {},
|
|
24
|
+
}));
|
|
25
|
+
describe("Layout virtual anchors", () => {
|
|
26
|
+
it("does not render anchor tabs", () => {
|
|
27
|
+
render(_jsx(Layout, { wrapDnd: false, defaultConfig: { kind: "leaf", tabs: ["panel-1", "@anchor-1"] }, children: _jsx(Panel, { name: "panel-1", children: _jsx("div", { children: "Panel One" }) }) }));
|
|
28
|
+
expect(screen.getByText("panel-1")).not.toBeNull();
|
|
29
|
+
expect(screen.queryByText("@anchor-1")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it("does not render anchor-only leaves", () => {
|
|
32
|
+
const { container } = render(_jsx(Layout, { wrapDnd: false, defaultConfig: {
|
|
33
|
+
kind: "row",
|
|
34
|
+
children: [
|
|
35
|
+
{ kind: "leaf", tabs: ["@anchor-1"] },
|
|
36
|
+
{ kind: "leaf", tabs: ["panel-1"] },
|
|
37
|
+
],
|
|
38
|
+
}, children: _jsx(Panel, { name: "panel-1", children: _jsx("div", { children: "Panel One" }) }) }));
|
|
39
|
+
// eslint-disable-next-line testing-library/no-container,testing-library/no-node-access
|
|
40
|
+
expect(container.querySelectorAll(".panel.leaf")).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type DefaultContainerConfig = {
|
|
|
45
45
|
nesting?: number;
|
|
46
46
|
};
|
|
47
47
|
export type DefaultLayoutConfig = string | DefaultLeafConfig | DefaultContainerConfig | DefaultLayoutConfig[];
|
|
48
|
+
export type PanelPlacement = {
|
|
49
|
+
where: "before-tab" | "after-tab" | "first-tab" | "last-tab" | "left" | "right" | "top" | "bottom";
|
|
50
|
+
of: string;
|
|
51
|
+
};
|
|
48
52
|
/**
|
|
49
53
|
* Properties for a panel component.
|
|
50
54
|
*
|
|
@@ -54,6 +58,7 @@ export type PanelProps = {
|
|
|
54
58
|
children: React.ReactNode;
|
|
55
59
|
name?: string;
|
|
56
60
|
header?: React.ReactNode;
|
|
61
|
+
place?: PanelPlacement;
|
|
57
62
|
};
|
|
58
63
|
/**
|
|
59
64
|
* Represents a drop zone within the layout.
|
package/utils.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { Zone, LayoutConfig } from "./types";
|
|
1
|
+
import { Zone, LayoutConfig, PanelPlacement } from "./types";
|
|
2
|
+
export declare const isAnchorTab: (name: string) => boolean;
|
|
3
|
+
export declare const getLayoutTabs: (inside: LayoutConfig) => string[];
|
|
4
|
+
export declare const insertPanel: (place: PanelPlacement | undefined, name: string, inside: LayoutConfig) => LayoutConfig;
|
|
2
5
|
/**
|
|
3
6
|
* Moves a panel within the layout based on a drop zone and panel name.
|
|
4
7
|
*
|
package/utils.js
CHANGED
|
@@ -32,6 +32,151 @@ const simplifyLayout = (config) => {
|
|
|
32
32
|
}
|
|
33
33
|
return config;
|
|
34
34
|
};
|
|
35
|
+
export const isAnchorTab = (name) => name.startsWith("@");
|
|
36
|
+
export const getLayoutTabs = (inside) => {
|
|
37
|
+
if (inside.kind === "leaf") {
|
|
38
|
+
return [...inside.tabs];
|
|
39
|
+
}
|
|
40
|
+
return inside.children.flatMap((child) => getLayoutTabs(child));
|
|
41
|
+
};
|
|
42
|
+
const cloneLayout = (inside) => {
|
|
43
|
+
if (inside.kind === "leaf") {
|
|
44
|
+
return {
|
|
45
|
+
...inside,
|
|
46
|
+
tabs: [...inside.tabs],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
...inside,
|
|
51
|
+
children: inside.children.map(cloneLayout),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const findLeafPathByTab = (inside, name, path = []) => {
|
|
55
|
+
if (inside.kind === "leaf") {
|
|
56
|
+
return inside.tabs.includes(name) ? path : null;
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < inside.children.length; i += 1) {
|
|
59
|
+
const childPath = findLeafPathByTab(inside.children[i], name, [...path, i]);
|
|
60
|
+
if (childPath) {
|
|
61
|
+
return childPath;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
};
|
|
66
|
+
const getNodeAtPath = (inside, path) => {
|
|
67
|
+
let current = inside;
|
|
68
|
+
path.forEach((index) => {
|
|
69
|
+
if (current.kind !== "leaf") {
|
|
70
|
+
current = current.children[index];
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return current;
|
|
74
|
+
};
|
|
75
|
+
const insertTabRelative = (inside, name, target, where) => {
|
|
76
|
+
const targetPath = findLeafPathByTab(inside, target);
|
|
77
|
+
if (!targetPath) {
|
|
78
|
+
return inside;
|
|
79
|
+
}
|
|
80
|
+
const next = cloneLayout(inside);
|
|
81
|
+
const targetLeaf = getNodeAtPath(next, targetPath);
|
|
82
|
+
const targetTabIndex = targetLeaf.tabs.findIndex((tab) => tab === target);
|
|
83
|
+
if (targetTabIndex === -1) {
|
|
84
|
+
return inside;
|
|
85
|
+
}
|
|
86
|
+
const insertIndex = where === "before-tab"
|
|
87
|
+
? targetTabIndex
|
|
88
|
+
: where === "after-tab"
|
|
89
|
+
? targetTabIndex + 1
|
|
90
|
+
: where === "first-tab"
|
|
91
|
+
? 0
|
|
92
|
+
: targetLeaf.tabs.length;
|
|
93
|
+
targetLeaf.tabs.splice(insertIndex, 0, name);
|
|
94
|
+
targetLeaf.tabIndex = insertIndex;
|
|
95
|
+
return next;
|
|
96
|
+
};
|
|
97
|
+
const insertSplitRelative = (inside, name, target, where) => {
|
|
98
|
+
const targetPath = findLeafPathByTab(inside, target);
|
|
99
|
+
if (!targetPath) {
|
|
100
|
+
return inside;
|
|
101
|
+
}
|
|
102
|
+
const splitKind = where === "left" || where === "right" ? "row" : "column";
|
|
103
|
+
const insertBefore = where === "left" || where === "top";
|
|
104
|
+
const next = cloneLayout(inside);
|
|
105
|
+
const targetLeaf = getNodeAtPath(next, targetPath);
|
|
106
|
+
const newLeaf = {
|
|
107
|
+
kind: "leaf",
|
|
108
|
+
tabs: [name],
|
|
109
|
+
tabIndex: 0,
|
|
110
|
+
size: 50,
|
|
111
|
+
};
|
|
112
|
+
if (targetPath.length === 0) {
|
|
113
|
+
const originalRoot = next;
|
|
114
|
+
const existingRoot = {
|
|
115
|
+
...originalRoot,
|
|
116
|
+
tabs: [...originalRoot.tabs],
|
|
117
|
+
size: 50,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
kind: splitKind,
|
|
121
|
+
children: insertBefore ? [newLeaf, existingRoot] : [existingRoot, newLeaf],
|
|
122
|
+
size: originalRoot.size,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const parentPath = targetPath.slice(0, -1);
|
|
126
|
+
const targetIndex = targetPath[targetPath.length - 1];
|
|
127
|
+
const parent = getNodeAtPath(next, parentPath);
|
|
128
|
+
if (parent.kind === splitKind) {
|
|
129
|
+
const currentTarget = parent.children[targetIndex];
|
|
130
|
+
const insertedSize = currentTarget.size / 2;
|
|
131
|
+
currentTarget.size -= insertedSize;
|
|
132
|
+
const insertedLeaf = {
|
|
133
|
+
...newLeaf,
|
|
134
|
+
size: insertedSize,
|
|
135
|
+
};
|
|
136
|
+
parent.children.splice(insertBefore ? targetIndex : targetIndex + 1, 0, insertedLeaf);
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
const wrappedTarget = targetLeaf;
|
|
140
|
+
const wrappedTargetLeaf = {
|
|
141
|
+
...wrappedTarget,
|
|
142
|
+
tabs: [...wrappedTarget.tabs],
|
|
143
|
+
size: 50,
|
|
144
|
+
};
|
|
145
|
+
const wrappedContainer = {
|
|
146
|
+
kind: splitKind,
|
|
147
|
+
children: insertBefore ? [newLeaf, wrappedTargetLeaf] : [wrappedTargetLeaf, newLeaf],
|
|
148
|
+
size: wrappedTarget.size,
|
|
149
|
+
};
|
|
150
|
+
parent.children[targetIndex] = wrappedContainer;
|
|
151
|
+
return next;
|
|
152
|
+
};
|
|
153
|
+
export const insertPanel = (place, name, inside) => {
|
|
154
|
+
const existingTabs = getLayoutTabs(inside);
|
|
155
|
+
if (existingTabs.includes(name)) {
|
|
156
|
+
return inside;
|
|
157
|
+
}
|
|
158
|
+
if (existingTabs.length === 0) {
|
|
159
|
+
if (inside.kind === "leaf") {
|
|
160
|
+
return {
|
|
161
|
+
...inside,
|
|
162
|
+
tabs: [name],
|
|
163
|
+
tabIndex: 0,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
kind: "leaf",
|
|
168
|
+
tabs: [name],
|
|
169
|
+
tabIndex: 0,
|
|
170
|
+
size: inside.size,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const target = place && existingTabs.includes(place.of) ? place.of : existingTabs[0];
|
|
174
|
+
const where = place?.where || "after-tab";
|
|
175
|
+
if (where === "before-tab" || where === "after-tab" || where === "first-tab" || where === "last-tab") {
|
|
176
|
+
return insertTabRelative(inside, name, target, where);
|
|
177
|
+
}
|
|
178
|
+
return insertSplitRelative(inside, name, target, where);
|
|
179
|
+
};
|
|
35
180
|
/**
|
|
36
181
|
* Moves a panel within the layout based on a drop zone and panel name.
|
|
37
182
|
*
|
|
@@ -201,13 +346,15 @@ export const movePanel = (zone, name, inside) => {
|
|
|
201
346
|
export const filterPanels = (names, inside) => {
|
|
202
347
|
const editLayout = (visitedConfig) => {
|
|
203
348
|
let config = visitedConfig;
|
|
204
|
-
if (config.kind === "leaf" &&
|
|
349
|
+
if (config.kind === "leaf" &&
|
|
350
|
+
!config.tabs.every((name) => names.includes(name) || isAnchorTab(name))) {
|
|
205
351
|
/* If it's a simple leaf, try to remove the matching tab if it was
|
|
206
352
|
* the tab that was picked by the user (since we're moving it) */
|
|
353
|
+
const tabs = config.tabs.filter((name) => names.includes(name) || isAnchorTab(name));
|
|
207
354
|
config = {
|
|
208
355
|
...config,
|
|
209
|
-
tabs
|
|
210
|
-
tabIndex: Math.min(
|
|
356
|
+
tabs,
|
|
357
|
+
tabIndex: tabs.length ? Math.min(tabs.length - 1, config.tabIndex) : 0,
|
|
211
358
|
};
|
|
212
359
|
if (config.tabs.length === 0) {
|
|
213
360
|
config = null;
|
package/utils.test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/utils.test.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { filterPanels, insertPanel } from "./utils";
|
|
2
|
+
describe("filterPanels", () => {
|
|
3
|
+
it("keeps anchors even when they are not part of rendered Panel children", () => {
|
|
4
|
+
const config = {
|
|
5
|
+
kind: "leaf",
|
|
6
|
+
tabs: ["@anchor-1", "Panel 1", "Panel 2"],
|
|
7
|
+
tabIndex: 2,
|
|
8
|
+
size: 100,
|
|
9
|
+
};
|
|
10
|
+
const filtered = filterPanels(["Panel 1"], config);
|
|
11
|
+
expect(filtered.kind).toBe("leaf");
|
|
12
|
+
expect(filtered.tabs).toEqual(["@anchor-1", "Panel 1"]);
|
|
13
|
+
expect(filtered.tabIndex).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("insertPanel", () => {
|
|
17
|
+
it("falls back to first tab when place.of is missing", () => {
|
|
18
|
+
const config = {
|
|
19
|
+
kind: "leaf",
|
|
20
|
+
tabs: ["Panel 1", "Panel 2"],
|
|
21
|
+
tabIndex: 0,
|
|
22
|
+
size: 100,
|
|
23
|
+
};
|
|
24
|
+
const updated = insertPanel({ where: "after-tab", of: "Missing" }, "Panel 3", config);
|
|
25
|
+
expect(updated.kind).toBe("leaf");
|
|
26
|
+
expect(updated.tabs).toEqual(["Panel 1", "Panel 3", "Panel 2"]);
|
|
27
|
+
});
|
|
28
|
+
it("inserts a new sibling leaf when splitting in the same container axis", () => {
|
|
29
|
+
const config = {
|
|
30
|
+
kind: "row",
|
|
31
|
+
size: 100,
|
|
32
|
+
children: [
|
|
33
|
+
{ kind: "leaf", tabs: ["Panel 1"], tabIndex: 0, size: 60 },
|
|
34
|
+
{ kind: "leaf", tabs: ["Panel 2"], tabIndex: 0, size: 40 },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const updated = insertPanel({ where: "right", of: "Panel 1" }, "Panel 3", config);
|
|
38
|
+
expect(updated.kind).toBe("row");
|
|
39
|
+
const tabs = updated.children.map((child) => child.tabs[0]);
|
|
40
|
+
expect(tabs).toEqual(["Panel 1", "Panel 3", "Panel 2"]);
|
|
41
|
+
expect(updated.children.map((child) => child.size)).toEqual([30, 30, 40]);
|
|
42
|
+
});
|
|
43
|
+
it("inserts at the start of the target tab list with first-tab", () => {
|
|
44
|
+
const config = {
|
|
45
|
+
kind: "leaf",
|
|
46
|
+
tabs: ["Panel 1", "Panel 2", "Panel 3"],
|
|
47
|
+
tabIndex: 1,
|
|
48
|
+
size: 100,
|
|
49
|
+
};
|
|
50
|
+
const updated = insertPanel({ where: "first-tab", of: "Panel 2" }, "Panel 4", config);
|
|
51
|
+
expect(updated.kind).toBe("leaf");
|
|
52
|
+
expect(updated.tabs).toEqual(["Panel 4", "Panel 1", "Panel 2", "Panel 3"]);
|
|
53
|
+
});
|
|
54
|
+
it("inserts at the end of the target tab list with last-tab", () => {
|
|
55
|
+
const config = {
|
|
56
|
+
kind: "leaf",
|
|
57
|
+
tabs: ["Panel 1", "Panel 2", "Panel 3"],
|
|
58
|
+
tabIndex: 1,
|
|
59
|
+
size: 100,
|
|
60
|
+
};
|
|
61
|
+
const updated = insertPanel({ where: "last-tab", of: "Panel 2" }, "Panel 4", config);
|
|
62
|
+
expect(updated.kind).toBe("leaf");
|
|
63
|
+
expect(updated.tabs).toEqual(["Panel 1", "Panel 2", "Panel 3", "Panel 4"]);
|
|
64
|
+
});
|
|
65
|
+
});
|