react-resizable-panels 0.0.54 → 0.0.55

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 (34) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/declarations/src/Panel.d.ts +5 -4
  3. package/dist/declarations/src/PanelGroup.d.ts +7 -3
  4. package/dist/declarations/src/index.d.ts +3 -2
  5. package/dist/declarations/src/types.d.ts +2 -1
  6. package/dist/declarations/src/utils/group.d.ts +29 -0
  7. package/dist/react-resizable-panels.browser.cjs.js +385 -108
  8. package/dist/react-resizable-panels.browser.cjs.mjs +2 -1
  9. package/dist/react-resizable-panels.browser.development.cjs.js +417 -108
  10. package/dist/react-resizable-panels.browser.development.cjs.mjs +2 -1
  11. package/dist/react-resizable-panels.browser.development.esm.js +417 -109
  12. package/dist/react-resizable-panels.browser.esm.js +385 -109
  13. package/dist/react-resizable-panels.cjs.js +385 -108
  14. package/dist/react-resizable-panels.cjs.js.map +1 -0
  15. package/dist/react-resizable-panels.cjs.mjs +2 -1
  16. package/dist/react-resizable-panels.development.cjs.js +417 -108
  17. package/dist/react-resizable-panels.development.cjs.mjs +2 -1
  18. package/dist/react-resizable-panels.development.esm.js +417 -109
  19. package/dist/react-resizable-panels.development.node.cjs.js +282 -76
  20. package/dist/react-resizable-panels.development.node.cjs.mjs +2 -1
  21. package/dist/react-resizable-panels.development.node.esm.js +282 -77
  22. package/dist/react-resizable-panels.esm.js +385 -109
  23. package/dist/react-resizable-panels.esm.js.map +1 -0
  24. package/dist/react-resizable-panels.node.cjs.js +254 -76
  25. package/dist/react-resizable-panels.node.cjs.mjs +2 -1
  26. package/dist/react-resizable-panels.node.esm.js +254 -77
  27. package/package.json +1 -1
  28. package/src/Panel.ts +32 -32
  29. package/src/PanelContexts.ts +4 -2
  30. package/src/PanelGroup.ts +221 -111
  31. package/src/hooks/useWindowSplitterBehavior.ts +14 -11
  32. package/src/index.ts +11 -3
  33. package/src/types.ts +2 -1
  34. package/src/utils/group.ts +327 -28
@@ -47,8 +47,8 @@ function PanelWithForwardedRef({
47
47
  defaultSize = null,
48
48
  forwardedRef,
49
49
  id: idFromProps = null,
50
- maxSize = 100,
51
- minSize = 10,
50
+ maxSize = null,
51
+ minSize,
52
52
  onCollapse = null,
53
53
  onResize = null,
54
54
  order = null,
@@ -63,11 +63,22 @@ function PanelWithForwardedRef({
63
63
  const {
64
64
  collapsePanel,
65
65
  expandPanel,
66
+ getPanelSize,
66
67
  getPanelStyle,
67
68
  registerPanel,
68
69
  resizePanel,
70
+ units,
69
71
  unregisterPanel
70
72
  } = context;
73
+ if (minSize == null) {
74
+ if (units === "percentages") {
75
+ // Mimics legacy default value for percentage based panel groups
76
+ minSize = 10;
77
+ } else {
78
+ // There is no meaningful minimum pixel default we can provide
79
+ minSize = 0;
80
+ }
81
+ }
71
82
 
72
83
  // Use a ref to guard against users passing inline props
73
84
  const callbacksRef = useRef({
@@ -78,22 +89,6 @@ function PanelWithForwardedRef({
78
89
  callbacksRef.current.onCollapse = onCollapse;
79
90
  callbacksRef.current.onResize = onResize;
80
91
  });
81
-
82
- // Basic props validation
83
- if (minSize < 0 || minSize > 100) {
84
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
85
- } else if (maxSize < 0 || maxSize > 100) {
86
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
87
- } else {
88
- if (defaultSize !== null) {
89
- if (defaultSize < 0 || defaultSize > 100) {
90
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
91
- } else if (minSize > defaultSize && !collapsible) {
92
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
93
- defaultSize = minSize;
94
- }
95
- }
96
- }
97
92
  const style = getPanelStyle(panelId, defaultSize);
98
93
  const committedValuesRef = useRef({
99
94
  size: parseSizeFromStyle(style)
@@ -133,11 +128,14 @@ function PanelWithForwardedRef({
133
128
  getCollapsed() {
134
129
  return committedValuesRef.current.size === 0;
135
130
  },
136
- getSize() {
137
- return committedValuesRef.current.size;
131
+ getId() {
132
+ return panelId;
138
133
  },
139
- resize: percentage => resizePanel(panelId, percentage)
140
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
134
+ getSize(units) {
135
+ return getPanelSize(panelId, units);
136
+ },
137
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
138
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
141
139
  return createElement(Type, {
142
140
  children,
143
141
  className: classNameFromProps,
@@ -173,7 +171,13 @@ function parseSizeFromStyle(style) {
173
171
 
174
172
  const PRECISION = 10;
175
173
 
176
- function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse, initialDragState) {
174
+ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
175
+ const {
176
+ id: groupId,
177
+ panels,
178
+ units
179
+ } = committedValues;
180
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
177
181
  const {
178
182
  sizes: initialSizes
179
183
  } = initialDragState || {};
@@ -181,9 +185,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
181
185
  // If we're resizing by mouse or touch, use the initial sizes as a base.
182
186
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
183
187
  const baseSizes = initialSizes || prevSizes;
184
- if (delta === 0) {
185
- return baseSizes;
186
- }
187
188
  const panelsArray = panelsMapToSortedArray(panels);
188
189
  const nextSizes = baseSizes.concat();
189
190
  let deltaApplied = 0;
@@ -198,11 +199,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
198
199
 
199
200
  // Max-bounds check the panel being expanded first.
200
201
  {
201
- const pivotId = delta < 0 ? idAfter : idBefore;
202
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
202
203
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
203
204
  const panel = panelsArray[index];
204
205
  const baseSize = baseSizes[index];
205
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
206
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
206
207
  if (baseSize === nextSize) {
207
208
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
208
209
  return baseSizes;
@@ -210,29 +211,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
210
211
  if (nextSize === 0 && baseSize > 0) {
211
212
  panelSizeBeforeCollapse.set(pivotId, baseSize);
212
213
  }
213
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
214
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
214
215
  }
215
216
  }
216
- let pivotId = delta < 0 ? idBefore : idAfter;
217
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
217
218
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
218
219
  while (true) {
219
220
  const panel = panelsArray[index];
220
221
  const baseSize = baseSizes[index];
221
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
222
- const nextSize = safeResizePanel(panel, 0 - deltaRemaining, baseSize, event);
222
+ const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
223
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
223
224
  if (baseSize !== nextSize) {
224
225
  if (nextSize === 0 && baseSize > 0) {
225
226
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
226
227
  }
227
228
  deltaApplied += baseSize - nextSize;
228
229
  nextSizes[index] = nextSize;
229
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
230
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
230
231
  numeric: true
231
232
  }) >= 0) {
232
233
  break;
233
234
  }
234
235
  }
235
- if (delta < 0) {
236
+ if (deltaPixels < 0) {
236
237
  if (--index < 0) {
237
238
  break;
238
239
  }
@@ -250,7 +251,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
250
251
  }
251
252
 
252
253
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
253
- pivotId = delta < 0 ? idAfter : idBefore;
254
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
254
255
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
255
256
  nextSizes[index] = baseSizes[index] + deltaApplied;
256
257
  return nextSizes;
@@ -289,6 +290,89 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
289
290
  }
290
291
  });
291
292
  }
293
+ function calculateDefaultLayout({
294
+ groupId,
295
+ panels,
296
+ units
297
+ }) {
298
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
299
+ const panelsArray = panelsMapToSortedArray(panels);
300
+ const sizes = Array(panelsArray.length);
301
+ let numPanelsWithSizes = 0;
302
+ let remainingSize = 100;
303
+
304
+ // Assigning default sizes requires a couple of passes:
305
+ // First, all panels with defaultSize should be set as-is
306
+ for (let index = 0; index < panelsArray.length; index++) {
307
+ const panel = panelsArray[index];
308
+ const {
309
+ defaultSize
310
+ } = panel.current;
311
+ if (defaultSize != null) {
312
+ numPanelsWithSizes++;
313
+ sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
314
+ remainingSize -= sizes[index];
315
+ }
316
+ }
317
+
318
+ // Remaining total size should be distributed evenly between panels
319
+ // This may require two passes, depending on min/max constraints
320
+ for (let index = 0; index < panelsArray.length; index++) {
321
+ const panel = panelsArray[index];
322
+ let {
323
+ defaultSize,
324
+ id,
325
+ maxSize,
326
+ minSize
327
+ } = panel.current;
328
+ if (defaultSize != null) {
329
+ continue;
330
+ }
331
+ if (units === "pixels") {
332
+ minSize = minSize / groupSizePixels * 100;
333
+ if (maxSize != null) {
334
+ maxSize = maxSize / groupSizePixels * 100;
335
+ }
336
+ }
337
+ const remainingPanels = panelsArray.length - numPanelsWithSizes;
338
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
339
+ sizes[index] = size;
340
+ numPanelsWithSizes++;
341
+ remainingSize -= size;
342
+ }
343
+
344
+ // If there is additional, left over space, assign it to any panel(s) that permits it
345
+ // (It's not worth taking multiple additional passes to evenly distribute)
346
+ if (remainingSize !== 0) {
347
+ for (let index = 0; index < panelsArray.length; index++) {
348
+ const panel = panelsArray[index];
349
+ let {
350
+ maxSize,
351
+ minSize
352
+ } = panel.current;
353
+ if (units === "pixels") {
354
+ minSize = minSize / groupSizePixels * 100;
355
+ if (maxSize != null) {
356
+ maxSize = maxSize / groupSizePixels * 100;
357
+ }
358
+ }
359
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
360
+ if (size !== sizes[index]) {
361
+ remainingSize -= size - sizes[index];
362
+ sizes[index] = size;
363
+
364
+ // Fuzzy comparison to account for imprecise floating point math
365
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ // Finally, if there is still left-over size, log an error
373
+ if (Math.abs(remainingSize).toFixed(3) !== "0.000") ;
374
+ return sizes;
375
+ }
292
376
  function getBeforeAndAfterIds(id, panelsArray) {
293
377
  if (panelsArray.length < 2) {
294
378
  return [null, null];
@@ -302,6 +386,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
302
386
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
303
387
  return [idBefore, idAfter];
304
388
  }
389
+ function getAvailableGroupSizePixels(groupId) {
390
+ const panelGroupElement = getPanelGroup(groupId);
391
+ if (panelGroupElement == null) {
392
+ return NaN;
393
+ }
394
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
395
+ const resizeHandles = getResizeHandlesForGroup(groupId);
396
+ if (direction === "horizontal") {
397
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
398
+ return accumulated + handle.offsetWidth;
399
+ }, 0);
400
+ } else {
401
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
402
+ return accumulated + handle.offsetHeight;
403
+ }, 0);
404
+ }
405
+ }
305
406
 
306
407
  // This method returns a number between 1 and 100 representing
307
408
  // the % of the group's overall space this panel should occupy.
@@ -372,18 +473,24 @@ function panelsMapToSortedArray(panels) {
372
473
  }
373
474
  });
374
475
  }
375
- function safeResizePanel(panel, delta, prevSize, event) {
376
- const nextSizeUnsafe = prevSize + delta;
377
- const {
476
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
477
+ let {
378
478
  collapsedSize,
379
479
  collapsible,
380
480
  maxSize,
381
481
  minSize
382
482
  } = panel.current;
483
+ if (units === "pixels") {
484
+ collapsedSize = collapsedSize / groupSizePixels * 100;
485
+ if (maxSize != null) {
486
+ maxSize = maxSize / groupSizePixels * 100;
487
+ }
488
+ minSize = minSize / groupSizePixels * 100;
489
+ }
383
490
  if (collapsible) {
384
491
  if (prevSize > collapsedSize) {
385
492
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
386
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
493
+ if (nextSize <= minSize / 2 + collapsedSize) {
387
494
  return collapsedSize;
388
495
  }
389
496
  } else {
@@ -392,14 +499,97 @@ function safeResizePanel(panel, delta, prevSize, event) {
392
499
  // Keyboard events should expand a collapsed panel to the min size,
393
500
  // but mouse events should wait until the panel has reached its min size
394
501
  // to avoid a visual flickering when dragging between collapsed and min size.
395
- if (nextSizeUnsafe < minSize) {
502
+ if (nextSize < minSize) {
396
503
  return collapsedSize;
397
504
  }
398
505
  }
399
506
  }
400
507
  }
401
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
402
- return nextSize;
508
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
509
+ }
510
+ function validatePanelProps(units, panelData) {
511
+ const {
512
+ collapsible,
513
+ defaultSize,
514
+ maxSize,
515
+ minSize
516
+ } = panelData.current;
517
+
518
+ // Basic props validation
519
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
520
+ panelData.current.minSize = 0;
521
+ }
522
+ if (maxSize != null) {
523
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
524
+ panelData.current.maxSize = null;
525
+ }
526
+ }
527
+ if (defaultSize !== null) {
528
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
529
+ panelData.current.defaultSize = null;
530
+ } else if (defaultSize < minSize && !collapsible) {
531
+ panelData.current.defaultSize = minSize;
532
+ } else if (maxSize != null && defaultSize > maxSize) {
533
+ panelData.current.defaultSize = maxSize;
534
+ }
535
+ }
536
+ }
537
+ function validatePanelGroupLayout({
538
+ groupId,
539
+ panels,
540
+ nextSizes,
541
+ prevSizes,
542
+ units
543
+ }) {
544
+ // Clone because this method modifies
545
+ nextSizes = [...nextSizes];
546
+ const panelsArray = panelsMapToSortedArray(panels);
547
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
548
+ let remainingSize = 0;
549
+
550
+ // First, check all of the proposed sizes against the min/max constraints
551
+ for (let index = 0; index < panelsArray.length; index++) {
552
+ const panel = panelsArray[index];
553
+ const prevSize = prevSizes[index];
554
+ const nextSize = nextSizes[index];
555
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
556
+ if (nextSize != safeNextSize) {
557
+ remainingSize += nextSize - safeNextSize;
558
+ nextSizes[index] = safeNextSize;
559
+ }
560
+ }
561
+
562
+ // If there is additional, left over space, assign it to any panel(s) that permits it
563
+ // (It's not worth taking multiple additional passes to evenly distribute)
564
+ if (remainingSize.toFixed(3) !== "0.000") {
565
+ for (let index = 0; index < panelsArray.length; index++) {
566
+ const panel = panelsArray[index];
567
+ let {
568
+ maxSize,
569
+ minSize
570
+ } = panel.current;
571
+ if (units === "pixels") {
572
+ minSize = minSize / groupSizePixels * 100;
573
+ if (maxSize != null) {
574
+ maxSize = maxSize / groupSizePixels * 100;
575
+ }
576
+ }
577
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
578
+ if (size !== nextSizes[index]) {
579
+ remainingSize -= size - nextSizes[index];
580
+ nextSizes[index] = size;
581
+
582
+ // Fuzzy comparison to account for imprecise floating point math
583
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
584
+ break;
585
+ }
586
+ }
587
+ }
588
+ }
589
+
590
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
591
+ if (remainingSize.toFixed(3) !== "0.000") ;
592
+ return nextSizes;
403
593
  }
404
594
 
405
595
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -425,6 +615,7 @@ function useWindowSplitterPanelGroupBehavior({
425
615
  panels
426
616
  } = committedValuesRef.current;
427
617
  const groupElement = getPanelGroup(groupId);
618
+ assert(groupElement != null, `No group found for id "${groupId}"`);
428
619
  const {
429
620
  height,
430
621
  width
@@ -437,23 +628,28 @@ function useWindowSplitterPanelGroupBehavior({
437
628
  if (idBefore == null || idAfter == null) {
438
629
  return () => {};
439
630
  }
440
- let minSize = 0;
441
- let maxSize = 100;
631
+ let currentMinSize = 0;
632
+ let currentMaxSize = 100;
442
633
  let totalMinSize = 0;
443
634
  let totalMaxSize = 0;
444
635
 
445
636
  // A panel's effective min/max sizes also need to account for other panel's sizes.
446
637
  panelsArray.forEach(panelData => {
447
- if (panelData.current.id === idBefore) {
448
- maxSize = panelData.current.maxSize;
449
- minSize = panelData.current.minSize;
638
+ const {
639
+ id,
640
+ maxSize,
641
+ minSize
642
+ } = panelData.current;
643
+ if (id === idBefore) {
644
+ currentMinSize = minSize;
645
+ currentMaxSize = maxSize != null ? maxSize : 100;
450
646
  } else {
451
- totalMinSize += panelData.current.minSize;
452
- totalMaxSize += panelData.current.maxSize;
647
+ totalMinSize += minSize;
648
+ totalMaxSize += maxSize != null ? maxSize : 100;
453
649
  }
454
650
  });
455
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
456
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
651
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
652
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
457
653
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
458
654
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
459
655
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -477,7 +673,7 @@ function useWindowSplitterPanelGroupBehavior({
477
673
  } else {
478
674
  delta = -(direction === "horizontal" ? width : height);
479
675
  }
480
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
676
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
481
677
  if (sizes !== nextSizes) {
482
678
  setSizes(nextSizes);
483
679
  }
@@ -792,9 +988,6 @@ const defaultStorage = {
792
988
  // * dragHandleRect, sizes:
793
989
  // When resizing is done via mouse/touch event– some initial state is stored
794
990
  // so that any panels that contract will also expand if drag direction is reversed.
795
- // TODO
796
- // Within an active drag, remember original positions to refine more easily on expand.
797
- // Look at what the Chrome devtools Sources does.
798
991
  function PanelGroupWithForwardedRef({
799
992
  autoSaveId,
800
993
  children = null,
@@ -806,7 +999,8 @@ function PanelGroupWithForwardedRef({
806
999
  onLayout,
807
1000
  storage = defaultStorage,
808
1001
  style: styleFromProps = {},
809
- tagName: Type = "div"
1002
+ tagName: Type = "div",
1003
+ units = "percentages"
810
1004
  }) {
811
1005
  const groupId = useUniqueId(idFromProps);
812
1006
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -819,6 +1013,7 @@ function PanelGroupWithForwardedRef({
819
1013
  useRef({
820
1014
  didLogDefaultSizeWarning: false,
821
1015
  didLogIdAndOrderWarning: false,
1016
+ didLogInvalidLayoutWarning: false,
822
1017
  prevPanelIds: []
823
1018
  });
824
1019
 
@@ -841,32 +1036,58 @@ function PanelGroupWithForwardedRef({
841
1036
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
842
1037
  const committedValuesRef = useRef({
843
1038
  direction,
1039
+ id: groupId,
844
1040
  panels,
845
- sizes
1041
+ sizes,
1042
+ units
846
1043
  });
847
1044
  useImperativeHandle(forwardedRef, () => ({
848
- getLayout: () => {
1045
+ getId: () => groupId,
1046
+ getLayout: unitsFromParams => {
849
1047
  const {
850
- sizes
1048
+ sizes,
1049
+ units: unitsFromProps
851
1050
  } = committedValuesRef.current;
852
- return sizes;
1051
+ const units = unitsFromParams ?? unitsFromProps;
1052
+ if (units === "pixels") {
1053
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1054
+ return sizes.map(size => size / 100 * groupSizePixels);
1055
+ } else {
1056
+ return sizes;
1057
+ }
853
1058
  },
854
- setLayout: sizes => {
855
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
856
- assert(total === 100, "Panel sizes must add up to 100%");
1059
+ setLayout: (sizes, unitsFromParams) => {
857
1060
  const {
858
- panels
1061
+ id: groupId,
1062
+ panels,
1063
+ sizes: prevSizes,
1064
+ units
859
1065
  } = committedValuesRef.current;
1066
+ if ((unitsFromParams || units) === "pixels") {
1067
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1068
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1069
+ }
860
1070
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
861
1071
  const panelsArray = panelsMapToSortedArray(panels);
862
- setSizes(sizes);
863
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1072
+ const nextSizes = validatePanelGroupLayout({
1073
+ groupId,
1074
+ panels,
1075
+ nextSizes: sizes,
1076
+ prevSizes,
1077
+ units
1078
+ });
1079
+ if (!areEqual(prevSizes, nextSizes)) {
1080
+ setSizes(nextSizes);
1081
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1082
+ }
864
1083
  }
865
- }), []);
1084
+ }), [groupId]);
866
1085
  useIsomorphicLayoutEffect(() => {
867
1086
  committedValuesRef.current.direction = direction;
1087
+ committedValuesRef.current.id = groupId;
868
1088
  committedValuesRef.current.panels = panels;
869
1089
  committedValuesRef.current.sizes = sizes;
1090
+ committedValuesRef.current.units = units;
870
1091
  });
871
1092
  useWindowSplitterPanelGroupBehavior({
872
1093
  committedValuesRef,
@@ -908,7 +1129,11 @@ function PanelGroupWithForwardedRef({
908
1129
  // Compute the initial sizes based on default weights.
909
1130
  // This assumes that panels register during initial mount (no conditional rendering)!
910
1131
  useIsomorphicLayoutEffect(() => {
911
- const sizes = committedValuesRef.current.sizes;
1132
+ const {
1133
+ id: groupId,
1134
+ sizes,
1135
+ units
1136
+ } = committedValuesRef.current;
912
1137
  if (sizes.length === panels.size) {
913
1138
  // Only compute (or restore) default sizes once per panel configuration.
914
1139
  return;
@@ -922,39 +1147,23 @@ function PanelGroupWithForwardedRef({
922
1147
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
923
1148
  }
924
1149
  if (defaultSizes != null) {
925
- setSizes(defaultSizes);
1150
+ // Validate saved sizes in case something has changed since last render
1151
+ // e.g. for pixel groups, this could be the size of the window
1152
+ const validatedSizes = validatePanelGroupLayout({
1153
+ groupId,
1154
+ panels,
1155
+ nextSizes: defaultSizes,
1156
+ prevSizes: defaultSizes,
1157
+ units
1158
+ });
1159
+ setSizes(validatedSizes);
926
1160
  } else {
927
- const panelsArray = panelsMapToSortedArray(panels);
928
- let panelsWithNullDefaultSize = 0;
929
- let totalDefaultSize = 0;
930
- let totalMinSize = 0;
931
-
932
- // TODO
933
- // Implicit default size calculations below do not account for inferred min/max size values.
934
- // e.g. if Panel A has a maxSize of 40 then Panels A and B can't both have an implicit default size of 50.
935
- // For now, these logic edge cases are left to the user to handle via props.
936
-
937
- panelsArray.forEach(panel => {
938
- totalMinSize += panel.current.minSize;
939
- if (panel.current.defaultSize === null) {
940
- panelsWithNullDefaultSize++;
941
- } else {
942
- totalDefaultSize += panel.current.defaultSize;
943
- }
1161
+ const sizes = calculateDefaultLayout({
1162
+ groupId,
1163
+ panels,
1164
+ units
944
1165
  });
945
- if (totalDefaultSize > 100) {
946
- throw new Error(`Default panel sizes cannot exceed 100%`);
947
- } else if (panelsArray.length > 1 && panelsWithNullDefaultSize === 0 && totalDefaultSize !== 100) {
948
- throw new Error(`Invalid default sizes specified for panels`);
949
- } else if (totalMinSize > 100) {
950
- throw new Error(`Minimum panel sizes cannot exceed 100%`);
951
- }
952
- setSizes(panelsArray.map(panel => {
953
- if (panel.current.defaultSize === null) {
954
- return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
955
- }
956
- return panel.current.defaultSize;
957
- }));
1166
+ setSizes(sizes);
958
1167
  }
959
1168
  }, [autoSaveId, panels, storage]);
960
1169
  useEffect(() => {
@@ -972,6 +1181,48 @@ function PanelGroupWithForwardedRef({
972
1181
  debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
973
1182
  }
974
1183
  }, [autoSaveId, panels, sizes, storage]);
1184
+ useIsomorphicLayoutEffect(() => {
1185
+ // Pixel panel constraints need to be reassessed after a group resize
1186
+ // We can avoid the ResizeObserver overhead for relative layouts
1187
+ if (units === "pixels") {
1188
+ const resizeObserver = new ResizeObserver(() => {
1189
+ const {
1190
+ panels,
1191
+ sizes: prevSizes
1192
+ } = committedValuesRef.current;
1193
+ const nextSizes = validatePanelGroupLayout({
1194
+ groupId,
1195
+ panels,
1196
+ nextSizes: prevSizes,
1197
+ prevSizes,
1198
+ units
1199
+ });
1200
+ if (!areEqual(prevSizes, nextSizes)) {
1201
+ setSizes(nextSizes);
1202
+ }
1203
+ });
1204
+ resizeObserver.observe(getPanelGroup(groupId));
1205
+ return () => {
1206
+ resizeObserver.disconnect();
1207
+ };
1208
+ }
1209
+ }, [groupId, units]);
1210
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1211
+ const {
1212
+ panels,
1213
+ units: unitsFromProps
1214
+ } = committedValuesRef.current;
1215
+ const panelsArray = panelsMapToSortedArray(panels);
1216
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1217
+ const size = sizes[index];
1218
+ const units = unitsFromParams ?? unitsFromProps;
1219
+ if (units === "pixels") {
1220
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1221
+ return size / 100 * groupSizePixels;
1222
+ } else {
1223
+ return size;
1224
+ }
1225
+ }, [groupId, sizes]);
975
1226
  const getPanelStyle = useCallback((id, defaultSize) => {
976
1227
  const {
977
1228
  panels
@@ -1002,6 +1253,10 @@ function PanelGroupWithForwardedRef({
1002
1253
  };
1003
1254
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1004
1255
  const registerPanel = useCallback((id, panelRef) => {
1256
+ const {
1257
+ units
1258
+ } = committedValuesRef.current;
1259
+ validatePanelProps(units, panelRef);
1005
1260
  setPanels(prevPanels => {
1006
1261
  if (prevPanels.has(id)) {
1007
1262
  return prevPanels;
@@ -1038,7 +1293,10 @@ function PanelGroupWithForwardedRef({
1038
1293
  }
1039
1294
  const size = isHorizontal ? rect.width : rect.height;
1040
1295
  const delta = movement / size * 100;
1041
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1296
+
1297
+ // If a validateLayout method has been provided
1298
+ // it's important to use it before updating the mouse cursor
1299
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1042
1300
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1043
1301
 
1044
1302
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1065,6 +1323,8 @@ function PanelGroupWithForwardedRef({
1065
1323
  }
1066
1324
  if (sizesChanged) {
1067
1325
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1326
+
1327
+ // It's okay to bypass in this case because we already validated above
1068
1328
  setSizes(nextSizes);
1069
1329
 
1070
1330
  // If resize change handlers have been declared, this is the time to call them.
@@ -1118,7 +1378,7 @@ function PanelGroupWithForwardedRef({
1118
1378
  }
1119
1379
  const isLastPanel = index === panelsArray.length - 1;
1120
1380
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1121
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1381
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1122
1382
  if (prevSizes !== nextSizes) {
1123
1383
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1124
1384
  setSizes(nextSizes);
@@ -1161,7 +1421,7 @@ function PanelGroupWithForwardedRef({
1161
1421
  }
1162
1422
  const isLastPanel = index === panelsArray.length - 1;
1163
1423
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1164
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1424
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1165
1425
  if (prevSizes !== nextSizes) {
1166
1426
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1167
1427
  setSizes(nextSizes);
@@ -1171,21 +1431,34 @@ function PanelGroupWithForwardedRef({
1171
1431
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1172
1432
  }
1173
1433
  }, []);
1174
- const resizePanel = useCallback((id, nextSize) => {
1434
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1175
1435
  const {
1436
+ id: groupId,
1176
1437
  panels,
1177
- sizes: prevSizes
1438
+ sizes: prevSizes,
1439
+ units
1178
1440
  } = committedValuesRef.current;
1441
+ if ((unitsFromParams || units) === "pixels") {
1442
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1443
+ nextSize = nextSize / groupSizePixels * 100;
1444
+ }
1179
1445
  const panel = panels.get(id);
1180
1446
  if (panel == null) {
1181
1447
  return;
1182
1448
  }
1183
- const {
1449
+ let {
1184
1450
  collapsedSize,
1185
1451
  collapsible,
1186
1452
  maxSize,
1187
1453
  minSize
1188
1454
  } = panel.current;
1455
+ if (units === "pixels") {
1456
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1457
+ minSize = minSize / groupSizePixels * 100;
1458
+ if (maxSize != null) {
1459
+ maxSize = maxSize / groupSizePixels * 100;
1460
+ }
1461
+ }
1189
1462
  const panelsArray = panelsMapToSortedArray(panels);
1190
1463
  const index = panelsArray.indexOf(panel);
1191
1464
  if (index < 0) {
@@ -1196,7 +1469,7 @@ function PanelGroupWithForwardedRef({
1196
1469
  return;
1197
1470
  }
1198
1471
  if (collapsible && nextSize === collapsedSize) ; else {
1199
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1472
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1200
1473
  }
1201
1474
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1202
1475
  if (idBefore == null || idAfter == null) {
@@ -1204,7 +1477,7 @@ function PanelGroupWithForwardedRef({
1204
1477
  }
1205
1478
  const isLastPanel = index === panelsArray.length - 1;
1206
1479
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1207
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1480
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1208
1481
  if (prevSizes !== nextSizes) {
1209
1482
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1210
1483
  setSizes(nextSizes);
@@ -1219,6 +1492,7 @@ function PanelGroupWithForwardedRef({
1219
1492
  collapsePanel,
1220
1493
  direction,
1221
1494
  expandPanel,
1495
+ getPanelSize,
1222
1496
  getPanelStyle,
1223
1497
  groupId,
1224
1498
  registerPanel,
@@ -1240,8 +1514,9 @@ function PanelGroupWithForwardedRef({
1240
1514
  setActiveHandleId(null);
1241
1515
  initialDragStateRef.current = null;
1242
1516
  },
1517
+ units,
1243
1518
  unregisterPanel
1244
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1519
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1245
1520
  const style = {
1246
1521
  display: "flex",
1247
1522
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1256,6 +1531,7 @@ function PanelGroupWithForwardedRef({
1256
1531
  "data-panel-group": "",
1257
1532
  "data-panel-group-direction": direction,
1258
1533
  "data-panel-group-id": groupId,
1534
+ "data-panel-group-units": units,
1259
1535
  style: {
1260
1536
  ...style,
1261
1537
  ...styleFromProps
@@ -1405,4 +1681,4 @@ function PanelResizeHandle({
1405
1681
  }
1406
1682
  PanelResizeHandle.displayName = "PanelResizeHandle";
1407
1683
 
1408
- export { Panel, PanelGroup, PanelResizeHandle };
1684
+ export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };