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