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