react-simple-dock 0.2.2 → 0.2.4
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 +157 -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,11 @@ 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
|
+
.map((child, childIndex) => ({ child, childIndex }))
|
|
124
|
+
.filter(({ child }) => isRenderableConfig(child));
|
|
79
125
|
useEffect(() => {
|
|
80
126
|
const handleMouseUp = (e) => {
|
|
81
127
|
document.removeEventListener("mousemove", startEvent.current?.handleMouseMove);
|
|
@@ -105,17 +151,21 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
105
151
|
const { clientX, clientY } = e;
|
|
106
152
|
// get Panel top left corner
|
|
107
153
|
const { top, left } = (startEvent.current?.target).parentElement.getBoundingClientRect();
|
|
154
|
+
const parent = (startEvent.current?.target).parentElement;
|
|
155
|
+
const containerWidth = parent.offsetWidth;
|
|
156
|
+
const containerHeight = parent.offsetHeight;
|
|
108
157
|
let ratio;
|
|
109
158
|
if (startEvent.current.side === "bottom") {
|
|
110
|
-
ratio = (clientY - top) /
|
|
159
|
+
ratio = containerHeight > 0 ? (clientY - top) / containerHeight : 0;
|
|
111
160
|
}
|
|
112
161
|
else if (startEvent.current.side === "right") {
|
|
113
|
-
ratio = (clientX - left) /
|
|
162
|
+
ratio = containerWidth > 0 ? (clientX - left) / containerWidth : 0;
|
|
114
163
|
}
|
|
115
164
|
else {
|
|
116
165
|
return;
|
|
117
166
|
}
|
|
118
|
-
|
|
167
|
+
const boundedRatio = Math.max(0, Math.min(1, ratio));
|
|
168
|
+
onResize && onResize(boundedRatio, index, startEvent.current.target);
|
|
119
169
|
}, [onResize, index]);
|
|
120
170
|
const handleMouseDown = (e, side) => {
|
|
121
171
|
if (startEvent.current) {
|
|
@@ -126,8 +176,6 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
126
176
|
e.preventDefault();
|
|
127
177
|
saveSizes && saveSizes();
|
|
128
178
|
startEvent.current = {
|
|
129
|
-
width: e.clientX - left,
|
|
130
|
-
height: e.clientY - top,
|
|
131
179
|
target: e.currentTarget,
|
|
132
180
|
side: side,
|
|
133
181
|
handleMouseMove,
|
|
@@ -138,53 +186,82 @@ const NestedPanel = React.memo(({ leaves, config, index, onResize, saveSizes, is
|
|
|
138
186
|
if (config.kind === "row") {
|
|
139
187
|
// return {gridTemplateColumns: config.children.map(c => `calc((${100}% - var(--grid-gap) * ${config.children.length - 1}) * ${c.size / 100})`).join(" ")};
|
|
140
188
|
return {
|
|
141
|
-
gridTemplateColumns:
|
|
189
|
+
gridTemplateColumns: renderableChildren.map(({ child, childIndex }) => (childIndex == 0 ? `${child.size}fr` : `7px ${child.size}fr`)).join(" "),
|
|
142
190
|
};
|
|
143
191
|
}
|
|
144
192
|
if (config.kind === "column") {
|
|
145
193
|
// return {gridTemplateRows: config.children.map(c => `calc((${100}% - var(--grid-gap) * ${config.children.length - 1}) * ${c.size / 100})`).join(" ")};
|
|
146
194
|
return {
|
|
147
|
-
gridTemplateRows:
|
|
195
|
+
gridTemplateRows: renderableChildren.map(({ child, childIndex }) => (childIndex == 0 ? `${child.size}fr` : `7px ${child.size}fr`)).join(" "),
|
|
148
196
|
};
|
|
149
197
|
}
|
|
150
198
|
};
|
|
151
199
|
const handleResize = useCallback((ratio, idx, target) => {
|
|
152
200
|
if (config.kind === "leaf")
|
|
153
201
|
return;
|
|
154
|
-
|
|
155
|
-
|
|
202
|
+
const current = renderableChildren[idx];
|
|
203
|
+
const next = renderableChildren[idx + 1];
|
|
204
|
+
if (!current || !next) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
156
207
|
const total = savedSizes.current.reduce((a, b) => a + b, 0);
|
|
208
|
+
if (total <= 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const sizeBefore = savedSizes.current.slice(0, idx).reduce((a, b) => a + b, 0);
|
|
212
|
+
const pairTotal = savedSizes.current[idx] + savedSizes.current[idx + 1];
|
|
213
|
+
let size = ratio * total - sizeBefore;
|
|
214
|
+
size = Math.max(0, Math.min(pairTotal, size));
|
|
215
|
+
let nextSize = pairTotal - size;
|
|
157
216
|
if (config.kind === "column") {
|
|
158
|
-
const headerHeightBefore = getPanelElementMaxHeaderHeight(
|
|
159
|
-
const headerHeightAfter = getPanelElementMaxHeaderHeight(
|
|
217
|
+
const headerHeightBefore = getPanelElementMaxHeaderHeight(current.child, panelElements);
|
|
218
|
+
const headerHeightAfter = getPanelElementMaxHeaderHeight(next.child, panelElements);
|
|
160
219
|
const parentHeight = panelContentRef.current.offsetHeight;
|
|
220
|
+
if (parentHeight <= 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
161
223
|
if ((size * parentHeight) / total < headerHeightBefore) {
|
|
162
224
|
size = (headerHeightBefore / parentHeight) * total;
|
|
163
|
-
|
|
225
|
+
size = Math.max(0, Math.min(pairTotal, size));
|
|
226
|
+
nextSize = pairTotal - size;
|
|
164
227
|
}
|
|
165
228
|
else if ((nextSize * parentHeight) / total < headerHeightAfter) {
|
|
166
|
-
nextSize = (headerHeightAfter / parentHeight) * total;
|
|
167
|
-
size =
|
|
229
|
+
nextSize = Math.max(0, Math.min(pairTotal, (headerHeightAfter / parentHeight) * total));
|
|
230
|
+
size = pairTotal - nextSize;
|
|
168
231
|
}
|
|
169
232
|
}
|
|
170
|
-
|
|
171
|
-
|
|
233
|
+
current.child.size = size;
|
|
234
|
+
next.child.size = nextSize;
|
|
172
235
|
Object.assign(panelContentRef.current.style, makeStyle());
|
|
173
|
-
}, [config]);
|
|
236
|
+
}, [config, renderableChildren]);
|
|
174
237
|
const handleSaveSizes = useCallback(() => {
|
|
175
238
|
if (config.kind === "leaf")
|
|
176
239
|
return;
|
|
177
|
-
savedSizes.current =
|
|
178
|
-
}, [config]);
|
|
179
|
-
const handleHeaderClick = (
|
|
240
|
+
savedSizes.current = renderableChildren.map(({ child }) => child.size);
|
|
241
|
+
}, [config, renderableChildren]);
|
|
242
|
+
const handleHeaderClick = (name) => {
|
|
180
243
|
if (config.kind === "leaf") {
|
|
244
|
+
const tabIndex = config.tabs.findIndex((tab) => tab === name);
|
|
245
|
+
if (tabIndex === -1) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
181
248
|
forceUpdate();
|
|
182
|
-
config.tabIndex =
|
|
249
|
+
config.tabIndex = tabIndex;
|
|
183
250
|
}
|
|
184
251
|
};
|
|
252
|
+
const activeTab = config.kind === "leaf"
|
|
253
|
+
? (() => {
|
|
254
|
+
const selected = config.tabs[config.tabIndex];
|
|
255
|
+
if (selected && !isAnchorTab(selected) && leaves[selected]) {
|
|
256
|
+
return selected;
|
|
257
|
+
}
|
|
258
|
+
return getRenderableTabs(config.tabs).find((tab) => leaves[tab]) || null;
|
|
259
|
+
})()
|
|
260
|
+
: null;
|
|
261
|
+
const leafContent = activeTab ? (_jsx("div", { children: leaves[activeTab].element }, activeTab)) : (_jsx("div", { style: { width: "100%", height: "100%" } }));
|
|
185
262
|
const panelContentRef = useRef(null);
|
|
186
263
|
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" ? (
|
|
264
|
+
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
265
|
});
|
|
189
266
|
const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
190
267
|
const [, set] = useState();
|
|
@@ -245,7 +322,8 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
245
322
|
zones.push({ rect, config, index, element });
|
|
246
323
|
};
|
|
247
324
|
if (config.kind === "leaf") {
|
|
248
|
-
|
|
325
|
+
const renderableTabs = getRenderableTabs(config.tabs);
|
|
326
|
+
if (!(renderableTabs.length === 1 && renderableTabs[0] === name)) {
|
|
249
327
|
pushZone("TOP", left, top, width, height / 2);
|
|
250
328
|
pushZone("BOTTOM", left, top + height / 2, width, height / 2);
|
|
251
329
|
pushZone("LEFT", left, top, width / 2, height);
|
|
@@ -260,24 +338,32 @@ const Overlay = ({ panelElements, onDrop, rootConfig, }) => {
|
|
|
260
338
|
pushZone("TAB", left, top, width, element.children[0].offsetHeight);
|
|
261
339
|
}
|
|
262
340
|
else {
|
|
263
|
-
const
|
|
264
|
-
|
|
341
|
+
const renderableChildren = config.children.filter((child) => isRenderableConfig(child));
|
|
342
|
+
if (!renderableChildren.length) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const firstChild = renderableChildren[0];
|
|
346
|
+
const lastChild = renderableChildren[renderableChildren.length - 1];
|
|
347
|
+
const firstTab = getFirstRenderableTab(firstChild);
|
|
348
|
+
const lastTab = getLastRenderableTab(lastChild);
|
|
349
|
+
const firstTabCount = getRenderableTabCount(firstChild);
|
|
350
|
+
const lastTabCount = getRenderableTabCount(lastChild);
|
|
265
351
|
if (config.kind === "row" || config === rootConfig) {
|
|
266
|
-
const zoneWidth = width / (config.kind === "row" ?
|
|
352
|
+
const zoneWidth = width / (config.kind === "row" ? renderableChildren.length + 1 : 2);
|
|
267
353
|
// check that the dragged item is not the last item in the row and a single tab
|
|
268
|
-
if (config === rootConfig ||
|
|
354
|
+
if (config === rootConfig || lastTabCount > 1 || lastTab !== name) {
|
|
269
355
|
pushZone("RIGHT", left + width - zoneWidth, top, zoneWidth, height);
|
|
270
356
|
}
|
|
271
|
-
if (config === rootConfig ||
|
|
357
|
+
if (config === rootConfig || firstTabCount > 1 || firstTab !== name) {
|
|
272
358
|
pushZone("LEFT", left, top, zoneWidth, height);
|
|
273
359
|
}
|
|
274
360
|
}
|
|
275
361
|
if (config.kind === "column" || config === rootConfig) {
|
|
276
|
-
const zoneHeight = height / (config.kind === "column" ?
|
|
277
|
-
if (config === rootConfig ||
|
|
362
|
+
const zoneHeight = height / (config.kind === "column" ? renderableChildren.length + 1 : 2);
|
|
363
|
+
if (config === rootConfig || lastTabCount > 1 || lastTab !== name) {
|
|
278
364
|
pushZone("BOTTOM", left, top + height - zoneHeight, width, zoneHeight);
|
|
279
365
|
}
|
|
280
|
-
if (config === rootConfig ||
|
|
366
|
+
if (config === rootConfig || firstTabCount > 1 || firstTab !== name) {
|
|
281
367
|
pushZone("TOP", left, top, width, zoneHeight);
|
|
282
368
|
}
|
|
283
369
|
}
|
|
@@ -409,11 +495,19 @@ export const Panel = (props) => {
|
|
|
409
495
|
*/
|
|
410
496
|
export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOnMobile = true, }) {
|
|
411
497
|
const children_array = React.Children.toArray(children);
|
|
412
|
-
const
|
|
413
|
-
|
|
498
|
+
const panelDefinitions = children_array.map((panel, i) => ({
|
|
499
|
+
name: getPanelName(panel, i),
|
|
500
|
+
header: panel.props.header,
|
|
501
|
+
element: panel.props.children,
|
|
502
|
+
place: panel.props.place,
|
|
503
|
+
}));
|
|
504
|
+
const panelNames = panelDefinitions.map((panel) => panel.name);
|
|
505
|
+
const placementByName = new Map(panelDefinitions.map((panel) => [panel.name, panel.place]));
|
|
506
|
+
const namedChildren = Object.fromEntries(panelDefinitions.map((panel) => [
|
|
507
|
+
panel.name,
|
|
414
508
|
{
|
|
415
|
-
element:
|
|
416
|
-
header:
|
|
509
|
+
element: panel.element,
|
|
510
|
+
header: panel.header,
|
|
417
511
|
},
|
|
418
512
|
]));
|
|
419
513
|
const panelElements = useRef(new Map());
|
|
@@ -423,9 +517,9 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
423
517
|
: {
|
|
424
518
|
kind: "row",
|
|
425
519
|
size: 1,
|
|
426
|
-
children:
|
|
520
|
+
children: panelDefinitions.map((panel) => ({
|
|
427
521
|
kind: "leaf",
|
|
428
|
-
tabs: [
|
|
522
|
+
tabs: [panel.name],
|
|
429
523
|
tabIndex: 0,
|
|
430
524
|
size: 100 / children_array.length,
|
|
431
525
|
})),
|
|
@@ -433,7 +527,7 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
433
527
|
if (collapseTabsOnMobile && isMobileDevice()) {
|
|
434
528
|
// If collapseTabsOnMobile is a list, use the names in the list as the first tabs
|
|
435
529
|
// then complete with the rest of the named children
|
|
436
|
-
const actualTabs =
|
|
530
|
+
const actualTabs = panelNames;
|
|
437
531
|
const tabs = [
|
|
438
532
|
...(Array.isArray(collapseTabsOnMobile)
|
|
439
533
|
? collapseTabsOnMobile.filter((name) => actualTabs.includes(name))
|
|
@@ -451,18 +545,26 @@ export function Layout({ children, defaultConfig, wrapDnd = true, collapseTabsOn
|
|
|
451
545
|
return base;
|
|
452
546
|
});
|
|
453
547
|
let config = rootConfig;
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
548
|
+
let nextConfig = rootConfig;
|
|
549
|
+
if (nextConfig.kind !== "leaf" || nextConfig.tabs.length > 0) {
|
|
550
|
+
nextConfig = filterPanels(panelNames, nextConfig) || { kind: "leaf", tabs: [], tabIndex: 0, size: 100 };
|
|
551
|
+
}
|
|
552
|
+
const tabs = getLayoutTabs(nextConfig);
|
|
553
|
+
const missingPanels = panelNames.filter((name) => !tabs.includes(name));
|
|
554
|
+
if (missingPanels.length > 0) {
|
|
555
|
+
missingPanels.forEach((name) => {
|
|
556
|
+
nextConfig = insertPanel(placementByName.get(name), name, nextConfig);
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (nextConfig !== rootConfig) {
|
|
560
|
+
config = nextConfig;
|
|
561
|
+
setRootConfig(nextConfig);
|
|
460
562
|
}
|
|
461
563
|
const handleDrop = (zone, name) => {
|
|
462
564
|
const newConfig = movePanel(zone, name, rootConfig);
|
|
463
565
|
setRootConfig(newConfig);
|
|
464
566
|
};
|
|
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 })] }));
|
|
567
|
+
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
568
|
if (wrapDnd) {
|
|
467
569
|
return _jsx(DndProvider, { backend: HTML5Backend, children: container });
|
|
468
570
|
}
|
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
|
+
});
|