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