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
@@ -69,8 +69,8 @@ function PanelWithForwardedRef({
69
69
  defaultSize = null,
70
70
  forwardedRef,
71
71
  id: idFromProps = null,
72
- maxSize = 100,
73
- minSize = 10,
72
+ maxSize = null,
73
+ minSize,
74
74
  onCollapse = null,
75
75
  onResize = null,
76
76
  order = null,
@@ -85,11 +85,22 @@ function PanelWithForwardedRef({
85
85
  const {
86
86
  collapsePanel,
87
87
  expandPanel,
88
+ getPanelSize,
88
89
  getPanelStyle,
89
90
  registerPanel,
90
91
  resizePanel,
92
+ units,
91
93
  unregisterPanel
92
94
  } = context;
95
+ if (minSize == null) {
96
+ if (units === "percentages") {
97
+ // Mimics legacy default value for percentage based panel groups
98
+ minSize = 10;
99
+ } else {
100
+ // There is no meaningful minimum pixel default we can provide
101
+ minSize = 0;
102
+ }
103
+ }
93
104
 
94
105
  // Use a ref to guard against users passing inline props
95
106
  const callbacksRef = useRef({
@@ -100,22 +111,6 @@ function PanelWithForwardedRef({
100
111
  callbacksRef.current.onCollapse = onCollapse;
101
112
  callbacksRef.current.onResize = onResize;
102
113
  });
103
-
104
- // Basic props validation
105
- if (minSize < 0 || minSize > 100) {
106
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
107
- } else if (maxSize < 0 || maxSize > 100) {
108
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
109
- } else {
110
- if (defaultSize !== null) {
111
- if (defaultSize < 0 || defaultSize > 100) {
112
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
113
- } else if (minSize > defaultSize && !collapsible) {
114
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
115
- defaultSize = minSize;
116
- }
117
- }
118
- }
119
114
  const style = getPanelStyle(panelId, defaultSize);
120
115
  const committedValuesRef = useRef({
121
116
  size: parseSizeFromStyle(style)
@@ -155,11 +150,14 @@ function PanelWithForwardedRef({
155
150
  getCollapsed() {
156
151
  return committedValuesRef.current.size === 0;
157
152
  },
158
- getSize() {
159
- return committedValuesRef.current.size;
153
+ getId() {
154
+ return panelId;
160
155
  },
161
- resize: percentage => resizePanel(panelId, percentage)
162
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
156
+ getSize(units) {
157
+ return getPanelSize(panelId, units);
158
+ },
159
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
160
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
163
161
  return createElement(Type, {
164
162
  children,
165
163
  className: classNameFromProps,
@@ -195,7 +193,13 @@ function parseSizeFromStyle(style) {
195
193
 
196
194
  const PRECISION = 10;
197
195
 
198
- function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse, initialDragState) {
196
+ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
197
+ const {
198
+ id: groupId,
199
+ panels,
200
+ units
201
+ } = committedValues;
202
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
199
203
  const {
200
204
  sizes: initialSizes
201
205
  } = initialDragState || {};
@@ -203,9 +207,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
203
207
  // If we're resizing by mouse or touch, use the initial sizes as a base.
204
208
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
205
209
  const baseSizes = initialSizes || prevSizes;
206
- if (delta === 0) {
207
- return baseSizes;
208
- }
209
210
  const panelsArray = panelsMapToSortedArray(panels);
210
211
  const nextSizes = baseSizes.concat();
211
212
  let deltaApplied = 0;
@@ -220,11 +221,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
220
221
 
221
222
  // Max-bounds check the panel being expanded first.
222
223
  {
223
- const pivotId = delta < 0 ? idAfter : idBefore;
224
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
224
225
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
225
226
  const panel = panelsArray[index];
226
227
  const baseSize = baseSizes[index];
227
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
228
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
228
229
  if (baseSize === nextSize) {
229
230
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
230
231
  return baseSizes;
@@ -232,29 +233,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
232
233
  if (nextSize === 0 && baseSize > 0) {
233
234
  panelSizeBeforeCollapse.set(pivotId, baseSize);
234
235
  }
235
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
236
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
236
237
  }
237
238
  }
238
- let pivotId = delta < 0 ? idBefore : idAfter;
239
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
239
240
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
240
241
  while (true) {
241
242
  const panel = panelsArray[index];
242
243
  const baseSize = baseSizes[index];
243
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
244
- const nextSize = safeResizePanel(panel, 0 - deltaRemaining, baseSize, event);
244
+ const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
245
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
245
246
  if (baseSize !== nextSize) {
246
247
  if (nextSize === 0 && baseSize > 0) {
247
248
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
248
249
  }
249
250
  deltaApplied += baseSize - nextSize;
250
251
  nextSizes[index] = nextSize;
251
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
252
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
252
253
  numeric: true
253
254
  }) >= 0) {
254
255
  break;
255
256
  }
256
257
  }
257
- if (delta < 0) {
258
+ if (deltaPixels < 0) {
258
259
  if (--index < 0) {
259
260
  break;
260
261
  }
@@ -272,7 +273,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
272
273
  }
273
274
 
274
275
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
275
- pivotId = delta < 0 ? idAfter : idBefore;
276
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
276
277
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
277
278
  nextSizes[index] = baseSizes[index] + deltaApplied;
278
279
  return nextSizes;
@@ -311,6 +312,89 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
311
312
  }
312
313
  });
313
314
  }
315
+ function calculateDefaultLayout({
316
+ groupId,
317
+ panels,
318
+ units
319
+ }) {
320
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
321
+ const panelsArray = panelsMapToSortedArray(panels);
322
+ const sizes = Array(panelsArray.length);
323
+ let numPanelsWithSizes = 0;
324
+ let remainingSize = 100;
325
+
326
+ // Assigning default sizes requires a couple of passes:
327
+ // First, all panels with defaultSize should be set as-is
328
+ for (let index = 0; index < panelsArray.length; index++) {
329
+ const panel = panelsArray[index];
330
+ const {
331
+ defaultSize
332
+ } = panel.current;
333
+ if (defaultSize != null) {
334
+ numPanelsWithSizes++;
335
+ sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
336
+ remainingSize -= sizes[index];
337
+ }
338
+ }
339
+
340
+ // Remaining total size should be distributed evenly between panels
341
+ // This may require two passes, depending on min/max constraints
342
+ for (let index = 0; index < panelsArray.length; index++) {
343
+ const panel = panelsArray[index];
344
+ let {
345
+ defaultSize,
346
+ id,
347
+ maxSize,
348
+ minSize
349
+ } = panel.current;
350
+ if (defaultSize != null) {
351
+ continue;
352
+ }
353
+ if (units === "pixels") {
354
+ minSize = minSize / groupSizePixels * 100;
355
+ if (maxSize != null) {
356
+ maxSize = maxSize / groupSizePixels * 100;
357
+ }
358
+ }
359
+ const remainingPanels = panelsArray.length - numPanelsWithSizes;
360
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
361
+ sizes[index] = size;
362
+ numPanelsWithSizes++;
363
+ remainingSize -= size;
364
+ }
365
+
366
+ // If there is additional, left over space, assign it to any panel(s) that permits it
367
+ // (It's not worth taking multiple additional passes to evenly distribute)
368
+ if (remainingSize !== 0) {
369
+ for (let index = 0; index < panelsArray.length; index++) {
370
+ const panel = panelsArray[index];
371
+ let {
372
+ maxSize,
373
+ minSize
374
+ } = panel.current;
375
+ if (units === "pixels") {
376
+ minSize = minSize / groupSizePixels * 100;
377
+ if (maxSize != null) {
378
+ maxSize = maxSize / groupSizePixels * 100;
379
+ }
380
+ }
381
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
382
+ if (size !== sizes[index]) {
383
+ remainingSize -= size - sizes[index];
384
+ sizes[index] = size;
385
+
386
+ // Fuzzy comparison to account for imprecise floating point math
387
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
388
+ break;
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ // Finally, if there is still left-over size, log an error
395
+ if (Math.abs(remainingSize).toFixed(3) !== "0.000") ;
396
+ return sizes;
397
+ }
314
398
  function getBeforeAndAfterIds(id, panelsArray) {
315
399
  if (panelsArray.length < 2) {
316
400
  return [null, null];
@@ -324,6 +408,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
324
408
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
325
409
  return [idBefore, idAfter];
326
410
  }
411
+ function getAvailableGroupSizePixels(groupId) {
412
+ const panelGroupElement = getPanelGroup(groupId);
413
+ if (panelGroupElement == null) {
414
+ return NaN;
415
+ }
416
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
417
+ const resizeHandles = getResizeHandlesForGroup(groupId);
418
+ if (direction === "horizontal") {
419
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
420
+ return accumulated + handle.offsetWidth;
421
+ }, 0);
422
+ } else {
423
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
424
+ return accumulated + handle.offsetHeight;
425
+ }, 0);
426
+ }
427
+ }
327
428
 
328
429
  // This method returns a number between 1 and 100 representing
329
430
  // the % of the group's overall space this panel should occupy.
@@ -394,18 +495,24 @@ function panelsMapToSortedArray(panels) {
394
495
  }
395
496
  });
396
497
  }
397
- function safeResizePanel(panel, delta, prevSize, event) {
398
- const nextSizeUnsafe = prevSize + delta;
399
- const {
498
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
499
+ let {
400
500
  collapsedSize,
401
501
  collapsible,
402
502
  maxSize,
403
503
  minSize
404
504
  } = panel.current;
505
+ if (units === "pixels") {
506
+ collapsedSize = collapsedSize / groupSizePixels * 100;
507
+ if (maxSize != null) {
508
+ maxSize = maxSize / groupSizePixels * 100;
509
+ }
510
+ minSize = minSize / groupSizePixels * 100;
511
+ }
405
512
  if (collapsible) {
406
513
  if (prevSize > collapsedSize) {
407
514
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
408
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
515
+ if (nextSize <= minSize / 2 + collapsedSize) {
409
516
  return collapsedSize;
410
517
  }
411
518
  } else {
@@ -414,14 +521,97 @@ function safeResizePanel(panel, delta, prevSize, event) {
414
521
  // Keyboard events should expand a collapsed panel to the min size,
415
522
  // but mouse events should wait until the panel has reached its min size
416
523
  // to avoid a visual flickering when dragging between collapsed and min size.
417
- if (nextSizeUnsafe < minSize) {
524
+ if (nextSize < minSize) {
418
525
  return collapsedSize;
419
526
  }
420
527
  }
421
528
  }
422
529
  }
423
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
424
- return nextSize;
530
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
531
+ }
532
+ function validatePanelProps(units, panelData) {
533
+ const {
534
+ collapsible,
535
+ defaultSize,
536
+ maxSize,
537
+ minSize
538
+ } = panelData.current;
539
+
540
+ // Basic props validation
541
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
542
+ panelData.current.minSize = 0;
543
+ }
544
+ if (maxSize != null) {
545
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
546
+ panelData.current.maxSize = null;
547
+ }
548
+ }
549
+ if (defaultSize !== null) {
550
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
551
+ panelData.current.defaultSize = null;
552
+ } else if (defaultSize < minSize && !collapsible) {
553
+ panelData.current.defaultSize = minSize;
554
+ } else if (maxSize != null && defaultSize > maxSize) {
555
+ panelData.current.defaultSize = maxSize;
556
+ }
557
+ }
558
+ }
559
+ function validatePanelGroupLayout({
560
+ groupId,
561
+ panels,
562
+ nextSizes,
563
+ prevSizes,
564
+ units
565
+ }) {
566
+ // Clone because this method modifies
567
+ nextSizes = [...nextSizes];
568
+ const panelsArray = panelsMapToSortedArray(panels);
569
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
570
+ let remainingSize = 0;
571
+
572
+ // First, check all of the proposed sizes against the min/max constraints
573
+ for (let index = 0; index < panelsArray.length; index++) {
574
+ const panel = panelsArray[index];
575
+ const prevSize = prevSizes[index];
576
+ const nextSize = nextSizes[index];
577
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
578
+ if (nextSize != safeNextSize) {
579
+ remainingSize += nextSize - safeNextSize;
580
+ nextSizes[index] = safeNextSize;
581
+ }
582
+ }
583
+
584
+ // If there is additional, left over space, assign it to any panel(s) that permits it
585
+ // (It's not worth taking multiple additional passes to evenly distribute)
586
+ if (remainingSize.toFixed(3) !== "0.000") {
587
+ for (let index = 0; index < panelsArray.length; index++) {
588
+ const panel = panelsArray[index];
589
+ let {
590
+ maxSize,
591
+ minSize
592
+ } = panel.current;
593
+ if (units === "pixels") {
594
+ minSize = minSize / groupSizePixels * 100;
595
+ if (maxSize != null) {
596
+ maxSize = maxSize / groupSizePixels * 100;
597
+ }
598
+ }
599
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
600
+ if (size !== nextSizes[index]) {
601
+ remainingSize -= size - nextSizes[index];
602
+ nextSizes[index] = size;
603
+
604
+ // Fuzzy comparison to account for imprecise floating point math
605
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
606
+ break;
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
613
+ if (remainingSize.toFixed(3) !== "0.000") ;
614
+ return nextSizes;
425
615
  }
426
616
 
427
617
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -447,6 +637,7 @@ function useWindowSplitterPanelGroupBehavior({
447
637
  panels
448
638
  } = committedValuesRef.current;
449
639
  const groupElement = getPanelGroup(groupId);
640
+ assert(groupElement != null, `No group found for id "${groupId}"`);
450
641
  const {
451
642
  height,
452
643
  width
@@ -459,23 +650,28 @@ function useWindowSplitterPanelGroupBehavior({
459
650
  if (idBefore == null || idAfter == null) {
460
651
  return () => {};
461
652
  }
462
- let minSize = 0;
463
- let maxSize = 100;
653
+ let currentMinSize = 0;
654
+ let currentMaxSize = 100;
464
655
  let totalMinSize = 0;
465
656
  let totalMaxSize = 0;
466
657
 
467
658
  // A panel's effective min/max sizes also need to account for other panel's sizes.
468
659
  panelsArray.forEach(panelData => {
469
- if (panelData.current.id === idBefore) {
470
- maxSize = panelData.current.maxSize;
471
- minSize = panelData.current.minSize;
660
+ const {
661
+ id,
662
+ maxSize,
663
+ minSize
664
+ } = panelData.current;
665
+ if (id === idBefore) {
666
+ currentMinSize = minSize;
667
+ currentMaxSize = maxSize != null ? maxSize : 100;
472
668
  } else {
473
- totalMinSize += panelData.current.minSize;
474
- totalMaxSize += panelData.current.maxSize;
669
+ totalMinSize += minSize;
670
+ totalMaxSize += maxSize != null ? maxSize : 100;
475
671
  }
476
672
  });
477
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
478
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
673
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
674
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
479
675
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
480
676
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
481
677
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -499,7 +695,7 @@ function useWindowSplitterPanelGroupBehavior({
499
695
  } else {
500
696
  delta = -(direction === "horizontal" ? width : height);
501
697
  }
502
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
698
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
503
699
  if (sizes !== nextSizes) {
504
700
  setSizes(nextSizes);
505
701
  }
@@ -814,9 +1010,6 @@ const defaultStorage = {
814
1010
  // * dragHandleRect, sizes:
815
1011
  // When resizing is done via mouse/touch event– some initial state is stored
816
1012
  // so that any panels that contract will also expand if drag direction is reversed.
817
- // TODO
818
- // Within an active drag, remember original positions to refine more easily on expand.
819
- // Look at what the Chrome devtools Sources does.
820
1013
  function PanelGroupWithForwardedRef({
821
1014
  autoSaveId,
822
1015
  children = null,
@@ -828,7 +1021,8 @@ function PanelGroupWithForwardedRef({
828
1021
  onLayout,
829
1022
  storage = defaultStorage,
830
1023
  style: styleFromProps = {},
831
- tagName: Type = "div"
1024
+ tagName: Type = "div",
1025
+ units = "percentages"
832
1026
  }) {
833
1027
  const groupId = useUniqueId(idFromProps);
834
1028
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -841,6 +1035,7 @@ function PanelGroupWithForwardedRef({
841
1035
  useRef({
842
1036
  didLogDefaultSizeWarning: false,
843
1037
  didLogIdAndOrderWarning: false,
1038
+ didLogInvalidLayoutWarning: false,
844
1039
  prevPanelIds: []
845
1040
  });
846
1041
 
@@ -863,32 +1058,58 @@ function PanelGroupWithForwardedRef({
863
1058
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
864
1059
  const committedValuesRef = useRef({
865
1060
  direction,
1061
+ id: groupId,
866
1062
  panels,
867
- sizes
1063
+ sizes,
1064
+ units
868
1065
  });
869
1066
  useImperativeHandle(forwardedRef, () => ({
870
- getLayout: () => {
1067
+ getId: () => groupId,
1068
+ getLayout: unitsFromParams => {
871
1069
  const {
872
- sizes
1070
+ sizes,
1071
+ units: unitsFromProps
873
1072
  } = committedValuesRef.current;
874
- return sizes;
1073
+ const units = unitsFromParams ?? unitsFromProps;
1074
+ if (units === "pixels") {
1075
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1076
+ return sizes.map(size => size / 100 * groupSizePixels);
1077
+ } else {
1078
+ return sizes;
1079
+ }
875
1080
  },
876
- setLayout: sizes => {
877
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
878
- assert(total === 100, "Panel sizes must add up to 100%");
1081
+ setLayout: (sizes, unitsFromParams) => {
879
1082
  const {
880
- panels
1083
+ id: groupId,
1084
+ panels,
1085
+ sizes: prevSizes,
1086
+ units
881
1087
  } = committedValuesRef.current;
1088
+ if ((unitsFromParams || units) === "pixels") {
1089
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1090
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1091
+ }
882
1092
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
883
1093
  const panelsArray = panelsMapToSortedArray(panels);
884
- setSizes(sizes);
885
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1094
+ const nextSizes = validatePanelGroupLayout({
1095
+ groupId,
1096
+ panels,
1097
+ nextSizes: sizes,
1098
+ prevSizes,
1099
+ units
1100
+ });
1101
+ if (!areEqual(prevSizes, nextSizes)) {
1102
+ setSizes(nextSizes);
1103
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1104
+ }
886
1105
  }
887
- }), []);
1106
+ }), [groupId]);
888
1107
  useIsomorphicLayoutEffect(() => {
889
1108
  committedValuesRef.current.direction = direction;
1109
+ committedValuesRef.current.id = groupId;
890
1110
  committedValuesRef.current.panels = panels;
891
1111
  committedValuesRef.current.sizes = sizes;
1112
+ committedValuesRef.current.units = units;
892
1113
  });
893
1114
  useWindowSplitterPanelGroupBehavior({
894
1115
  committedValuesRef,
@@ -930,7 +1151,11 @@ function PanelGroupWithForwardedRef({
930
1151
  // Compute the initial sizes based on default weights.
931
1152
  // This assumes that panels register during initial mount (no conditional rendering)!
932
1153
  useIsomorphicLayoutEffect(() => {
933
- const sizes = committedValuesRef.current.sizes;
1154
+ const {
1155
+ id: groupId,
1156
+ sizes,
1157
+ units
1158
+ } = committedValuesRef.current;
934
1159
  if (sizes.length === panels.size) {
935
1160
  // Only compute (or restore) default sizes once per panel configuration.
936
1161
  return;
@@ -944,39 +1169,23 @@ function PanelGroupWithForwardedRef({
944
1169
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
945
1170
  }
946
1171
  if (defaultSizes != null) {
947
- setSizes(defaultSizes);
1172
+ // Validate saved sizes in case something has changed since last render
1173
+ // e.g. for pixel groups, this could be the size of the window
1174
+ const validatedSizes = validatePanelGroupLayout({
1175
+ groupId,
1176
+ panels,
1177
+ nextSizes: defaultSizes,
1178
+ prevSizes: defaultSizes,
1179
+ units
1180
+ });
1181
+ setSizes(validatedSizes);
948
1182
  } else {
949
- const panelsArray = panelsMapToSortedArray(panels);
950
- let panelsWithNullDefaultSize = 0;
951
- let totalDefaultSize = 0;
952
- let totalMinSize = 0;
953
-
954
- // TODO
955
- // Implicit default size calculations below do not account for inferred min/max size values.
956
- // 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.
957
- // For now, these logic edge cases are left to the user to handle via props.
958
-
959
- panelsArray.forEach(panel => {
960
- totalMinSize += panel.current.minSize;
961
- if (panel.current.defaultSize === null) {
962
- panelsWithNullDefaultSize++;
963
- } else {
964
- totalDefaultSize += panel.current.defaultSize;
965
- }
1183
+ const sizes = calculateDefaultLayout({
1184
+ groupId,
1185
+ panels,
1186
+ units
966
1187
  });
967
- if (totalDefaultSize > 100) {
968
- throw new Error(`Default panel sizes cannot exceed 100%`);
969
- } else if (panelsArray.length > 1 && panelsWithNullDefaultSize === 0 && totalDefaultSize !== 100) {
970
- throw new Error(`Invalid default sizes specified for panels`);
971
- } else if (totalMinSize > 100) {
972
- throw new Error(`Minimum panel sizes cannot exceed 100%`);
973
- }
974
- setSizes(panelsArray.map(panel => {
975
- if (panel.current.defaultSize === null) {
976
- return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
977
- }
978
- return panel.current.defaultSize;
979
- }));
1188
+ setSizes(sizes);
980
1189
  }
981
1190
  }, [autoSaveId, panels, storage]);
982
1191
  useEffect(() => {
@@ -994,6 +1203,48 @@ function PanelGroupWithForwardedRef({
994
1203
  debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
995
1204
  }
996
1205
  }, [autoSaveId, panels, sizes, storage]);
1206
+ useIsomorphicLayoutEffect(() => {
1207
+ // Pixel panel constraints need to be reassessed after a group resize
1208
+ // We can avoid the ResizeObserver overhead for relative layouts
1209
+ if (units === "pixels") {
1210
+ const resizeObserver = new ResizeObserver(() => {
1211
+ const {
1212
+ panels,
1213
+ sizes: prevSizes
1214
+ } = committedValuesRef.current;
1215
+ const nextSizes = validatePanelGroupLayout({
1216
+ groupId,
1217
+ panels,
1218
+ nextSizes: prevSizes,
1219
+ prevSizes,
1220
+ units
1221
+ });
1222
+ if (!areEqual(prevSizes, nextSizes)) {
1223
+ setSizes(nextSizes);
1224
+ }
1225
+ });
1226
+ resizeObserver.observe(getPanelGroup(groupId));
1227
+ return () => {
1228
+ resizeObserver.disconnect();
1229
+ };
1230
+ }
1231
+ }, [groupId, units]);
1232
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1233
+ const {
1234
+ panels,
1235
+ units: unitsFromProps
1236
+ } = committedValuesRef.current;
1237
+ const panelsArray = panelsMapToSortedArray(panels);
1238
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1239
+ const size = sizes[index];
1240
+ const units = unitsFromParams ?? unitsFromProps;
1241
+ if (units === "pixels") {
1242
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1243
+ return size / 100 * groupSizePixels;
1244
+ } else {
1245
+ return size;
1246
+ }
1247
+ }, [groupId, sizes]);
997
1248
  const getPanelStyle = useCallback((id, defaultSize) => {
998
1249
  const {
999
1250
  panels
@@ -1024,6 +1275,10 @@ function PanelGroupWithForwardedRef({
1024
1275
  };
1025
1276
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1026
1277
  const registerPanel = useCallback((id, panelRef) => {
1278
+ const {
1279
+ units
1280
+ } = committedValuesRef.current;
1281
+ validatePanelProps(units, panelRef);
1027
1282
  setPanels(prevPanels => {
1028
1283
  if (prevPanels.has(id)) {
1029
1284
  return prevPanels;
@@ -1060,7 +1315,10 @@ function PanelGroupWithForwardedRef({
1060
1315
  }
1061
1316
  const size = isHorizontal ? rect.width : rect.height;
1062
1317
  const delta = movement / size * 100;
1063
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1318
+
1319
+ // If a validateLayout method has been provided
1320
+ // it's important to use it before updating the mouse cursor
1321
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1064
1322
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1065
1323
 
1066
1324
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1087,6 +1345,8 @@ function PanelGroupWithForwardedRef({
1087
1345
  }
1088
1346
  if (sizesChanged) {
1089
1347
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1348
+
1349
+ // It's okay to bypass in this case because we already validated above
1090
1350
  setSizes(nextSizes);
1091
1351
 
1092
1352
  // If resize change handlers have been declared, this is the time to call them.
@@ -1140,7 +1400,7 @@ function PanelGroupWithForwardedRef({
1140
1400
  }
1141
1401
  const isLastPanel = index === panelsArray.length - 1;
1142
1402
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1143
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1403
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1144
1404
  if (prevSizes !== nextSizes) {
1145
1405
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1146
1406
  setSizes(nextSizes);
@@ -1183,7 +1443,7 @@ function PanelGroupWithForwardedRef({
1183
1443
  }
1184
1444
  const isLastPanel = index === panelsArray.length - 1;
1185
1445
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1186
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1446
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1187
1447
  if (prevSizes !== nextSizes) {
1188
1448
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1189
1449
  setSizes(nextSizes);
@@ -1193,21 +1453,34 @@ function PanelGroupWithForwardedRef({
1193
1453
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1194
1454
  }
1195
1455
  }, []);
1196
- const resizePanel = useCallback((id, nextSize) => {
1456
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1197
1457
  const {
1458
+ id: groupId,
1198
1459
  panels,
1199
- sizes: prevSizes
1460
+ sizes: prevSizes,
1461
+ units
1200
1462
  } = committedValuesRef.current;
1463
+ if ((unitsFromParams || units) === "pixels") {
1464
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1465
+ nextSize = nextSize / groupSizePixels * 100;
1466
+ }
1201
1467
  const panel = panels.get(id);
1202
1468
  if (panel == null) {
1203
1469
  return;
1204
1470
  }
1205
- const {
1471
+ let {
1206
1472
  collapsedSize,
1207
1473
  collapsible,
1208
1474
  maxSize,
1209
1475
  minSize
1210
1476
  } = panel.current;
1477
+ if (units === "pixels") {
1478
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1479
+ minSize = minSize / groupSizePixels * 100;
1480
+ if (maxSize != null) {
1481
+ maxSize = maxSize / groupSizePixels * 100;
1482
+ }
1483
+ }
1211
1484
  const panelsArray = panelsMapToSortedArray(panels);
1212
1485
  const index = panelsArray.indexOf(panel);
1213
1486
  if (index < 0) {
@@ -1218,7 +1491,7 @@ function PanelGroupWithForwardedRef({
1218
1491
  return;
1219
1492
  }
1220
1493
  if (collapsible && nextSize === collapsedSize) ; else {
1221
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1494
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1222
1495
  }
1223
1496
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1224
1497
  if (idBefore == null || idAfter == null) {
@@ -1226,7 +1499,7 @@ function PanelGroupWithForwardedRef({
1226
1499
  }
1227
1500
  const isLastPanel = index === panelsArray.length - 1;
1228
1501
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1229
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1502
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1230
1503
  if (prevSizes !== nextSizes) {
1231
1504
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1232
1505
  setSizes(nextSizes);
@@ -1241,6 +1514,7 @@ function PanelGroupWithForwardedRef({
1241
1514
  collapsePanel,
1242
1515
  direction,
1243
1516
  expandPanel,
1517
+ getPanelSize,
1244
1518
  getPanelStyle,
1245
1519
  groupId,
1246
1520
  registerPanel,
@@ -1262,8 +1536,9 @@ function PanelGroupWithForwardedRef({
1262
1536
  setActiveHandleId(null);
1263
1537
  initialDragStateRef.current = null;
1264
1538
  },
1539
+ units,
1265
1540
  unregisterPanel
1266
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1541
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1267
1542
  const style = {
1268
1543
  display: "flex",
1269
1544
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1278,6 +1553,7 @@ function PanelGroupWithForwardedRef({
1278
1553
  "data-panel-group": "",
1279
1554
  "data-panel-group-direction": direction,
1280
1555
  "data-panel-group-id": groupId,
1556
+ "data-panel-group-units": units,
1281
1557
  style: {
1282
1558
  ...style,
1283
1559
  ...styleFromProps
@@ -1430,3 +1706,4 @@ PanelResizeHandle.displayName = "PanelResizeHandle";
1430
1706
  exports.Panel = Panel;
1431
1707
  exports.PanelGroup = PanelGroup;
1432
1708
  exports.PanelResizeHandle = PanelResizeHandle;
1709
+ exports.getAvailableGroupSizePixels = getAvailableGroupSizePixels;