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