react-resizable-panels 0.0.55 → 0.0.57

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