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