react-resizable-panels 0.0.55 → 0.0.57

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