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