react-resizable-panels 0.0.55 → 0.0.56

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