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