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