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