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