react-resizable-panels 0.0.54 → 0.0.56

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