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