react-resizable-panels 0.0.55 → 0.0.57

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