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