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