react-resizable-panels 0.0.54 → 0.0.56
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/.eslintrc.cjs +26 -0
- package/CHANGELOG.md +253 -80
- package/README.md +55 -49
- package/dist/declarations/src/Panel.d.ts +76 -20
- package/dist/declarations/src/PanelGroup.d.ts +29 -21
- package/dist/declarations/src/PanelResizeHandle.d.ts +1 -1
- package/dist/declarations/src/index.d.ts +5 -5
- package/dist/declarations/src/types.d.ts +3 -25
- package/dist/declarations/src/vendor/react.d.ts +4 -4
- package/dist/react-resizable-panels.browser.cjs.js +1279 -796
- package/dist/react-resizable-panels.browser.development.cjs.js +1404 -809
- package/dist/react-resizable-panels.browser.development.esm.js +1398 -803
- package/dist/react-resizable-panels.browser.esm.js +1279 -796
- package/dist/react-resizable-panels.cjs.js +1279 -796
- package/dist/react-resizable-panels.cjs.js.map +1 -0
- package/dist/react-resizable-panels.development.cjs.js +1399 -804
- package/dist/react-resizable-panels.development.esm.js +1400 -805
- package/dist/react-resizable-panels.development.node.cjs.js +1172 -755
- package/dist/react-resizable-panels.development.node.esm.js +1173 -756
- package/dist/react-resizable-panels.esm.js +1279 -796
- package/dist/react-resizable-panels.esm.js.map +1 -0
- package/dist/react-resizable-panels.node.cjs.js +1064 -749
- package/dist/react-resizable-panels.node.esm.js +1065 -750
- package/jest.config.js +10 -0
- package/package.json +3 -1
- package/src/Panel.test.tsx +308 -0
- package/src/Panel.ts +179 -127
- package/src/PanelGroup.test.tsx +210 -0
- package/src/PanelGroup.ts +751 -580
- package/src/PanelGroupContext.ts +33 -0
- package/src/PanelResizeHandle.ts +13 -8
- package/src/hooks/useUniqueId.ts +1 -1
- package/src/hooks/useWindowSplitterBehavior.ts +9 -161
- package/src/hooks/useWindowSplitterPanelGroupBehavior.ts +185 -0
- package/src/index.ts +24 -11
- package/src/types.ts +3 -29
- package/src/utils/adjustLayoutByDelta.test.ts +1808 -0
- package/src/utils/adjustLayoutByDelta.ts +211 -0
- package/src/utils/calculateAriaValues.test.ts +111 -0
- package/src/utils/calculateAriaValues.ts +67 -0
- package/src/utils/calculateDeltaPercentage.ts +68 -0
- package/src/utils/calculateDragOffsetPercentage.ts +30 -0
- package/src/utils/calculateUnsafeDefaultLayout.test.ts +92 -0
- package/src/utils/calculateUnsafeDefaultLayout.ts +55 -0
- package/src/utils/callPanelCallbacks.ts +81 -0
- package/src/utils/compareLayouts.test.ts +9 -0
- package/src/utils/compareLayouts.ts +12 -0
- package/src/utils/computePanelFlexBoxStyle.ts +44 -0
- package/src/utils/computePercentagePanelConstraints.test.ts +71 -0
- package/src/utils/computePercentagePanelConstraints.ts +56 -0
- package/src/utils/convertPercentageToPixels.test.ts +9 -0
- package/src/utils/convertPercentageToPixels.ts +6 -0
- package/src/utils/convertPixelConstraintsToPercentages.ts +55 -0
- package/src/utils/convertPixelsToPercentage.test.ts +9 -0
- package/src/utils/convertPixelsToPercentage.ts +6 -0
- package/src/utils/determinePivotIndices.ts +10 -0
- package/src/utils/dom/calculateAvailablePanelSizeInPixels.ts +29 -0
- package/src/utils/dom/getAvailableGroupSizePixels.ts +29 -0
- package/src/utils/dom/getPanelElement.ts +7 -0
- package/src/utils/dom/getPanelGroupElement.ts +7 -0
- package/src/utils/dom/getResizeHandleElement.ts +9 -0
- package/src/utils/dom/getResizeHandleElementIndex.ts +12 -0
- package/src/utils/dom/getResizeHandleElementsForGroup.ts +9 -0
- package/src/utils/dom/getResizeHandlePanelIds.ts +18 -0
- package/src/utils/events.ts +13 -0
- package/src/utils/getPercentageSizeFromMixedSizes.test.ts +47 -0
- package/src/utils/getPercentageSizeFromMixedSizes.ts +15 -0
- package/src/utils/getResizeEventCursorPosition.ts +19 -0
- package/src/utils/initializeDefaultStorage.ts +26 -0
- package/src/utils/numbers/fuzzyCompareNumbers.test.ts +16 -0
- package/src/utils/numbers/fuzzyCompareNumbers.ts +17 -0
- package/src/utils/numbers/fuzzyNumbersEqual.ts +9 -0
- package/src/utils/resizePanel.ts +41 -0
- package/src/utils/serialization.ts +9 -4
- package/src/utils/shouldMonitorPixelBasedConstraints.test.ts +23 -0
- package/src/utils/shouldMonitorPixelBasedConstraints.ts +13 -0
- package/src/utils/test-utils.ts +136 -0
- package/src/utils/validatePanelConstraints.test.ts +151 -0
- package/src/utils/validatePanelConstraints.ts +103 -0
- package/src/utils/validatePanelGroupLayout.test.ts +233 -0
- package/src/utils/validatePanelGroupLayout.ts +88 -0
- package/src/vendor/react.ts +4 -0
- package/.eslintrc.json +0 -22
- package/src/PanelContexts.ts +0 -20
- package/src/utils/coordinates.ts +0 -149
- package/src/utils/group.ts +0 -315
|
@@ -7,6 +7,7 @@ import * as React from 'react';
|
|
|
7
7
|
const {
|
|
8
8
|
createElement,
|
|
9
9
|
createContext,
|
|
10
|
+
createRef,
|
|
10
11
|
forwardRef,
|
|
11
12
|
useCallback,
|
|
12
13
|
useContext,
|
|
@@ -21,6 +22,9 @@ const {
|
|
|
21
22
|
// `toString()` prevents bundlers from trying to `import { useId } from 'react'`
|
|
22
23
|
const useId = React["useId".toString()];
|
|
23
24
|
|
|
25
|
+
const PanelGroupContext = createContext(null);
|
|
26
|
+
PanelGroupContext.displayName = "PanelGroupContext";
|
|
27
|
+
|
|
24
28
|
const wrappedUseId = typeof useId === "function" ? useId : () => null;
|
|
25
29
|
let counter = 0;
|
|
26
30
|
function useUniqueId(idFromParams = null) {
|
|
@@ -29,105 +33,117 @@ function useUniqueId(idFromParams = null) {
|
|
|
29
33
|
if (idRef.current === null) {
|
|
30
34
|
idRef.current = "" + counter++;
|
|
31
35
|
}
|
|
32
|
-
return idRef.current;
|
|
36
|
+
return idFromParams ?? idRef.current;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
const PanelGroupContext = createContext(null);
|
|
36
|
-
PanelGroupContext.displayName = "PanelGroupContext";
|
|
37
|
-
|
|
38
39
|
function PanelWithForwardedRef({
|
|
39
|
-
children
|
|
40
|
+
children,
|
|
40
41
|
className: classNameFromProps = "",
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
collapsedSizePercentage,
|
|
43
|
+
collapsedSizePixels,
|
|
44
|
+
collapsible,
|
|
45
|
+
defaultSizePercentage,
|
|
46
|
+
defaultSizePixels,
|
|
44
47
|
forwardedRef,
|
|
45
|
-
id: idFromProps
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
id: idFromProps,
|
|
49
|
+
maxSizePercentage,
|
|
50
|
+
maxSizePixels,
|
|
51
|
+
minSizePercentage,
|
|
52
|
+
minSizePixels,
|
|
53
|
+
onCollapse,
|
|
54
|
+
onExpand,
|
|
55
|
+
onResize,
|
|
56
|
+
order,
|
|
57
|
+
style: styleFromProps,
|
|
52
58
|
tagName: Type = "div"
|
|
53
59
|
}) {
|
|
54
60
|
const context = useContext(PanelGroupContext);
|
|
55
61
|
if (context === null) {
|
|
56
62
|
throw Error(`Panel components must be rendered within a PanelGroup container`);
|
|
57
63
|
}
|
|
58
|
-
const panelId = useUniqueId(idFromProps);
|
|
59
64
|
const {
|
|
60
65
|
collapsePanel,
|
|
61
66
|
expandPanel,
|
|
67
|
+
getPanelSize,
|
|
62
68
|
getPanelStyle,
|
|
69
|
+
isPanelCollapsed,
|
|
63
70
|
registerPanel,
|
|
64
71
|
resizePanel,
|
|
65
72
|
unregisterPanel
|
|
66
73
|
} = context;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
const panelId = useUniqueId(idFromProps);
|
|
75
|
+
const panelDataRef = useRef({
|
|
76
|
+
callbacks: {
|
|
77
|
+
onCollapse,
|
|
78
|
+
onExpand,
|
|
79
|
+
onResize
|
|
80
|
+
},
|
|
81
|
+
constraints: {
|
|
82
|
+
collapsedSizePercentage,
|
|
83
|
+
collapsedSizePixels,
|
|
84
|
+
collapsible,
|
|
85
|
+
defaultSizePercentage,
|
|
86
|
+
defaultSizePixels,
|
|
87
|
+
maxSizePercentage,
|
|
88
|
+
maxSizePixels,
|
|
89
|
+
minSizePercentage,
|
|
90
|
+
minSizePixels
|
|
91
|
+
},
|
|
92
|
+
id: panelId,
|
|
93
|
+
idIsFromProps: idFromProps !== undefined,
|
|
94
|
+
order
|
|
72
95
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
callbacksRef.current.onResize = onResize;
|
|
96
|
+
const devWarningsRef = useRef({
|
|
97
|
+
didLogMissingDefaultSizeWarning: false
|
|
76
98
|
});
|
|
77
99
|
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (defaultSize < 0 || defaultSize > 100) {
|
|
86
|
-
throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
|
|
87
|
-
} else if (minSize > defaultSize && !collapsible) {
|
|
88
|
-
console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
|
|
89
|
-
defaultSize = minSize;
|
|
100
|
+
// Normally we wouldn't log a warning during render,
|
|
101
|
+
// but effects don't run on the server, so we can't do it there
|
|
102
|
+
{
|
|
103
|
+
if (!devWarningsRef.current.didLogMissingDefaultSizeWarning) {
|
|
104
|
+
if (defaultSizePercentage == null && defaultSizePixels == null) {
|
|
105
|
+
devWarningsRef.current.didLogMissingDefaultSizeWarning = true;
|
|
106
|
+
console.warn(`WARNING: Panel defaultSizePercentage or defaultSizePixels prop recommended to avoid layout shift after server rendering`);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
|
-
const style = getPanelStyle(panelId, defaultSize);
|
|
94
|
-
const committedValuesRef = useRef({
|
|
95
|
-
size: parseSizeFromStyle(style)
|
|
96
|
-
});
|
|
97
|
-
useRef({
|
|
98
|
-
callbacksRef,
|
|
99
|
-
collapsedSize,
|
|
100
|
-
collapsible,
|
|
101
|
-
defaultSize,
|
|
102
|
-
id: panelId,
|
|
103
|
-
idWasAutoGenerated: idFromProps == null,
|
|
104
|
-
maxSize,
|
|
105
|
-
minSize,
|
|
106
|
-
order
|
|
107
|
-
});
|
|
108
110
|
useImperativeHandle(forwardedRef, () => ({
|
|
109
|
-
collapse: () =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
collapse: () => {
|
|
112
|
+
collapsePanel(panelDataRef.current);
|
|
113
|
+
},
|
|
114
|
+
expand: () => {
|
|
115
|
+
expandPanel(panelDataRef.current);
|
|
116
|
+
},
|
|
117
|
+
getId() {
|
|
118
|
+
return panelId;
|
|
113
119
|
},
|
|
114
120
|
getSize() {
|
|
115
|
-
return
|
|
121
|
+
return getPanelSize(panelDataRef.current);
|
|
122
|
+
},
|
|
123
|
+
isCollapsed() {
|
|
124
|
+
return isPanelCollapsed(panelDataRef.current);
|
|
116
125
|
},
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
isExpanded() {
|
|
127
|
+
return !isPanelCollapsed(panelDataRef.current);
|
|
128
|
+
},
|
|
129
|
+
resize: mixedSizes => {
|
|
130
|
+
resizePanel(panelDataRef.current, mixedSizes);
|
|
131
|
+
}
|
|
132
|
+
}), [collapsePanel, expandPanel, getPanelSize, isPanelCollapsed, panelId, resizePanel]);
|
|
133
|
+
const style = getPanelStyle(panelDataRef.current);
|
|
119
134
|
return createElement(Type, {
|
|
120
135
|
children,
|
|
121
136
|
className: classNameFromProps,
|
|
122
|
-
"data-panel": "",
|
|
123
|
-
"data-panel-collapsible": collapsible || undefined,
|
|
124
|
-
"data-panel-id": panelId,
|
|
125
|
-
"data-panel-size": parseFloat("" + style.flexGrow).toFixed(1),
|
|
126
|
-
id: `data-panel-id-${panelId}`,
|
|
127
137
|
style: {
|
|
128
138
|
...style,
|
|
129
139
|
...styleFromProps
|
|
130
|
-
}
|
|
140
|
+
},
|
|
141
|
+
// CSS selectors
|
|
142
|
+
"data-panel": "",
|
|
143
|
+
// e2e test attributes
|
|
144
|
+
"data-panel-collapsible": collapsible || undefined ,
|
|
145
|
+
"data-panel-id": panelId ,
|
|
146
|
+
"data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
|
|
131
147
|
});
|
|
132
148
|
}
|
|
133
149
|
const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
|
|
@@ -137,33 +153,133 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
|
|
|
137
153
|
PanelWithForwardedRef.displayName = "Panel";
|
|
138
154
|
Panel.displayName = "forwardRef(Panel)";
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
const PRECISION = 10;
|
|
157
|
+
|
|
158
|
+
function convertPixelsToPercentage(pixels, groupSizePixels) {
|
|
159
|
+
return pixels / groupSizePixels * 100;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function convertPixelConstraintsToPercentages(panelConstraints, groupSizePixels) {
|
|
163
|
+
let {
|
|
164
|
+
collapsedSizePercentage = 0,
|
|
165
|
+
collapsedSizePixels,
|
|
166
|
+
defaultSizePercentage,
|
|
167
|
+
defaultSizePixels,
|
|
168
|
+
maxSizePercentage = 100,
|
|
169
|
+
maxSizePixels,
|
|
170
|
+
minSizePercentage = 0,
|
|
171
|
+
minSizePixels
|
|
172
|
+
} = panelConstraints;
|
|
173
|
+
if (collapsedSizePixels != null) {
|
|
174
|
+
collapsedSizePercentage = convertPixelsToPercentage(collapsedSizePixels, groupSizePixels);
|
|
175
|
+
}
|
|
176
|
+
if (defaultSizePixels != null) {
|
|
177
|
+
defaultSizePercentage = convertPixelsToPercentage(defaultSizePixels, groupSizePixels);
|
|
178
|
+
}
|
|
179
|
+
if (minSizePixels != null) {
|
|
180
|
+
minSizePercentage = convertPixelsToPercentage(minSizePixels, groupSizePixels);
|
|
181
|
+
}
|
|
182
|
+
if (maxSizePixels != null) {
|
|
183
|
+
maxSizePercentage = convertPixelsToPercentage(maxSizePixels, groupSizePixels);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
collapsedSizePercentage,
|
|
187
|
+
defaultSizePercentage,
|
|
188
|
+
maxSizePercentage,
|
|
189
|
+
minSizePercentage
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels) {
|
|
194
|
+
// All panel constraints, excluding the current one
|
|
195
|
+
let totalMinConstraints = 0;
|
|
196
|
+
let totalMaxConstraints = 0;
|
|
197
|
+
for (let index = 0; index < panelConstraintsArray.length; index++) {
|
|
198
|
+
if (index !== panelIndex) {
|
|
199
|
+
const {
|
|
200
|
+
collapsible
|
|
201
|
+
} = panelConstraintsArray[index];
|
|
202
|
+
const {
|
|
203
|
+
collapsedSizePercentage,
|
|
204
|
+
maxSizePercentage,
|
|
205
|
+
minSizePercentage
|
|
206
|
+
} = convertPixelConstraintsToPercentages(panelConstraintsArray[index], groupSizePixels);
|
|
207
|
+
totalMaxConstraints += maxSizePercentage;
|
|
208
|
+
totalMinConstraints += collapsible ? collapsedSizePercentage : minSizePercentage;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
142
211
|
const {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
212
|
+
collapsedSizePercentage,
|
|
213
|
+
defaultSizePercentage,
|
|
214
|
+
maxSizePercentage,
|
|
215
|
+
minSizePercentage
|
|
216
|
+
} = convertPixelConstraintsToPercentages(panelConstraintsArray[panelIndex], groupSizePixels);
|
|
217
|
+
return {
|
|
218
|
+
collapsedSizePercentage,
|
|
219
|
+
defaultSizePercentage,
|
|
220
|
+
maxSizePercentage: panelConstraintsArray.length > 1 ? Math.min(maxSizePercentage, 100 - totalMinConstraints) : maxSizePercentage,
|
|
221
|
+
minSizePercentage: panelConstraintsArray.length > 1 ? Math.max(minSizePercentage, 100 - totalMaxConstraints) : minSizePercentage
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
|
|
226
|
+
actual = parseFloat(actual.toFixed(fractionDigits));
|
|
227
|
+
expected = parseFloat(expected.toFixed(fractionDigits));
|
|
228
|
+
const delta = actual - expected;
|
|
229
|
+
if (delta === 0) {
|
|
230
|
+
return 0;
|
|
147
231
|
} else {
|
|
148
|
-
return
|
|
232
|
+
return delta > 0 ? 1 : -1;
|
|
149
233
|
}
|
|
150
234
|
}
|
|
151
235
|
|
|
152
|
-
|
|
236
|
+
function fuzzyNumbersEqual(actual, expected, fractionDigits) {
|
|
237
|
+
return fuzzyCompareNumbers(actual, expected, fractionDigits) === 0;
|
|
238
|
+
}
|
|
153
239
|
|
|
154
|
-
|
|
240
|
+
// Panel size must be in percentages; pixel values should be pre-converted
|
|
241
|
+
function resizePanel({
|
|
242
|
+
groupSizePixels,
|
|
243
|
+
panelConstraints,
|
|
244
|
+
panelIndex,
|
|
245
|
+
size
|
|
246
|
+
}) {
|
|
247
|
+
let {
|
|
248
|
+
collapsible
|
|
249
|
+
} = panelConstraints[panelIndex];
|
|
155
250
|
const {
|
|
156
|
-
|
|
157
|
-
|
|
251
|
+
collapsedSizePercentage,
|
|
252
|
+
maxSizePercentage,
|
|
253
|
+
minSizePercentage
|
|
254
|
+
} = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
|
|
255
|
+
if (minSizePercentage != null) {
|
|
256
|
+
if (fuzzyCompareNumbers(size, minSizePercentage) < 0) {
|
|
257
|
+
if (collapsible) {
|
|
258
|
+
size = collapsedSizePercentage;
|
|
259
|
+
} else {
|
|
260
|
+
size = minSizePercentage;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (maxSizePercentage != null) {
|
|
265
|
+
size = Math.min(maxSizePercentage, size);
|
|
266
|
+
}
|
|
267
|
+
return size;
|
|
268
|
+
}
|
|
158
269
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
270
|
+
// All units must be in percentages; pixel values should be pre-converted
|
|
271
|
+
function adjustLayoutByDelta({
|
|
272
|
+
delta,
|
|
273
|
+
groupSizePixels,
|
|
274
|
+
layout: prevLayout,
|
|
275
|
+
panelConstraints,
|
|
276
|
+
pivotIndices,
|
|
277
|
+
trigger
|
|
278
|
+
}) {
|
|
279
|
+
if (fuzzyNumbersEqual(delta, 0)) {
|
|
280
|
+
return prevLayout;
|
|
164
281
|
}
|
|
165
|
-
const
|
|
166
|
-
const nextSizes = baseSizes.concat();
|
|
282
|
+
const nextLayout = [...prevLayout];
|
|
167
283
|
let deltaApplied = 0;
|
|
168
284
|
|
|
169
285
|
// A resizing panel affects the panels before or after it.
|
|
@@ -174,268 +290,271 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
|
|
|
174
290
|
// A positive delta means the panel immediately before the resizer should "expand".
|
|
175
291
|
// This is accomplished by shrinking/contracting (and shifting) one or more of the panels after the resizer.
|
|
176
292
|
|
|
177
|
-
//
|
|
293
|
+
// First, check the panel we're pivoting around;
|
|
294
|
+
// We should only expand or contract by as much as its constraints allow
|
|
178
295
|
{
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
296
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
297
|
+
const initialSize = nextLayout[pivotIndex];
|
|
298
|
+
const {
|
|
299
|
+
collapsible
|
|
300
|
+
} = panelConstraints[pivotIndex];
|
|
301
|
+
const {
|
|
302
|
+
collapsedSizePercentage,
|
|
303
|
+
maxSizePercentage,
|
|
304
|
+
minSizePercentage
|
|
305
|
+
} = computePercentagePanelConstraints(panelConstraints, pivotIndex, groupSizePixels);
|
|
306
|
+
const isCollapsed = collapsible && fuzzyNumbersEqual(initialSize, collapsedSizePercentage);
|
|
307
|
+
let unsafeSize = initialSize + Math.abs(delta);
|
|
308
|
+
if (isCollapsed) {
|
|
309
|
+
switch (trigger) {
|
|
310
|
+
case "keyboard":
|
|
311
|
+
if (minSizePercentage > unsafeSize) {
|
|
312
|
+
unsafeSize = minSizePercentage;
|
|
313
|
+
}
|
|
190
314
|
}
|
|
191
|
-
delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
|
|
192
315
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
deltaApplied += baseSize - nextSize;
|
|
206
|
-
nextSizes[index] = nextSize;
|
|
207
|
-
if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
|
|
208
|
-
numeric: true
|
|
209
|
-
}) >= 0) {
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
316
|
+
const safeSize = resizePanel({
|
|
317
|
+
groupSizePixels,
|
|
318
|
+
panelConstraints,
|
|
319
|
+
panelIndex: pivotIndex,
|
|
320
|
+
size: unsafeSize
|
|
321
|
+
});
|
|
322
|
+
if (fuzzyNumbersEqual(initialSize, safeSize)) {
|
|
323
|
+
// If there's no room for the pivot panel to grow, we should ignore this change
|
|
324
|
+
return nextLayout;
|
|
325
|
+
} else {
|
|
326
|
+
delta = delta < 0 ? initialSize - safeSize : safeSize - initialSize;
|
|
212
327
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Delta added to a panel needs to be subtracted from other panels
|
|
331
|
+
// within the constraints that those panels allow
|
|
332
|
+
{
|
|
333
|
+
const pivotIndex = delta < 0 ? pivotIndices[0] : pivotIndices[1];
|
|
334
|
+
let index = pivotIndex;
|
|
335
|
+
while (index >= 0 && index < panelConstraints.length) {
|
|
336
|
+
const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
|
|
337
|
+
const prevSize = prevLayout[index];
|
|
338
|
+
const unsafeSize = prevSize - deltaRemaining;
|
|
339
|
+
let safeSize = resizePanel({
|
|
340
|
+
groupSizePixels,
|
|
341
|
+
panelConstraints,
|
|
342
|
+
panelIndex: index,
|
|
343
|
+
size: unsafeSize
|
|
344
|
+
});
|
|
345
|
+
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
|
|
346
|
+
deltaApplied += prevSize - safeSize;
|
|
347
|
+
nextLayout[index] = safeSize;
|
|
348
|
+
if (deltaApplied.toPrecision(3).localeCompare(Math.abs(delta).toPrecision(3), undefined, {
|
|
349
|
+
numeric: true
|
|
350
|
+
}) >= 0) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
216
353
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
354
|
+
if (delta < 0) {
|
|
355
|
+
index--;
|
|
356
|
+
} else {
|
|
357
|
+
index++;
|
|
220
358
|
}
|
|
221
359
|
}
|
|
222
360
|
}
|
|
223
361
|
|
|
224
362
|
// If we were unable to resize any of the panels panels, return the previous state.
|
|
225
|
-
// This will essentially bailout and ignore
|
|
226
|
-
if (deltaApplied
|
|
227
|
-
return
|
|
363
|
+
// This will essentially bailout and ignore e.g. drags past a panel's boundaries
|
|
364
|
+
if (fuzzyNumbersEqual(deltaApplied, 0)) {
|
|
365
|
+
return prevLayout;
|
|
228
366
|
}
|
|
367
|
+
{
|
|
368
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
369
|
+
const unsafeSize = prevLayout[pivotIndex] + deltaApplied;
|
|
370
|
+
const safeSize = resizePanel({
|
|
371
|
+
groupSizePixels,
|
|
372
|
+
panelConstraints,
|
|
373
|
+
panelIndex: pivotIndex,
|
|
374
|
+
size: unsafeSize
|
|
375
|
+
});
|
|
229
376
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
377
|
+
// Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
|
|
378
|
+
nextLayout[pivotIndex] = safeSize;
|
|
379
|
+
|
|
380
|
+
// Edge case where expanding or contracting one panel caused another one to change collapsed state
|
|
381
|
+
if (!fuzzyNumbersEqual(safeSize, unsafeSize)) {
|
|
382
|
+
let deltaRemaining = unsafeSize - safeSize;
|
|
383
|
+
const pivotIndex = delta < 0 ? pivotIndices[1] : pivotIndices[0];
|
|
384
|
+
let index = pivotIndex;
|
|
385
|
+
while (index >= 0 && index < panelConstraints.length) {
|
|
386
|
+
const prevSize = nextLayout[index];
|
|
387
|
+
const unsafeSize = prevSize + deltaRemaining;
|
|
388
|
+
const safeSize = resizePanel({
|
|
389
|
+
groupSizePixels,
|
|
390
|
+
panelConstraints,
|
|
391
|
+
panelIndex: index,
|
|
392
|
+
size: unsafeSize
|
|
393
|
+
});
|
|
394
|
+
if (!fuzzyNumbersEqual(prevSize, safeSize)) {
|
|
395
|
+
deltaRemaining -= safeSize - prevSize;
|
|
396
|
+
nextLayout[index] = safeSize;
|
|
397
|
+
}
|
|
398
|
+
if (fuzzyNumbersEqual(deltaRemaining, 0)) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
if (delta > 0) {
|
|
402
|
+
index--;
|
|
403
|
+
} else {
|
|
404
|
+
index++;
|
|
405
|
+
}
|
|
259
406
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
407
|
+
|
|
408
|
+
// If we can't redistribute, this layout is invalid;
|
|
409
|
+
// There may be an incremental layout that is valid though
|
|
410
|
+
if (!fuzzyNumbersEqual(deltaRemaining, 0)) {
|
|
411
|
+
try {
|
|
412
|
+
return adjustLayoutByDelta({
|
|
413
|
+
delta: delta < 0 ? delta + 1 : delta - 1,
|
|
414
|
+
groupSizePixels,
|
|
415
|
+
layout: prevLayout,
|
|
416
|
+
panelConstraints,
|
|
417
|
+
pivotIndices,
|
|
418
|
+
trigger
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error instanceof RangeError) {
|
|
422
|
+
console.error(`Could not apply delta ${delta} to layout`);
|
|
423
|
+
return prevLayout;
|
|
424
|
+
}
|
|
425
|
+
} finally {
|
|
265
426
|
}
|
|
266
427
|
}
|
|
267
428
|
}
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
function getBeforeAndAfterIds(id, panelsArray) {
|
|
271
|
-
if (panelsArray.length < 2) {
|
|
272
|
-
return [null, null];
|
|
273
429
|
}
|
|
274
|
-
|
|
275
|
-
if (index < 0) {
|
|
276
|
-
return [null, null];
|
|
277
|
-
}
|
|
278
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
279
|
-
const idBefore = isLastPanel ? panelsArray[index - 1].current.id : id;
|
|
280
|
-
const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
|
|
281
|
-
return [idBefore, idAfter];
|
|
430
|
+
return nextLayout;
|
|
282
431
|
}
|
|
283
432
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return "100";
|
|
289
|
-
}
|
|
290
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
291
|
-
const index = panelsArray.findIndex(panel => panel.current.id === id);
|
|
292
|
-
const size = sizes[index];
|
|
293
|
-
if (size == null) {
|
|
294
|
-
return "0";
|
|
433
|
+
function assert(expectedCondition, message = "Assertion failed!") {
|
|
434
|
+
if (!expectedCondition) {
|
|
435
|
+
console.error(message);
|
|
436
|
+
throw Error(message);
|
|
295
437
|
}
|
|
296
|
-
return size.toPrecision(PRECISION);
|
|
297
438
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
439
|
+
|
|
440
|
+
function getPercentageSizeFromMixedSizes({
|
|
441
|
+
sizePercentage,
|
|
442
|
+
sizePixels
|
|
443
|
+
}, groupSizePixels) {
|
|
444
|
+
if (sizePercentage != null) {
|
|
445
|
+
return sizePercentage;
|
|
446
|
+
} else if (sizePixels != null) {
|
|
447
|
+
return convertPixelsToPercentage(sizePixels, groupSizePixels);
|
|
302
448
|
}
|
|
303
|
-
return
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getResizeHandleElementsForGroup(groupId) {
|
|
453
|
+
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getResizeHandleElementIndex(groupId, id) {
|
|
457
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
458
|
+
const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
|
|
459
|
+
return index ?? null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function determinePivotIndices(groupId, dragHandleId) {
|
|
463
|
+
const index = getResizeHandleElementIndex(groupId, dragHandleId);
|
|
464
|
+
return index != null ? [index, index + 1] : [-1, -1];
|
|
304
465
|
}
|
|
305
|
-
|
|
466
|
+
|
|
467
|
+
function getPanelGroupElement(id) {
|
|
306
468
|
const element = document.querySelector(`[data-panel-group-id="${id}"]`);
|
|
307
469
|
if (element) {
|
|
308
470
|
return element;
|
|
309
471
|
}
|
|
310
472
|
return null;
|
|
311
473
|
}
|
|
312
|
-
|
|
474
|
+
|
|
475
|
+
function calculateAvailablePanelSizeInPixels(groupId) {
|
|
476
|
+
const panelGroupElement = getPanelGroupElement(groupId);
|
|
477
|
+
if (panelGroupElement == null) {
|
|
478
|
+
return NaN;
|
|
479
|
+
}
|
|
480
|
+
const direction = panelGroupElement.getAttribute("data-panel-group-direction");
|
|
481
|
+
const resizeHandles = getResizeHandleElementsForGroup(groupId);
|
|
482
|
+
if (direction === "horizontal") {
|
|
483
|
+
return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
|
|
484
|
+
return accumulated + handle.offsetWidth;
|
|
485
|
+
}, 0);
|
|
486
|
+
} else {
|
|
487
|
+
return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
|
|
488
|
+
return accumulated + handle.offsetHeight;
|
|
489
|
+
}, 0);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function getAvailableGroupSizePixels(groupId) {
|
|
494
|
+
const panelGroupElement = getPanelGroupElement(groupId);
|
|
495
|
+
if (panelGroupElement == null) {
|
|
496
|
+
return NaN;
|
|
497
|
+
}
|
|
498
|
+
const direction = panelGroupElement.getAttribute("data-panel-group-direction");
|
|
499
|
+
const resizeHandles = getResizeHandleElementsForGroup(groupId);
|
|
500
|
+
if (direction === "horizontal") {
|
|
501
|
+
return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
|
|
502
|
+
return accumulated + handle.offsetWidth;
|
|
503
|
+
}, 0);
|
|
504
|
+
} else {
|
|
505
|
+
return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
|
|
506
|
+
return accumulated + handle.offsetHeight;
|
|
507
|
+
}, 0);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function getResizeHandleElement(id) {
|
|
313
512
|
const element = document.querySelector(`[data-panel-resize-handle-id="${id}"]`);
|
|
314
513
|
if (element) {
|
|
315
514
|
return element;
|
|
316
515
|
}
|
|
317
516
|
return null;
|
|
318
517
|
}
|
|
319
|
-
|
|
320
|
-
const handles = getResizeHandles();
|
|
321
|
-
const index = handles.findIndex(handle => handle.getAttribute("data-panel-resize-handle-id") === id);
|
|
322
|
-
return index ?? null;
|
|
323
|
-
}
|
|
324
|
-
function getResizeHandles() {
|
|
325
|
-
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id]`));
|
|
326
|
-
}
|
|
327
|
-
function getResizeHandlesForGroup(groupId) {
|
|
328
|
-
return Array.from(document.querySelectorAll(`[data-panel-resize-handle-id][data-panel-group-id="${groupId}"]`));
|
|
329
|
-
}
|
|
518
|
+
|
|
330
519
|
function getResizeHandlePanelIds(groupId, handleId, panelsArray) {
|
|
331
|
-
const handle =
|
|
332
|
-
const handles =
|
|
520
|
+
const handle = getResizeHandleElement(handleId);
|
|
521
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
333
522
|
const index = handle ? handles.indexOf(handle) : -1;
|
|
334
|
-
const idBefore = panelsArray[index]?.
|
|
335
|
-
const idAfter = panelsArray[index + 1]?.
|
|
523
|
+
const idBefore = panelsArray[index]?.id ?? null;
|
|
524
|
+
const idAfter = panelsArray[index + 1]?.id ?? null;
|
|
336
525
|
return [idBefore, idAfter];
|
|
337
526
|
}
|
|
338
|
-
function panelsMapToSortedArray(panels) {
|
|
339
|
-
return Array.from(panels.values()).sort((panelA, panelB) => {
|
|
340
|
-
const orderA = panelA.current.order;
|
|
341
|
-
const orderB = panelB.current.order;
|
|
342
|
-
if (orderA == null && orderB == null) {
|
|
343
|
-
return 0;
|
|
344
|
-
} else if (orderA == null) {
|
|
345
|
-
return -1;
|
|
346
|
-
} else if (orderB == null) {
|
|
347
|
-
return 1;
|
|
348
|
-
} else {
|
|
349
|
-
return orderA - orderB;
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
function safeResizePanel(panel, delta, prevSize, event) {
|
|
354
|
-
const nextSizeUnsafe = prevSize + delta;
|
|
355
|
-
const {
|
|
356
|
-
collapsedSize,
|
|
357
|
-
collapsible,
|
|
358
|
-
maxSize,
|
|
359
|
-
minSize
|
|
360
|
-
} = panel.current;
|
|
361
|
-
if (collapsible) {
|
|
362
|
-
if (prevSize > collapsedSize) {
|
|
363
|
-
// Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
|
|
364
|
-
if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
|
|
365
|
-
return collapsedSize;
|
|
366
|
-
}
|
|
367
|
-
} else {
|
|
368
|
-
const isKeyboardEvent = event?.type?.startsWith("key");
|
|
369
|
-
if (!isKeyboardEvent) {
|
|
370
|
-
// Keyboard events should expand a collapsed panel to the min size,
|
|
371
|
-
// but mouse events should wait until the panel has reached its min size
|
|
372
|
-
// to avoid a visual flickering when dragging between collapsed and min size.
|
|
373
|
-
if (nextSizeUnsafe < minSize) {
|
|
374
|
-
return collapsedSize;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
|
|
380
|
-
return nextSize;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function assert(expectedCondition, message = "Assertion failed!") {
|
|
384
|
-
if (!expectedCondition) {
|
|
385
|
-
console.error(message);
|
|
386
|
-
throw Error(message);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
527
|
|
|
390
528
|
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
391
529
|
|
|
392
530
|
function useWindowSplitterPanelGroupBehavior({
|
|
393
531
|
committedValuesRef,
|
|
394
532
|
groupId,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
panelSizeBeforeCollapse
|
|
533
|
+
layout,
|
|
534
|
+
panelDataArray,
|
|
535
|
+
setLayout
|
|
399
536
|
}) {
|
|
537
|
+
useRef({
|
|
538
|
+
didWarnAboutMissingResizeHandle: false
|
|
539
|
+
});
|
|
400
540
|
useEffect(() => {
|
|
401
541
|
const {
|
|
402
542
|
direction,
|
|
403
|
-
|
|
543
|
+
panelDataArray
|
|
404
544
|
} = committedValuesRef.current;
|
|
405
|
-
const groupElement =
|
|
545
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
546
|
+
assert(groupElement != null, `No group found for id "${groupId}"`);
|
|
406
547
|
const {
|
|
407
548
|
height,
|
|
408
549
|
width
|
|
409
550
|
} = groupElement.getBoundingClientRect();
|
|
410
|
-
const handles =
|
|
551
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
411
552
|
const cleanupFunctions = handles.map(handle => {
|
|
412
553
|
const handleId = handle.getAttribute("data-panel-resize-handle-id");
|
|
413
|
-
const
|
|
414
|
-
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
|
|
554
|
+
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelDataArray);
|
|
415
555
|
if (idBefore == null || idAfter == null) {
|
|
416
556
|
return () => {};
|
|
417
557
|
}
|
|
418
|
-
let minSize = 0;
|
|
419
|
-
let maxSize = 100;
|
|
420
|
-
let totalMinSize = 0;
|
|
421
|
-
let totalMaxSize = 0;
|
|
422
|
-
|
|
423
|
-
// A panel's effective min/max sizes also need to account for other panel's sizes.
|
|
424
|
-
panelsArray.forEach(panelData => {
|
|
425
|
-
if (panelData.current.id === idBefore) {
|
|
426
|
-
maxSize = panelData.current.maxSize;
|
|
427
|
-
minSize = panelData.current.minSize;
|
|
428
|
-
} else {
|
|
429
|
-
totalMinSize += panelData.current.minSize;
|
|
430
|
-
totalMaxSize += panelData.current.maxSize;
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
|
|
434
|
-
const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
|
|
435
|
-
const flexGrow = getFlexGrow(panels, idBefore, sizes);
|
|
436
|
-
handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
|
|
437
|
-
handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
|
|
438
|
-
handle.setAttribute("aria-valuenow", "" + Math.round(parseInt(flexGrow)));
|
|
439
558
|
const onKeyDown = event => {
|
|
440
559
|
if (event.defaultPrevented) {
|
|
441
560
|
return;
|
|
@@ -444,20 +563,32 @@ function useWindowSplitterPanelGroupBehavior({
|
|
|
444
563
|
case "Enter":
|
|
445
564
|
{
|
|
446
565
|
event.preventDefault();
|
|
447
|
-
const index =
|
|
566
|
+
const index = panelDataArray.findIndex(panelData => panelData.id === idBefore);
|
|
448
567
|
if (index >= 0) {
|
|
449
|
-
const panelData =
|
|
450
|
-
const size =
|
|
568
|
+
const panelData = panelDataArray[index];
|
|
569
|
+
const size = layout[index];
|
|
451
570
|
if (size != null) {
|
|
571
|
+
const groupSizePixels = getAvailableGroupSizePixels(groupId);
|
|
572
|
+
const minSize = getPercentageSizeFromMixedSizes({
|
|
573
|
+
sizePercentage: panelData.constraints.minSizePercentage,
|
|
574
|
+
sizePixels: panelData.constraints.minSizePixels
|
|
575
|
+
}, groupSizePixels) ?? 0;
|
|
452
576
|
let delta = 0;
|
|
453
|
-
if (size.toPrecision(PRECISION) <=
|
|
577
|
+
if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
|
|
454
578
|
delta = direction === "horizontal" ? width : height;
|
|
455
579
|
} else {
|
|
456
580
|
delta = -(direction === "horizontal" ? width : height);
|
|
457
581
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
582
|
+
const nextLayout = adjustLayoutByDelta({
|
|
583
|
+
delta,
|
|
584
|
+
groupSizePixels,
|
|
585
|
+
layout,
|
|
586
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints),
|
|
587
|
+
pivotIndices: determinePivotIndices(groupId, handleId),
|
|
588
|
+
trigger: "keyboard"
|
|
589
|
+
});
|
|
590
|
+
if (layout !== nextLayout) {
|
|
591
|
+
setLayout(nextLayout);
|
|
461
592
|
}
|
|
462
593
|
}
|
|
463
594
|
}
|
|
@@ -466,72 +597,14 @@ function useWindowSplitterPanelGroupBehavior({
|
|
|
466
597
|
}
|
|
467
598
|
};
|
|
468
599
|
handle.addEventListener("keydown", onKeyDown);
|
|
469
|
-
const panelBefore = getPanel(idBefore);
|
|
470
|
-
if (panelBefore != null) {
|
|
471
|
-
handle.setAttribute("aria-controls", panelBefore.id);
|
|
472
|
-
}
|
|
473
600
|
return () => {
|
|
474
|
-
handle.removeAttribute("aria-valuemax");
|
|
475
|
-
handle.removeAttribute("aria-valuemin");
|
|
476
|
-
handle.removeAttribute("aria-valuenow");
|
|
477
601
|
handle.removeEventListener("keydown", onKeyDown);
|
|
478
|
-
if (panelBefore != null) {
|
|
479
|
-
handle.removeAttribute("aria-controls");
|
|
480
|
-
}
|
|
481
602
|
};
|
|
482
603
|
});
|
|
483
604
|
return () => {
|
|
484
605
|
cleanupFunctions.forEach(cleanupFunction => cleanupFunction());
|
|
485
606
|
};
|
|
486
|
-
}, [committedValuesRef, groupId,
|
|
487
|
-
}
|
|
488
|
-
function useWindowSplitterResizeHandlerBehavior({
|
|
489
|
-
disabled,
|
|
490
|
-
handleId,
|
|
491
|
-
resizeHandler
|
|
492
|
-
}) {
|
|
493
|
-
useEffect(() => {
|
|
494
|
-
if (disabled || resizeHandler == null) {
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const handleElement = getResizeHandle(handleId);
|
|
498
|
-
if (handleElement == null) {
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
const onKeyDown = event => {
|
|
502
|
-
if (event.defaultPrevented) {
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
switch (event.key) {
|
|
506
|
-
case "ArrowDown":
|
|
507
|
-
case "ArrowLeft":
|
|
508
|
-
case "ArrowRight":
|
|
509
|
-
case "ArrowUp":
|
|
510
|
-
case "End":
|
|
511
|
-
case "Home":
|
|
512
|
-
{
|
|
513
|
-
event.preventDefault();
|
|
514
|
-
resizeHandler(event);
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
case "F6":
|
|
518
|
-
{
|
|
519
|
-
event.preventDefault();
|
|
520
|
-
const handles = getResizeHandles();
|
|
521
|
-
const index = getResizeHandleIndex(handleId);
|
|
522
|
-
assert(index !== null);
|
|
523
|
-
const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
|
|
524
|
-
const nextHandle = handles[nextIndex];
|
|
525
|
-
nextHandle.focus();
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
handleElement.addEventListener("keydown", onKeyDown);
|
|
531
|
-
return () => {
|
|
532
|
-
handleElement.removeEventListener("keydown", onKeyDown);
|
|
533
|
-
};
|
|
534
|
-
}, [disabled, handleId, resizeHandler]);
|
|
607
|
+
}, [committedValuesRef, groupId, layout, panelDataArray, setLayout]);
|
|
535
608
|
}
|
|
536
609
|
|
|
537
610
|
function areEqual(arrayA, arrayB) {
|
|
@@ -546,41 +619,61 @@ function areEqual(arrayA, arrayB) {
|
|
|
546
619
|
return true;
|
|
547
620
|
}
|
|
548
621
|
|
|
549
|
-
function
|
|
622
|
+
function isKeyDown(event) {
|
|
623
|
+
return event.type === "keydown";
|
|
624
|
+
}
|
|
625
|
+
function isMouseEvent(event) {
|
|
626
|
+
return event.type.startsWith("mouse");
|
|
627
|
+
}
|
|
628
|
+
function isTouchEvent(event) {
|
|
629
|
+
return event.type.startsWith("touch");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function getResizeEventCursorPosition(direction, event) {
|
|
550
633
|
const isHorizontal = direction === "horizontal";
|
|
551
|
-
let pointerOffset = 0;
|
|
552
634
|
if (isMouseEvent(event)) {
|
|
553
|
-
|
|
635
|
+
return isHorizontal ? event.clientX : event.clientY;
|
|
554
636
|
} else if (isTouchEvent(event)) {
|
|
555
637
|
const firstTouch = event.touches[0];
|
|
556
|
-
|
|
638
|
+
return isHorizontal ? firstTouch.screenX : firstTouch.screenY;
|
|
557
639
|
} else {
|
|
558
|
-
|
|
640
|
+
throw Error(`Unsupported event type "${event.type}"`);
|
|
559
641
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState) {
|
|
645
|
+
const isHorizontal = direction === "horizontal";
|
|
646
|
+
const handleElement = getResizeHandleElement(dragHandleId);
|
|
647
|
+
const groupId = handleElement.getAttribute("data-panel-group-id");
|
|
648
|
+
let {
|
|
649
|
+
initialCursorPosition
|
|
650
|
+
} = initialDragState;
|
|
651
|
+
const cursorPosition = getResizeEventCursorPosition(direction, event);
|
|
652
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
653
|
+
const groupRect = groupElement.getBoundingClientRect();
|
|
654
|
+
const groupSizeInPixels = isHorizontal ? groupRect.width : groupRect.height;
|
|
655
|
+
const offsetPixels = cursorPosition - initialCursorPosition;
|
|
656
|
+
const offsetPercentage = offsetPixels / groupSizeInPixels * 100;
|
|
657
|
+
return offsetPercentage;
|
|
564
658
|
}
|
|
565
659
|
|
|
566
660
|
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX
|
|
567
|
-
function
|
|
568
|
-
const {
|
|
569
|
-
dragOffset = 0,
|
|
570
|
-
dragHandleRect,
|
|
571
|
-
sizes: initialSizes
|
|
572
|
-
} = initialDragState || {};
|
|
573
|
-
|
|
574
|
-
// If we're resizing by mouse or touch, use the initial sizes as a base.
|
|
575
|
-
// This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
|
|
576
|
-
const baseSizes = initialSizes || prevSizes;
|
|
661
|
+
function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initialDragState, keyboardResizeByOptions) {
|
|
577
662
|
if (isKeyDown(event)) {
|
|
578
663
|
const isHorizontal = direction === "horizontal";
|
|
579
|
-
const groupElement =
|
|
664
|
+
const groupElement = getPanelGroupElement(groupId);
|
|
580
665
|
const rect = groupElement.getBoundingClientRect();
|
|
581
666
|
const groupSizeInPixels = isHorizontal ? rect.width : rect.height;
|
|
582
|
-
|
|
583
|
-
|
|
667
|
+
let delta = 0;
|
|
668
|
+
if (event.shiftKey) {
|
|
669
|
+
delta = 100;
|
|
670
|
+
} else if (keyboardResizeByOptions.percentage != null) {
|
|
671
|
+
delta = keyboardResizeByOptions.percentage;
|
|
672
|
+
} else if (keyboardResizeByOptions.pixels != null) {
|
|
673
|
+
delta = keyboardResizeByOptions.pixels / groupSizeInPixels;
|
|
674
|
+
} else {
|
|
675
|
+
delta = 10;
|
|
676
|
+
}
|
|
584
677
|
let movement = 0;
|
|
585
678
|
switch (event.key) {
|
|
586
679
|
case "ArrowDown":
|
|
@@ -596,47 +689,121 @@ function getMovement(event, groupId, handleId, panelsArray, direction, prevSizes
|
|
|
596
689
|
movement = isHorizontal ? 0 : -delta;
|
|
597
690
|
break;
|
|
598
691
|
case "End":
|
|
599
|
-
movement =
|
|
692
|
+
movement = 100;
|
|
600
693
|
break;
|
|
601
694
|
case "Home":
|
|
602
|
-
movement = -
|
|
695
|
+
movement = -100;
|
|
603
696
|
break;
|
|
604
697
|
}
|
|
605
|
-
|
|
606
|
-
// If the Panel being resized is collapsible,
|
|
607
|
-
// we need to special case resizing around the minSize boundary.
|
|
608
|
-
// If contracting, Panels should shrink to their minSize and then snap to fully collapsed.
|
|
609
|
-
// If expanding from collapsed, they should snap back to their minSize.
|
|
610
|
-
const [idBefore, idAfter] = getResizeHandlePanelIds(groupId, handleId, panelsArray);
|
|
611
|
-
const targetPanelId = movement < 0 ? idBefore : idAfter;
|
|
612
|
-
const targetPanelIndex = panelsArray.findIndex(panel => panel.current.id === targetPanelId);
|
|
613
|
-
const targetPanel = panelsArray[targetPanelIndex];
|
|
614
|
-
if (targetPanel.current.collapsible) {
|
|
615
|
-
const baseSize = baseSizes[targetPanelIndex];
|
|
616
|
-
if (baseSize === 0 || baseSize.toPrecision(PRECISION) === targetPanel.current.minSize.toPrecision(PRECISION)) {
|
|
617
|
-
movement = movement < 0 ? -targetPanel.current.minSize * groupSizeInPixels : targetPanel.current.minSize * groupSizeInPixels;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
698
|
return movement;
|
|
621
699
|
} else {
|
|
622
|
-
return
|
|
700
|
+
return calculateDragOffsetPercentage(event, dragHandleId, direction, initialDragState);
|
|
623
701
|
}
|
|
624
702
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
function isMouseEvent(event) {
|
|
629
|
-
return event.type.startsWith("mouse");
|
|
630
|
-
}
|
|
631
|
-
function isTouchEvent(event) {
|
|
632
|
-
return event.type.startsWith("touch");
|
|
703
|
+
|
|
704
|
+
function convertPercentageToPixels(percentage, groupSizePixels) {
|
|
705
|
+
return percentage / 100 * groupSizePixels;
|
|
633
706
|
}
|
|
634
707
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
708
|
+
// Layout should be pre-converted into percentages
|
|
709
|
+
function callPanelCallbacks(groupId, panelsArray, layout, panelIdToLastNotifiedMixedSizesMap) {
|
|
710
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
711
|
+
layout.forEach((sizePercentage, index) => {
|
|
712
|
+
const panelData = panelsArray[index];
|
|
713
|
+
if (!panelData) {
|
|
714
|
+
// Handle initial mount (when panels are registered too late to be in the panels array)
|
|
715
|
+
// The subsequent render+effects will handle the resize notification
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const {
|
|
719
|
+
callbacks,
|
|
720
|
+
constraints,
|
|
721
|
+
id: panelId
|
|
722
|
+
} = panelData;
|
|
723
|
+
const {
|
|
724
|
+
collapsible
|
|
725
|
+
} = constraints;
|
|
726
|
+
const mixedSizes = {
|
|
727
|
+
sizePercentage,
|
|
728
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
729
|
+
};
|
|
730
|
+
const lastNotifiedMixedSizes = panelIdToLastNotifiedMixedSizesMap[panelId];
|
|
731
|
+
if (lastNotifiedMixedSizes == null || mixedSizes.sizePercentage !== lastNotifiedMixedSizes.sizePercentage || mixedSizes.sizePixels !== lastNotifiedMixedSizes.sizePixels) {
|
|
732
|
+
panelIdToLastNotifiedMixedSizesMap[panelId] = mixedSizes;
|
|
733
|
+
const {
|
|
734
|
+
onCollapse,
|
|
735
|
+
onExpand,
|
|
736
|
+
onResize
|
|
737
|
+
} = callbacks;
|
|
738
|
+
if (onResize) {
|
|
739
|
+
onResize(mixedSizes, lastNotifiedMixedSizes);
|
|
740
|
+
}
|
|
741
|
+
if (collapsible && (onCollapse || onExpand)) {
|
|
742
|
+
const collapsedSize = getPercentageSizeFromMixedSizes({
|
|
743
|
+
sizePercentage: constraints.collapsedSizePercentage,
|
|
744
|
+
sizePixels: constraints.collapsedSizePixels
|
|
745
|
+
}, groupSizePixels) ?? 0;
|
|
746
|
+
const size = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
|
|
747
|
+
if (onExpand && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage === collapsedSize) && size !== collapsedSize) {
|
|
748
|
+
onExpand();
|
|
749
|
+
}
|
|
750
|
+
if (onCollapse && (lastNotifiedMixedSizes == null || lastNotifiedMixedSizes.sizePercentage !== collapsedSize) && size === collapsedSize) {
|
|
751
|
+
onCollapse();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function compareLayouts(a, b) {
|
|
759
|
+
if (a.length !== b.length) {
|
|
760
|
+
return false;
|
|
761
|
+
} else {
|
|
762
|
+
for (let index = 0; index < a.length; index++) {
|
|
763
|
+
if (a[index] != b[index]) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// This method returns a number between 1 and 100 representing
|
|
772
|
+
|
|
773
|
+
// the % of the group's overall space this panel should occupy.
|
|
774
|
+
function computePanelFlexBoxStyle({
|
|
775
|
+
dragState,
|
|
776
|
+
layout,
|
|
777
|
+
panelData,
|
|
778
|
+
panelIndex,
|
|
779
|
+
precision = 3
|
|
780
|
+
}) {
|
|
781
|
+
const size = layout[panelIndex];
|
|
782
|
+
let flexGrow;
|
|
783
|
+
if (panelData.length === 1) {
|
|
784
|
+
flexGrow = "100";
|
|
785
|
+
} else if (size == null) {
|
|
786
|
+
flexGrow = "0";
|
|
787
|
+
} else {
|
|
788
|
+
flexGrow = size.toPrecision(precision);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
flexBasis: 0,
|
|
792
|
+
flexGrow,
|
|
793
|
+
flexShrink: 1,
|
|
794
|
+
// Without this, Panel sizes may be unintentionally overridden by their content
|
|
795
|
+
overflow: "hidden",
|
|
796
|
+
// Disable pointer events inside of a panel during resize
|
|
797
|
+
// This avoid edge cases like nested iframes
|
|
798
|
+
pointerEvents: dragState !== null ? "none" : undefined
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
let currentState = null;
|
|
803
|
+
let element = null;
|
|
804
|
+
function getCursorStyle(state) {
|
|
805
|
+
switch (state) {
|
|
806
|
+
case "horizontal":
|
|
640
807
|
return "ew-resize";
|
|
641
808
|
case "horizontal-max":
|
|
642
809
|
return "w-resize";
|
|
@@ -683,17 +850,47 @@ function debounce(callback, durationMs = 10) {
|
|
|
683
850
|
return callable;
|
|
684
851
|
}
|
|
685
852
|
|
|
853
|
+
// PanelGroup might be rendering in a server-side environment where localStorage is not available
|
|
854
|
+
// or on a browser with cookies/storage disabled.
|
|
855
|
+
// In either case, this function avoids accessing localStorage until needed,
|
|
856
|
+
// and avoids throwing user-visible errors.
|
|
857
|
+
function initializeDefaultStorage(storageObject) {
|
|
858
|
+
try {
|
|
859
|
+
if (typeof localStorage !== "undefined") {
|
|
860
|
+
// Bypass this check for future calls
|
|
861
|
+
storageObject.getItem = name => {
|
|
862
|
+
return localStorage.getItem(name);
|
|
863
|
+
};
|
|
864
|
+
storageObject.setItem = (name, value) => {
|
|
865
|
+
localStorage.setItem(name, value);
|
|
866
|
+
};
|
|
867
|
+
} else {
|
|
868
|
+
throw new Error("localStorage not supported in this environment");
|
|
869
|
+
}
|
|
870
|
+
} catch (error) {
|
|
871
|
+
console.error(error);
|
|
872
|
+
storageObject.getItem = () => null;
|
|
873
|
+
storageObject.setItem = () => {};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
686
877
|
// Note that Panel ids might be user-provided (stable) or useId generated (non-deterministic)
|
|
687
878
|
// so they should not be used as part of the serialization key.
|
|
688
|
-
// Using
|
|
879
|
+
// Using the min/max size attributes should work well enough as a backup.
|
|
689
880
|
// Pre-sorting by minSize allows remembering layouts even if panels are re-ordered/dragged.
|
|
690
881
|
function getSerializationKey(panels) {
|
|
691
882
|
return panels.map(panel => {
|
|
692
883
|
const {
|
|
693
|
-
|
|
884
|
+
constraints,
|
|
885
|
+
id,
|
|
886
|
+
idIsFromProps,
|
|
694
887
|
order
|
|
695
|
-
} = panel
|
|
696
|
-
|
|
888
|
+
} = panel;
|
|
889
|
+
if (idIsFromProps) {
|
|
890
|
+
return id;
|
|
891
|
+
} else {
|
|
892
|
+
return `${order}:${JSON.stringify(constraints)}`;
|
|
893
|
+
}
|
|
697
894
|
}).sort((a, b) => a.localeCompare(b)).join(",");
|
|
698
895
|
}
|
|
699
896
|
function loadSerializedPanelGroupState(autoSaveId, storage) {
|
|
@@ -719,31 +916,139 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
|
|
|
719
916
|
}
|
|
720
917
|
}
|
|
721
918
|
|
|
722
|
-
|
|
919
|
+
function validatePanelConstraints({
|
|
920
|
+
groupSizePixels,
|
|
921
|
+
panelConstraints,
|
|
922
|
+
panelId,
|
|
923
|
+
panelIndex
|
|
924
|
+
}) {
|
|
925
|
+
{
|
|
926
|
+
const warnings = [];
|
|
927
|
+
{
|
|
928
|
+
const {
|
|
929
|
+
collapsedSizePercentage,
|
|
930
|
+
collapsedSizePixels,
|
|
931
|
+
defaultSizePercentage,
|
|
932
|
+
defaultSizePixels,
|
|
933
|
+
maxSizePercentage,
|
|
934
|
+
maxSizePixels,
|
|
935
|
+
minSizePercentage,
|
|
936
|
+
minSizePixels
|
|
937
|
+
} = panelConstraints[panelIndex];
|
|
938
|
+
const conflictingUnits = [];
|
|
939
|
+
if (collapsedSizePercentage != null && collapsedSizePixels != null) {
|
|
940
|
+
conflictingUnits.push("collapsed size");
|
|
941
|
+
}
|
|
942
|
+
if (defaultSizePercentage != null && defaultSizePixels != null) {
|
|
943
|
+
conflictingUnits.push("default size");
|
|
944
|
+
}
|
|
945
|
+
if (maxSizePercentage != null && maxSizePixels != null) {
|
|
946
|
+
conflictingUnits.push("max size");
|
|
947
|
+
}
|
|
948
|
+
if (minSizePercentage != null && minSizePixels != null) {
|
|
949
|
+
conflictingUnits.push("min size");
|
|
950
|
+
}
|
|
951
|
+
if (conflictingUnits.length > 0) {
|
|
952
|
+
warnings.push(`should not specify both percentage and pixel units for: ${conflictingUnits.join(", ")}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
{
|
|
956
|
+
const {
|
|
957
|
+
collapsedSizePercentage,
|
|
958
|
+
defaultSizePercentage,
|
|
959
|
+
maxSizePercentage,
|
|
960
|
+
minSizePercentage
|
|
961
|
+
} = computePercentagePanelConstraints(panelConstraints, panelIndex, groupSizePixels);
|
|
962
|
+
if (minSizePercentage > maxSizePercentage) {
|
|
963
|
+
warnings.push(`min size (${minSizePercentage}%) should not be greater than max size (${maxSizePercentage}%)`);
|
|
964
|
+
}
|
|
965
|
+
if (defaultSizePercentage != null) {
|
|
966
|
+
if (defaultSizePercentage < 0) {
|
|
967
|
+
warnings.push("default size should not be less than 0");
|
|
968
|
+
} else if (defaultSizePercentage < minSizePercentage) {
|
|
969
|
+
warnings.push("default size should not be less than min size");
|
|
970
|
+
}
|
|
971
|
+
if (defaultSizePercentage > 100) {
|
|
972
|
+
warnings.push("default size should not be greater than 100");
|
|
973
|
+
} else if (defaultSizePercentage > maxSizePercentage) {
|
|
974
|
+
warnings.push("default size should not be greater than max size");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (collapsedSizePercentage > minSizePercentage) {
|
|
978
|
+
warnings.push("collapsed size should not be greater than min size");
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (warnings.length > 0) {
|
|
982
|
+
const name = panelId != null ? `Panel "${panelId}"` : "Panel";
|
|
983
|
+
console.warn(`${name} has an invalid configuration:\n\n${warnings.join("\n")}`);
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
723
989
|
|
|
724
|
-
//
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
990
|
+
// All units must be in percentages; pixel values should be pre-converted
|
|
991
|
+
function validatePanelGroupLayout({
|
|
992
|
+
groupSizePixels,
|
|
993
|
+
layout: prevLayout,
|
|
994
|
+
panelConstraints
|
|
995
|
+
}) {
|
|
996
|
+
const nextLayout = [...prevLayout];
|
|
997
|
+
|
|
998
|
+
// Validate layout expectations
|
|
999
|
+
if (nextLayout.length !== panelConstraints.length) {
|
|
1000
|
+
throw Error(`Invalid ${panelConstraints.length} panel layout: ${nextLayout.map(size => `${size}%`).join(", ")}`);
|
|
1001
|
+
} else if (!fuzzyNumbersEqual(nextLayout.reduce((accumulated, current) => accumulated + current, 0), 100)) {
|
|
1002
|
+
// This is not ideal so we should warn about it, but it may be recoverable in some cases
|
|
1003
|
+
// (especially if the amount is small)
|
|
1004
|
+
{
|
|
1005
|
+
console.warn(`WARNING: Invalid layout total size: ${nextLayout.map(size => `${size}%`).join(", ")}`);
|
|
740
1006
|
}
|
|
741
|
-
} catch (error) {
|
|
742
|
-
console.error(error);
|
|
743
|
-
storageObject.getItem = () => null;
|
|
744
|
-
storageObject.setItem = () => {};
|
|
745
1007
|
}
|
|
1008
|
+
let remainingSize = 0;
|
|
1009
|
+
|
|
1010
|
+
// First pass: Validate the proposed layout given each panel's constraints
|
|
1011
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
1012
|
+
const unsafeSize = nextLayout[index];
|
|
1013
|
+
const safeSize = resizePanel({
|
|
1014
|
+
groupSizePixels,
|
|
1015
|
+
panelConstraints,
|
|
1016
|
+
panelIndex: index,
|
|
1017
|
+
size: unsafeSize
|
|
1018
|
+
});
|
|
1019
|
+
if (unsafeSize != safeSize) {
|
|
1020
|
+
remainingSize += unsafeSize - safeSize;
|
|
1021
|
+
nextLayout[index] = safeSize;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// If there is additional, left over space, assign it to any panel(s) that permits it
|
|
1026
|
+
// (It's not worth taking multiple additional passes to evenly distribute)
|
|
1027
|
+
if (!fuzzyNumbersEqual(remainingSize, 0)) {
|
|
1028
|
+
for (let index = 0; index < panelConstraints.length; index++) {
|
|
1029
|
+
const prevSize = nextLayout[index];
|
|
1030
|
+
const unsafeSize = prevSize + remainingSize;
|
|
1031
|
+
const safeSize = resizePanel({
|
|
1032
|
+
groupSizePixels,
|
|
1033
|
+
panelConstraints,
|
|
1034
|
+
panelIndex: index,
|
|
1035
|
+
size: unsafeSize
|
|
1036
|
+
});
|
|
1037
|
+
if (prevSize !== safeSize) {
|
|
1038
|
+
remainingSize -= safeSize - prevSize;
|
|
1039
|
+
nextLayout[index] = safeSize;
|
|
1040
|
+
|
|
1041
|
+
// Once we've used up the remainder, bail
|
|
1042
|
+
if (fuzzyNumbersEqual(remainingSize, 0)) {
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return nextLayout;
|
|
746
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;
|
|
747
1052
|
const defaultStorage = {
|
|
748
1053
|
getItem: name => {
|
|
749
1054
|
initializeDefaultStorage(defaultStorage);
|
|
@@ -754,247 +1059,370 @@ const defaultStorage = {
|
|
|
754
1059
|
defaultStorage.setItem(name, value);
|
|
755
1060
|
}
|
|
756
1061
|
};
|
|
757
|
-
|
|
758
|
-
// Initial drag state serves a few purposes:
|
|
759
|
-
// * dragOffset:
|
|
760
|
-
// Resize is calculated by the distance between the current pointer event and the resize handle being "dragged"
|
|
761
|
-
// This value accounts for the initial offset when the touch/click starts, so the handle doesn't appear to "jump"
|
|
762
|
-
// * dragHandleRect, sizes:
|
|
763
|
-
// When resizing is done via mouse/touch event– some initial state is stored
|
|
764
|
-
// so that any panels that contract will also expand if drag direction is reversed.
|
|
765
|
-
// TODO
|
|
766
|
-
// Within an active drag, remember original positions to refine more easily on expand.
|
|
767
|
-
// Look at what the Chrome devtools Sources does.
|
|
1062
|
+
const debounceMap = {};
|
|
768
1063
|
function PanelGroupWithForwardedRef({
|
|
769
1064
|
autoSaveId,
|
|
770
|
-
children
|
|
1065
|
+
children,
|
|
771
1066
|
className: classNameFromProps = "",
|
|
772
1067
|
direction,
|
|
773
|
-
disablePointerEventsDuringResize = false,
|
|
774
1068
|
forwardedRef,
|
|
775
|
-
id: idFromProps
|
|
776
|
-
onLayout,
|
|
1069
|
+
id: idFromProps,
|
|
1070
|
+
onLayout = null,
|
|
1071
|
+
keyboardResizeByPercentage = null,
|
|
1072
|
+
keyboardResizeByPixels = null,
|
|
777
1073
|
storage = defaultStorage,
|
|
778
|
-
style: styleFromProps
|
|
1074
|
+
style: styleFromProps,
|
|
779
1075
|
tagName: Type = "div"
|
|
780
1076
|
}) {
|
|
781
1077
|
const groupId = useUniqueId(idFromProps);
|
|
782
|
-
const [
|
|
783
|
-
const [
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
// This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
|
|
788
|
-
const initialDragStateRef = useRef(null);
|
|
789
|
-
const devWarningsRef = useRef({
|
|
790
|
-
didLogDefaultSizeWarning: false,
|
|
791
|
-
didLogIdAndOrderWarning: false,
|
|
792
|
-
prevPanelIds: []
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
// Use a ref to guard against users passing inline props
|
|
796
|
-
const callbacksRef = useRef({
|
|
797
|
-
onLayout
|
|
798
|
-
});
|
|
799
|
-
useEffect(() => {
|
|
800
|
-
callbacksRef.current.onLayout = onLayout;
|
|
801
|
-
});
|
|
802
|
-
const panelIdToLastNotifiedSizeMapRef = useRef({});
|
|
803
|
-
|
|
804
|
-
// 0-1 values representing the relative size of each panel.
|
|
805
|
-
const [sizes, setSizes] = useState([]);
|
|
806
|
-
|
|
807
|
-
// Used to support imperative collapse/expand API.
|
|
808
|
-
const panelSizeBeforeCollapse = useRef(new Map());
|
|
1078
|
+
const [dragState, setDragState] = useState(null);
|
|
1079
|
+
const [layout, setLayout] = useState([]);
|
|
1080
|
+
const [panelDataArray, setPanelDataArray] = useState([]);
|
|
1081
|
+
const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
|
|
1082
|
+
const panelSizeBeforeCollapseRef = useRef(new Map());
|
|
809
1083
|
const prevDeltaRef = useRef(0);
|
|
810
|
-
|
|
811
|
-
// Store committed values to avoid unnecessarily re-running memoization/effects functions.
|
|
812
1084
|
const committedValuesRef = useRef({
|
|
813
1085
|
direction,
|
|
814
|
-
|
|
815
|
-
|
|
1086
|
+
dragState,
|
|
1087
|
+
id: groupId,
|
|
1088
|
+
keyboardResizeByPercentage,
|
|
1089
|
+
keyboardResizeByPixels,
|
|
1090
|
+
layout,
|
|
1091
|
+
onLayout,
|
|
1092
|
+
panelDataArray
|
|
1093
|
+
});
|
|
1094
|
+
const devWarningsRef = useRef({
|
|
1095
|
+
didLogIdAndOrderWarning: false,
|
|
1096
|
+
didLogPanelConstraintsWarning: false,
|
|
1097
|
+
prevPanelIds: []
|
|
816
1098
|
});
|
|
817
1099
|
useImperativeHandle(forwardedRef, () => ({
|
|
1100
|
+
getId: () => committedValuesRef.current.id,
|
|
818
1101
|
getLayout: () => {
|
|
819
1102
|
const {
|
|
820
|
-
|
|
1103
|
+
id: groupId,
|
|
1104
|
+
layout
|
|
821
1105
|
} = committedValuesRef.current;
|
|
822
|
-
|
|
1106
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1107
|
+
return layout.map(sizePercentage => {
|
|
1108
|
+
return {
|
|
1109
|
+
sizePercentage,
|
|
1110
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1111
|
+
};
|
|
1112
|
+
});
|
|
823
1113
|
},
|
|
824
|
-
setLayout:
|
|
825
|
-
const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
|
|
826
|
-
assert(total === 100, "Panel sizes must add up to 100%");
|
|
1114
|
+
setLayout: mixedSizes => {
|
|
827
1115
|
const {
|
|
828
|
-
|
|
1116
|
+
id: groupId,
|
|
1117
|
+
layout: prevLayout,
|
|
1118
|
+
onLayout,
|
|
1119
|
+
panelDataArray
|
|
829
1120
|
} = committedValuesRef.current;
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
1121
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1122
|
+
const unsafeLayout = mixedSizes.map(mixedSize => getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels));
|
|
1123
|
+
const safeLayout = validatePanelGroupLayout({
|
|
1124
|
+
groupSizePixels,
|
|
1125
|
+
layout: unsafeLayout,
|
|
1126
|
+
panelConstraints: panelDataArray.map(panelData => panelData.constraints)
|
|
1127
|
+
});
|
|
1128
|
+
if (!areEqual(prevLayout, safeLayout)) {
|
|
1129
|
+
setLayout(safeLayout);
|
|
1130
|
+
if (onLayout) {
|
|
1131
|
+
onLayout(safeLayout.map(sizePercentage => ({
|
|
1132
|
+
sizePercentage,
|
|
1133
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1134
|
+
})));
|
|
1135
|
+
}
|
|
1136
|
+
callPanelCallbacks(groupId, panelDataArray, safeLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1137
|
+
}
|
|
834
1138
|
}
|
|
835
1139
|
}), []);
|
|
836
1140
|
useWindowSplitterPanelGroupBehavior({
|
|
837
1141
|
committedValuesRef,
|
|
838
1142
|
groupId,
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
panelSizeBeforeCollapse
|
|
1143
|
+
layout,
|
|
1144
|
+
panelDataArray,
|
|
1145
|
+
setLayout
|
|
843
1146
|
});
|
|
844
|
-
|
|
845
|
-
// Notify external code when sizes have changed.
|
|
846
|
-
useEffect(() => {
|
|
847
|
-
const {
|
|
848
|
-
onLayout
|
|
849
|
-
} = callbacksRef.current;
|
|
850
|
-
const {
|
|
851
|
-
panels,
|
|
852
|
-
sizes
|
|
853
|
-
} = committedValuesRef.current;
|
|
854
|
-
|
|
855
|
-
// Don't commit layout until all panels have registered and re-rendered with their actual sizes.
|
|
856
|
-
if (sizes.length > 0) {
|
|
857
|
-
if (onLayout) {
|
|
858
|
-
onLayout(sizes);
|
|
859
|
-
}
|
|
860
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
861
|
-
|
|
862
|
-
// When possible, we notify before the next render so that rendering work can be batched together.
|
|
863
|
-
// Some cases are difficult to detect though,
|
|
864
|
-
// for example– panels that are conditionally rendered can affect the size of neighboring panels.
|
|
865
|
-
// In this case, the best we can do is notify on commit.
|
|
866
|
-
// The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases.
|
|
867
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
868
|
-
callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
|
|
869
|
-
}
|
|
870
|
-
}, [sizes]);
|
|
871
1147
|
useEffect(() => {
|
|
872
1148
|
// If this panel has been configured to persist sizing information, save sizes to local storage.
|
|
873
1149
|
if (autoSaveId) {
|
|
874
|
-
if (
|
|
1150
|
+
if (layout.length === 0 || layout.length !== panelDataArray.length) {
|
|
875
1151
|
return;
|
|
876
1152
|
}
|
|
877
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
878
1153
|
|
|
879
1154
|
// Limit the frequency of localStorage updates.
|
|
880
1155
|
if (!debounceMap[autoSaveId]) {
|
|
881
|
-
debounceMap[autoSaveId] = debounce(savePanelGroupLayout,
|
|
1156
|
+
debounceMap[autoSaveId] = debounce(savePanelGroupLayout, LOCAL_STORAGE_DEBOUNCE_INTERVAL);
|
|
882
1157
|
}
|
|
883
|
-
debounceMap[autoSaveId](autoSaveId,
|
|
1158
|
+
debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
|
|
884
1159
|
}
|
|
1160
|
+
}, [autoSaveId, layout, panelDataArray, storage]);
|
|
1161
|
+
|
|
1162
|
+
// DEV warnings
|
|
1163
|
+
useEffect(() => {
|
|
885
1164
|
{
|
|
886
1165
|
const {
|
|
887
1166
|
didLogIdAndOrderWarning,
|
|
1167
|
+
didLogPanelConstraintsWarning,
|
|
888
1168
|
prevPanelIds
|
|
889
1169
|
} = devWarningsRef.current;
|
|
890
1170
|
if (!didLogIdAndOrderWarning) {
|
|
891
1171
|
const {
|
|
892
|
-
|
|
1172
|
+
panelDataArray
|
|
893
1173
|
} = committedValuesRef.current;
|
|
894
|
-
const panelIds =
|
|
1174
|
+
const panelIds = panelDataArray.map(({
|
|
1175
|
+
id
|
|
1176
|
+
}) => id);
|
|
895
1177
|
devWarningsRef.current.prevPanelIds = panelIds;
|
|
896
1178
|
const panelsHaveChanged = prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
|
|
897
1179
|
if (panelsHaveChanged) {
|
|
898
|
-
if (
|
|
1180
|
+
if (panelDataArray.find(({
|
|
1181
|
+
idIsFromProps,
|
|
1182
|
+
order
|
|
1183
|
+
}) => !idIsFromProps || order == null)) {
|
|
899
1184
|
devWarningsRef.current.didLogIdAndOrderWarning = true;
|
|
900
1185
|
console.warn(`WARNING: Panel id and order props recommended when panels are dynamically rendered`);
|
|
901
1186
|
}
|
|
902
1187
|
}
|
|
903
1188
|
}
|
|
1189
|
+
if (!didLogPanelConstraintsWarning) {
|
|
1190
|
+
const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
|
|
1191
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1192
|
+
for (let panelIndex = 0; panelIndex < panelConstraints.length; panelIndex++) {
|
|
1193
|
+
const isValid = validatePanelConstraints({
|
|
1194
|
+
groupSizePixels,
|
|
1195
|
+
panelConstraints,
|
|
1196
|
+
panelId: panelDataArray[panelIndex].id,
|
|
1197
|
+
panelIndex
|
|
1198
|
+
});
|
|
1199
|
+
if (!isValid) {
|
|
1200
|
+
devWarningsRef.current.didLogPanelConstraintsWarning = true;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
904
1205
|
}
|
|
905
|
-
}
|
|
906
|
-
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// External APIs are safe to memoize via committed values ref
|
|
1209
|
+
const collapsePanel = useCallback(panelData => {
|
|
907
1210
|
const {
|
|
908
|
-
|
|
1211
|
+
layout: prevLayout,
|
|
1212
|
+
onLayout,
|
|
1213
|
+
panelDataArray
|
|
909
1214
|
} = committedValuesRef.current;
|
|
1215
|
+
if (panelData.constraints.collapsible) {
|
|
1216
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1217
|
+
const {
|
|
1218
|
+
collapsedSizePercentage,
|
|
1219
|
+
panelSizePercentage,
|
|
1220
|
+
pivotIndices,
|
|
1221
|
+
groupSizePixels
|
|
1222
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1223
|
+
if (panelSizePercentage !== collapsedSizePercentage) {
|
|
1224
|
+
// Store size before collapse;
|
|
1225
|
+
// This is the size that gets restored if the expand() API is used.
|
|
1226
|
+
panelSizeBeforeCollapseRef.current.set(panelData.id, panelSizePercentage);
|
|
1227
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1228
|
+
const delta = isLastPanel ? panelSizePercentage - collapsedSizePercentage : collapsedSizePercentage - panelSizePercentage;
|
|
1229
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1230
|
+
delta,
|
|
1231
|
+
groupSizePixels,
|
|
1232
|
+
layout: prevLayout,
|
|
1233
|
+
panelConstraints: panelConstraintsArray,
|
|
1234
|
+
pivotIndices,
|
|
1235
|
+
trigger: "imperative-api"
|
|
1236
|
+
});
|
|
1237
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1238
|
+
setLayout(nextLayout);
|
|
1239
|
+
if (onLayout) {
|
|
1240
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1241
|
+
sizePercentage,
|
|
1242
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1243
|
+
})));
|
|
1244
|
+
}
|
|
1245
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}, [groupId]);
|
|
910
1250
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1251
|
+
// External APIs are safe to memoize via committed values ref
|
|
1252
|
+
const expandPanel = useCallback(panelData => {
|
|
1253
|
+
const {
|
|
1254
|
+
layout: prevLayout,
|
|
1255
|
+
onLayout,
|
|
1256
|
+
panelDataArray
|
|
1257
|
+
} = committedValuesRef.current;
|
|
1258
|
+
if (panelData.constraints.collapsible) {
|
|
1259
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1260
|
+
const {
|
|
1261
|
+
collapsedSizePercentage,
|
|
1262
|
+
panelSizePercentage,
|
|
1263
|
+
minSizePercentage,
|
|
1264
|
+
pivotIndices,
|
|
1265
|
+
groupSizePixels
|
|
1266
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1267
|
+
if (panelSizePercentage === collapsedSizePercentage) {
|
|
1268
|
+
// Restore this panel to the size it was before it was collapsed, if possible.
|
|
1269
|
+
const prevPanelSizePercentage = panelSizeBeforeCollapseRef.current.get(panelData.id);
|
|
1270
|
+
const baseSizePercentage = prevPanelSizePercentage != null ? prevPanelSizePercentage : minSizePercentage;
|
|
1271
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1272
|
+
const delta = isLastPanel ? panelSizePercentage - baseSizePercentage : baseSizePercentage - panelSizePercentage;
|
|
1273
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1274
|
+
delta,
|
|
1275
|
+
groupSizePixels,
|
|
1276
|
+
layout: prevLayout,
|
|
1277
|
+
panelConstraints: panelConstraintsArray,
|
|
1278
|
+
pivotIndices,
|
|
1279
|
+
trigger: "imperative-api"
|
|
1280
|
+
});
|
|
1281
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1282
|
+
setLayout(nextLayout);
|
|
1283
|
+
if (onLayout) {
|
|
1284
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1285
|
+
sizePercentage,
|
|
1286
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1287
|
+
})));
|
|
920
1288
|
}
|
|
1289
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
921
1290
|
}
|
|
922
1291
|
}
|
|
923
|
-
return {
|
|
924
|
-
flexBasis: 0,
|
|
925
|
-
flexGrow: defaultSize != null ? defaultSize : undefined,
|
|
926
|
-
flexShrink: 1,
|
|
927
|
-
// Without this, Panel sizes may be unintentionally overridden by their content.
|
|
928
|
-
overflow: "hidden"
|
|
929
|
-
};
|
|
930
1292
|
}
|
|
931
|
-
|
|
1293
|
+
}, [groupId]);
|
|
1294
|
+
|
|
1295
|
+
// External APIs are safe to memoize via committed values ref
|
|
1296
|
+
const getPanelSize = useCallback(panelData => {
|
|
1297
|
+
const {
|
|
1298
|
+
layout,
|
|
1299
|
+
panelDataArray
|
|
1300
|
+
} = committedValuesRef.current;
|
|
1301
|
+
const {
|
|
1302
|
+
panelSizePercentage,
|
|
1303
|
+
panelSizePixels
|
|
1304
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
932
1305
|
return {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
flexShrink: 1,
|
|
936
|
-
// Without this, Panel sizes may be unintentionally overridden by their content.
|
|
937
|
-
overflow: "hidden",
|
|
938
|
-
// Disable pointer events inside of a panel during resize.
|
|
939
|
-
// This avoid edge cases like nested iframes.
|
|
940
|
-
pointerEvents: disablePointerEventsDuringResize && activeHandleId !== null ? "none" : undefined
|
|
1306
|
+
sizePercentage: panelSizePercentage,
|
|
1307
|
+
sizePixels: panelSizePixels
|
|
941
1308
|
};
|
|
942
|
-
}, [
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1309
|
+
}, [groupId]);
|
|
1310
|
+
|
|
1311
|
+
// This API should never read from committedValuesRef
|
|
1312
|
+
const getPanelStyle = useCallback(panelData => {
|
|
1313
|
+
const panelIndex = panelDataArray.indexOf(panelData);
|
|
1314
|
+
return computePanelFlexBoxStyle({
|
|
1315
|
+
dragState,
|
|
1316
|
+
layout,
|
|
1317
|
+
panelData: panelDataArray,
|
|
1318
|
+
panelIndex
|
|
1319
|
+
});
|
|
1320
|
+
}, [dragState, layout, panelDataArray]);
|
|
1321
|
+
|
|
1322
|
+
// External APIs are safe to memoize via committed values ref
|
|
1323
|
+
const isPanelCollapsed = useCallback(panelData => {
|
|
1324
|
+
const {
|
|
1325
|
+
layout,
|
|
1326
|
+
panelDataArray
|
|
1327
|
+
} = committedValuesRef.current;
|
|
1328
|
+
const {
|
|
1329
|
+
collapsedSizePercentage,
|
|
1330
|
+
collapsible,
|
|
1331
|
+
panelSizePercentage
|
|
1332
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
1333
|
+
return collapsible === true && panelSizePercentage === collapsedSizePercentage;
|
|
1334
|
+
}, [groupId]);
|
|
1335
|
+
|
|
1336
|
+
// External APIs are safe to memoize via committed values ref
|
|
1337
|
+
const isPanelExpanded = useCallback(panelData => {
|
|
1338
|
+
const {
|
|
1339
|
+
layout,
|
|
1340
|
+
panelDataArray
|
|
1341
|
+
} = committedValuesRef.current;
|
|
1342
|
+
const {
|
|
1343
|
+
collapsedSizePercentage,
|
|
1344
|
+
collapsible,
|
|
1345
|
+
panelSizePercentage
|
|
1346
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, layout);
|
|
1347
|
+
return !collapsible || panelSizePercentage > collapsedSizePercentage;
|
|
1348
|
+
}, [groupId]);
|
|
1349
|
+
const registerPanel = useCallback(panelData => {
|
|
1350
|
+
setPanelDataArray(prevPanelDataArray => {
|
|
1351
|
+
const nextPanelDataArray = [...prevPanelDataArray, panelData];
|
|
1352
|
+
return nextPanelDataArray.sort((panelA, panelB) => {
|
|
1353
|
+
const orderA = panelA.order;
|
|
1354
|
+
const orderB = panelB.order;
|
|
1355
|
+
if (orderA == null && orderB == null) {
|
|
1356
|
+
return 0;
|
|
1357
|
+
} else if (orderA == null) {
|
|
1358
|
+
return -1;
|
|
1359
|
+
} else if (orderB == null) {
|
|
1360
|
+
return 1;
|
|
1361
|
+
} else {
|
|
1362
|
+
return orderA - orderB;
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
951
1365
|
});
|
|
952
1366
|
}, []);
|
|
953
|
-
const registerResizeHandle = useCallback(
|
|
954
|
-
|
|
1367
|
+
const registerResizeHandle = useCallback(dragHandleId => {
|
|
1368
|
+
return function resizeHandler(event) {
|
|
955
1369
|
event.preventDefault();
|
|
956
1370
|
const {
|
|
957
1371
|
direction,
|
|
958
|
-
|
|
959
|
-
|
|
1372
|
+
dragState,
|
|
1373
|
+
id: groupId,
|
|
1374
|
+
keyboardResizeByPercentage,
|
|
1375
|
+
keyboardResizeByPixels,
|
|
1376
|
+
onLayout,
|
|
1377
|
+
panelDataArray,
|
|
1378
|
+
layout: prevLayout
|
|
960
1379
|
} = committedValuesRef.current;
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1380
|
+
const {
|
|
1381
|
+
initialLayout
|
|
1382
|
+
} = dragState ?? {};
|
|
1383
|
+
const pivotIndices = determinePivotIndices(groupId, dragHandleId);
|
|
1384
|
+
let delta = calculateDeltaPercentage(event, groupId, dragHandleId, direction, dragState, {
|
|
1385
|
+
percentage: keyboardResizeByPercentage,
|
|
1386
|
+
pixels: keyboardResizeByPixels
|
|
1387
|
+
});
|
|
1388
|
+
if (delta === 0) {
|
|
968
1389
|
return;
|
|
969
1390
|
}
|
|
970
|
-
const groupElement = getPanelGroup(groupId);
|
|
971
|
-
const rect = groupElement.getBoundingClientRect();
|
|
972
|
-
const isHorizontal = direction === "horizontal";
|
|
973
1391
|
|
|
974
1392
|
// Support RTL layouts
|
|
1393
|
+
const isHorizontal = direction === "horizontal";
|
|
975
1394
|
if (document.dir === "rtl" && isHorizontal) {
|
|
976
|
-
|
|
1395
|
+
delta = -delta;
|
|
977
1396
|
}
|
|
978
|
-
const
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
|
|
1397
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1398
|
+
const panelConstraints = panelDataArray.map(panelData => panelData.constraints);
|
|
1399
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1400
|
+
delta,
|
|
1401
|
+
groupSizePixels,
|
|
1402
|
+
layout: initialLayout ?? prevLayout,
|
|
1403
|
+
panelConstraints,
|
|
1404
|
+
pivotIndices,
|
|
1405
|
+
trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch"
|
|
1406
|
+
});
|
|
1407
|
+
const layoutChanged = !compareLayouts(prevLayout, nextLayout);
|
|
982
1408
|
|
|
983
|
-
//
|
|
1409
|
+
// Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
|
|
1410
|
+
// Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
|
|
984
1411
|
if (isMouseEvent(event) || isTouchEvent(event)) {
|
|
985
1412
|
// Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
|
|
986
1413
|
// In this case, Panel sizes might not change–
|
|
987
1414
|
// but updating cursor in this scenario would cause a flicker.
|
|
988
1415
|
if (prevDeltaRef.current != delta) {
|
|
989
|
-
|
|
1416
|
+
prevDeltaRef.current = delta;
|
|
1417
|
+
if (!layoutChanged) {
|
|
990
1418
|
// If the pointer has moved too far to resize the panel any further,
|
|
991
1419
|
// update the cursor style for a visual clue.
|
|
992
1420
|
// This mimics VS Code behavior.
|
|
993
1421
|
|
|
994
1422
|
if (isHorizontal) {
|
|
995
|
-
setGlobalCursorStyle(
|
|
1423
|
+
setGlobalCursorStyle(delta < 0 ? "horizontal-min" : "horizontal-max");
|
|
996
1424
|
} else {
|
|
997
|
-
setGlobalCursorStyle(
|
|
1425
|
+
setGlobalCursorStyle(delta < 0 ? "vertical-min" : "vertical-max");
|
|
998
1426
|
}
|
|
999
1427
|
} else {
|
|
1000
1428
|
// Reset the cursor style to the the normal resize cursor.
|
|
@@ -1002,185 +1430,100 @@ function PanelGroupWithForwardedRef({
|
|
|
1002
1430
|
}
|
|
1003
1431
|
}
|
|
1004
1432
|
}
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1433
|
+
if (layoutChanged) {
|
|
1434
|
+
setLayout(nextLayout);
|
|
1435
|
+
if (onLayout) {
|
|
1436
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1437
|
+
sizePercentage,
|
|
1438
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1439
|
+
})));
|
|
1440
|
+
}
|
|
1441
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1012
1442
|
}
|
|
1013
|
-
prevDeltaRef.current = delta;
|
|
1014
1443
|
};
|
|
1015
|
-
return resizeHandler;
|
|
1016
|
-
}, [groupId]);
|
|
1017
|
-
const unregisterPanel = useCallback(id => {
|
|
1018
|
-
setPanels(prevPanels => {
|
|
1019
|
-
if (!prevPanels.has(id)) {
|
|
1020
|
-
return prevPanels;
|
|
1021
|
-
}
|
|
1022
|
-
const nextPanels = new Map(prevPanels);
|
|
1023
|
-
nextPanels.delete(id);
|
|
1024
|
-
return nextPanels;
|
|
1025
|
-
});
|
|
1026
1444
|
}, []);
|
|
1027
|
-
|
|
1445
|
+
|
|
1446
|
+
// External APIs are safe to memoize via committed values ref
|
|
1447
|
+
const resizePanel = useCallback((panelData, mixedSizes) => {
|
|
1028
1448
|
const {
|
|
1029
|
-
|
|
1030
|
-
|
|
1449
|
+
layout: prevLayout,
|
|
1450
|
+
onLayout,
|
|
1451
|
+
panelDataArray
|
|
1031
1452
|
} = committedValuesRef.current;
|
|
1032
|
-
const
|
|
1033
|
-
if (panel == null) {
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1453
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1036
1454
|
const {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
if (prevSizes !== nextSizes) {
|
|
1062
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1063
|
-
setSizes(nextSizes);
|
|
1064
|
-
|
|
1065
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1066
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1067
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1455
|
+
groupSizePixels,
|
|
1456
|
+
panelSizePercentage,
|
|
1457
|
+
pivotIndices
|
|
1458
|
+
} = panelDataHelper(groupId, panelDataArray, panelData, prevLayout);
|
|
1459
|
+
const sizePercentage = getPercentageSizeFromMixedSizes(mixedSizes, groupSizePixels);
|
|
1460
|
+
const isLastPanel = panelDataArray.indexOf(panelData) === panelDataArray.length - 1;
|
|
1461
|
+
const delta = isLastPanel ? panelSizePercentage - sizePercentage : sizePercentage - panelSizePercentage;
|
|
1462
|
+
const nextLayout = adjustLayoutByDelta({
|
|
1463
|
+
delta,
|
|
1464
|
+
groupSizePixels,
|
|
1465
|
+
layout: prevLayout,
|
|
1466
|
+
panelConstraints: panelConstraintsArray,
|
|
1467
|
+
pivotIndices,
|
|
1468
|
+
trigger: "imperative-api"
|
|
1469
|
+
});
|
|
1470
|
+
if (!compareLayouts(prevLayout, nextLayout)) {
|
|
1471
|
+
setLayout(nextLayout);
|
|
1472
|
+
if (onLayout) {
|
|
1473
|
+
onLayout(nextLayout.map(sizePercentage => ({
|
|
1474
|
+
sizePercentage,
|
|
1475
|
+
sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
|
|
1476
|
+
})));
|
|
1477
|
+
}
|
|
1478
|
+
callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
|
|
1068
1479
|
}
|
|
1069
|
-
}, []);
|
|
1070
|
-
const
|
|
1480
|
+
}, [groupId]);
|
|
1481
|
+
const startDragging = useCallback((dragHandleId, event) => {
|
|
1071
1482
|
const {
|
|
1072
|
-
|
|
1073
|
-
|
|
1483
|
+
direction,
|
|
1484
|
+
layout
|
|
1074
1485
|
} = committedValuesRef.current;
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
const sizeBeforeCollapse = panelSizeBeforeCollapse.current.get(id) || minSize;
|
|
1084
|
-
if (!sizeBeforeCollapse) {
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
1088
|
-
const index = panelsArray.indexOf(panel);
|
|
1089
|
-
if (index < 0) {
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
const currentSize = prevSizes[index];
|
|
1093
|
-
if (currentSize !== collapsedSize) {
|
|
1094
|
-
// Panel is already expanded.
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
|
|
1098
|
-
if (idBefore == null || idAfter == null) {
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
1102
|
-
const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
|
|
1103
|
-
const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
|
|
1104
|
-
if (prevSizes !== nextSizes) {
|
|
1105
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1106
|
-
setSizes(nextSizes);
|
|
1107
|
-
|
|
1108
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1109
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1110
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1111
|
-
}
|
|
1486
|
+
const handleElement = getResizeHandleElement(dragHandleId);
|
|
1487
|
+
const initialCursorPosition = getResizeEventCursorPosition(direction, event);
|
|
1488
|
+
setDragState({
|
|
1489
|
+
dragHandleId,
|
|
1490
|
+
dragHandleRect: handleElement.getBoundingClientRect(),
|
|
1491
|
+
initialCursorPosition,
|
|
1492
|
+
initialLayout: layout
|
|
1493
|
+
});
|
|
1112
1494
|
}, []);
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
} = panel.current;
|
|
1128
|
-
const panelsArray = panelsMapToSortedArray(panels);
|
|
1129
|
-
const index = panelsArray.indexOf(panel);
|
|
1130
|
-
if (index < 0) {
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
const currentSize = prevSizes[index];
|
|
1134
|
-
if (currentSize === nextSize) {
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
if (collapsible && nextSize === collapsedSize) ; else {
|
|
1138
|
-
nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
|
|
1139
|
-
}
|
|
1140
|
-
const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
|
|
1141
|
-
if (idBefore == null || idAfter == null) {
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
const isLastPanel = index === panelsArray.length - 1;
|
|
1145
|
-
const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
|
|
1146
|
-
const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
|
|
1147
|
-
if (prevSizes !== nextSizes) {
|
|
1148
|
-
const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
|
|
1149
|
-
setSizes(nextSizes);
|
|
1150
|
-
|
|
1151
|
-
// If resize change handlers have been declared, this is the time to call them.
|
|
1152
|
-
// Trigger user callbacks after updating state, so that user code can override the sizes.
|
|
1153
|
-
callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
|
|
1154
|
-
}
|
|
1495
|
+
const stopDragging = useCallback(() => {
|
|
1496
|
+
resetGlobalCursorStyle();
|
|
1497
|
+
setDragState(null);
|
|
1498
|
+
}, []);
|
|
1499
|
+
const unregisterPanel = useCallback(panelData => {
|
|
1500
|
+
delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
|
|
1501
|
+
setPanelDataArray(panelDataArray => {
|
|
1502
|
+
const index = panelDataArray.indexOf(panelData);
|
|
1503
|
+
if (index >= 0) {
|
|
1504
|
+
panelDataArray = [...panelDataArray];
|
|
1505
|
+
panelDataArray.splice(index, 1);
|
|
1506
|
+
}
|
|
1507
|
+
return panelDataArray;
|
|
1508
|
+
});
|
|
1155
1509
|
}, []);
|
|
1156
1510
|
const context = useMemo(() => ({
|
|
1157
|
-
activeHandleId,
|
|
1158
1511
|
collapsePanel,
|
|
1159
1512
|
direction,
|
|
1513
|
+
dragState,
|
|
1160
1514
|
expandPanel,
|
|
1515
|
+
getPanelSize,
|
|
1161
1516
|
getPanelStyle,
|
|
1162
1517
|
groupId,
|
|
1518
|
+
isPanelCollapsed,
|
|
1519
|
+
isPanelExpanded,
|
|
1163
1520
|
registerPanel,
|
|
1164
1521
|
registerResizeHandle,
|
|
1165
1522
|
resizePanel,
|
|
1166
|
-
startDragging
|
|
1167
|
-
|
|
1168
|
-
if (isMouseEvent(event) || isTouchEvent(event)) {
|
|
1169
|
-
const handleElement = getResizeHandle(id);
|
|
1170
|
-
initialDragStateRef.current = {
|
|
1171
|
-
dragHandleRect: handleElement.getBoundingClientRect(),
|
|
1172
|
-
dragOffset: getDragOffset(event, id, direction),
|
|
1173
|
-
sizes: committedValuesRef.current.sizes
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
},
|
|
1177
|
-
stopDragging: () => {
|
|
1178
|
-
resetGlobalCursorStyle();
|
|
1179
|
-
setActiveHandleId(null);
|
|
1180
|
-
initialDragStateRef.current = null;
|
|
1181
|
-
},
|
|
1523
|
+
startDragging,
|
|
1524
|
+
stopDragging,
|
|
1182
1525
|
unregisterPanel
|
|
1183
|
-
}), [
|
|
1526
|
+
}), [collapsePanel, dragState, direction, expandPanel, getPanelSize, getPanelStyle, groupId, isPanelCollapsed, isPanelExpanded, registerPanel, registerResizeHandle, resizePanel, startDragging, stopDragging, unregisterPanel]);
|
|
1184
1527
|
const style = {
|
|
1185
1528
|
display: "flex",
|
|
1186
1529
|
flexDirection: direction === "horizontal" ? "row" : "column",
|
|
@@ -1189,19 +1532,20 @@ function PanelGroupWithForwardedRef({
|
|
|
1189
1532
|
width: "100%"
|
|
1190
1533
|
};
|
|
1191
1534
|
return createElement(PanelGroupContext.Provider, {
|
|
1192
|
-
children: createElement(Type, {
|
|
1193
|
-
children,
|
|
1194
|
-
className: classNameFromProps,
|
|
1195
|
-
"data-panel-group": "",
|
|
1196
|
-
"data-panel-group-direction": direction,
|
|
1197
|
-
"data-panel-group-id": groupId,
|
|
1198
|
-
style: {
|
|
1199
|
-
...style,
|
|
1200
|
-
...styleFromProps
|
|
1201
|
-
}
|
|
1202
|
-
}),
|
|
1203
1535
|
value: context
|
|
1204
|
-
}
|
|
1536
|
+
}, createElement(Type, {
|
|
1537
|
+
children,
|
|
1538
|
+
className: classNameFromProps,
|
|
1539
|
+
style: {
|
|
1540
|
+
...style,
|
|
1541
|
+
...styleFromProps
|
|
1542
|
+
},
|
|
1543
|
+
// CSS selectors
|
|
1544
|
+
"data-panel-group": "",
|
|
1545
|
+
// e2e test attributes
|
|
1546
|
+
"data-panel-group-direction": direction ,
|
|
1547
|
+
"data-panel-group-id": groupId
|
|
1548
|
+
}));
|
|
1205
1549
|
}
|
|
1206
1550
|
const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwardedRef, {
|
|
1207
1551
|
...props,
|
|
@@ -1209,6 +1553,77 @@ const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwar
|
|
|
1209
1553
|
}));
|
|
1210
1554
|
PanelGroupWithForwardedRef.displayName = "PanelGroup";
|
|
1211
1555
|
PanelGroup.displayName = "forwardRef(PanelGroup)";
|
|
1556
|
+
function panelDataHelper(groupId, panelDataArray, panelData, layout) {
|
|
1557
|
+
const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
|
|
1558
|
+
const panelIndex = panelDataArray.indexOf(panelData);
|
|
1559
|
+
const panelConstraints = panelConstraintsArray[panelIndex];
|
|
1560
|
+
const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
|
|
1561
|
+
const percentagePanelConstraints = computePercentagePanelConstraints(panelConstraintsArray, panelIndex, groupSizePixels);
|
|
1562
|
+
const isLastPanel = panelIndex === panelDataArray.length - 1;
|
|
1563
|
+
const pivotIndices = isLastPanel ? [panelIndex - 1, panelIndex] : [panelIndex, panelIndex + 1];
|
|
1564
|
+
const panelSizePercentage = layout[panelIndex];
|
|
1565
|
+
const panelSizePixels = convertPercentageToPixels(panelSizePercentage, groupSizePixels);
|
|
1566
|
+
return {
|
|
1567
|
+
...percentagePanelConstraints,
|
|
1568
|
+
collapsible: panelConstraints.collapsible,
|
|
1569
|
+
panelSizePercentage,
|
|
1570
|
+
panelSizePixels,
|
|
1571
|
+
groupSizePixels,
|
|
1572
|
+
pivotIndices
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
1577
|
+
|
|
1578
|
+
function useWindowSplitterResizeHandlerBehavior({
|
|
1579
|
+
disabled,
|
|
1580
|
+
handleId,
|
|
1581
|
+
resizeHandler
|
|
1582
|
+
}) {
|
|
1583
|
+
useEffect(() => {
|
|
1584
|
+
if (disabled || resizeHandler == null) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const handleElement = getResizeHandleElement(handleId);
|
|
1588
|
+
if (handleElement == null) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const onKeyDown = event => {
|
|
1592
|
+
if (event.defaultPrevented) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
switch (event.key) {
|
|
1596
|
+
case "ArrowDown":
|
|
1597
|
+
case "ArrowLeft":
|
|
1598
|
+
case "ArrowRight":
|
|
1599
|
+
case "ArrowUp":
|
|
1600
|
+
case "End":
|
|
1601
|
+
case "Home":
|
|
1602
|
+
{
|
|
1603
|
+
event.preventDefault();
|
|
1604
|
+
resizeHandler(event);
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
case "F6":
|
|
1608
|
+
{
|
|
1609
|
+
event.preventDefault();
|
|
1610
|
+
const groupId = handleElement.getAttribute("data-panel-group-id");
|
|
1611
|
+
const handles = getResizeHandleElementsForGroup(groupId);
|
|
1612
|
+
const index = getResizeHandleElementIndex(groupId, handleId);
|
|
1613
|
+
assert(index !== null);
|
|
1614
|
+
const nextIndex = event.shiftKey ? index > 0 ? index - 1 : handles.length - 1 : index + 1 < handles.length ? index + 1 : 0;
|
|
1615
|
+
const nextHandle = handles[nextIndex];
|
|
1616
|
+
nextHandle.focus();
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
handleElement.addEventListener("keydown", onKeyDown);
|
|
1622
|
+
return () => {
|
|
1623
|
+
handleElement.removeEventListener("keydown", onKeyDown);
|
|
1624
|
+
};
|
|
1625
|
+
}, [disabled, handleId, resizeHandler]);
|
|
1626
|
+
}
|
|
1212
1627
|
|
|
1213
1628
|
function PanelResizeHandle({
|
|
1214
1629
|
children = null,
|
|
@@ -1233,15 +1648,15 @@ function PanelResizeHandle({
|
|
|
1233
1648
|
throw Error(`PanelResizeHandle components must be rendered within a PanelGroup container`);
|
|
1234
1649
|
}
|
|
1235
1650
|
const {
|
|
1236
|
-
activeHandleId,
|
|
1237
1651
|
direction,
|
|
1652
|
+
dragState,
|
|
1238
1653
|
groupId,
|
|
1239
1654
|
registerResizeHandle,
|
|
1240
1655
|
startDragging,
|
|
1241
1656
|
stopDragging
|
|
1242
1657
|
} = panelGroupContext;
|
|
1243
1658
|
const resizeHandleId = useUniqueId(idFromProps);
|
|
1244
|
-
const isDragging =
|
|
1659
|
+
const isDragging = dragState?.dragHandleId === resizeHandleId;
|
|
1245
1660
|
const [isFocused, setIsFocused] = useState(false);
|
|
1246
1661
|
const [resizeHandler, setResizeHandler] = useState(null);
|
|
1247
1662
|
const stopDraggingAndBlur = useCallback(() => {
|
|
@@ -1305,6 +1720,8 @@ function PanelResizeHandle({
|
|
|
1305
1720
|
return createElement(Type, {
|
|
1306
1721
|
children,
|
|
1307
1722
|
className: classNameFromProps,
|
|
1723
|
+
// CSS selectors
|
|
1724
|
+
"data-resize-handle": "",
|
|
1308
1725
|
"data-resize-handle-active": isDragging ? "pointer" : isFocused ? "keyboard" : undefined,
|
|
1309
1726
|
"data-panel-group-direction": direction,
|
|
1310
1727
|
"data-panel-group-id": groupId,
|