react-resizable-panels 0.0.54 → 0.0.56

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