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;
155
+ },
156
+ getSize(units) {
157
+ return getPanelSize(panelId, units);
160
158
  },
161
- resize: percentage => resizePanel(panelId, percentage)
162
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
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,93 @@ 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
+ {
397
+ console.error(`Invalid panel group configuration; default panel sizes should total 100% but was ${(100 - remainingSize).toFixed(1)}%. This can cause the cursor to become misaligned while dragging.`);
398
+ }
399
+ }
400
+ return sizes;
401
+ }
314
402
  function getBeforeAndAfterIds(id, panelsArray) {
315
403
  if (panelsArray.length < 2) {
316
404
  return [null, null];
@@ -324,6 +412,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
324
412
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
325
413
  return [idBefore, idAfter];
326
414
  }
415
+ function getAvailableGroupSizePixels(groupId) {
416
+ const panelGroupElement = getPanelGroup(groupId);
417
+ if (panelGroupElement == null) {
418
+ return NaN;
419
+ }
420
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
421
+ const resizeHandles = getResizeHandlesForGroup(groupId);
422
+ if (direction === "horizontal") {
423
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
424
+ return accumulated + handle.offsetWidth;
425
+ }, 0);
426
+ } else {
427
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
428
+ return accumulated + handle.offsetHeight;
429
+ }, 0);
430
+ }
431
+ }
327
432
 
328
433
  // This method returns a number between 1 and 100 representing
329
434
  // the % of the group's overall space this panel should occupy.
@@ -394,18 +499,24 @@ function panelsMapToSortedArray(panels) {
394
499
  }
395
500
  });
396
501
  }
397
- function safeResizePanel(panel, delta, prevSize, event) {
398
- const nextSizeUnsafe = prevSize + delta;
399
- const {
502
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
503
+ let {
400
504
  collapsedSize,
401
505
  collapsible,
402
506
  maxSize,
403
507
  minSize
404
508
  } = panel.current;
509
+ if (units === "pixels") {
510
+ collapsedSize = collapsedSize / groupSizePixels * 100;
511
+ if (maxSize != null) {
512
+ maxSize = maxSize / groupSizePixels * 100;
513
+ }
514
+ minSize = minSize / groupSizePixels * 100;
515
+ }
405
516
  if (collapsible) {
406
517
  if (prevSize > collapsedSize) {
407
518
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
408
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
519
+ if (nextSize <= minSize / 2 + collapsedSize) {
409
520
  return collapsedSize;
410
521
  }
411
522
  } else {
@@ -414,14 +525,119 @@ function safeResizePanel(panel, delta, prevSize, event) {
414
525
  // Keyboard events should expand a collapsed panel to the min size,
415
526
  // but mouse events should wait until the panel has reached its min size
416
527
  // to avoid a visual flickering when dragging between collapsed and min size.
417
- if (nextSizeUnsafe < minSize) {
528
+ if (nextSize < minSize) {
418
529
  return collapsedSize;
419
530
  }
420
531
  }
421
532
  }
422
533
  }
423
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
424
- return nextSize;
534
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
535
+ }
536
+ function validatePanelProps(units, panelData) {
537
+ const {
538
+ collapsible,
539
+ defaultSize,
540
+ maxSize,
541
+ minSize
542
+ } = panelData.current;
543
+
544
+ // Basic props validation
545
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
546
+ {
547
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
548
+ }
549
+ panelData.current.minSize = 0;
550
+ }
551
+ if (maxSize != null) {
552
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
553
+ {
554
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
555
+ }
556
+ panelData.current.maxSize = null;
557
+ }
558
+ }
559
+ if (defaultSize !== null) {
560
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
561
+ {
562
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
563
+ }
564
+ panelData.current.defaultSize = null;
565
+ } else if (defaultSize < minSize && !collapsible) {
566
+ {
567
+ console.error(`Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`);
568
+ }
569
+ panelData.current.defaultSize = minSize;
570
+ } else if (maxSize != null && defaultSize > maxSize) {
571
+ {
572
+ console.error(`Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`);
573
+ }
574
+ panelData.current.defaultSize = maxSize;
575
+ }
576
+ }
577
+ }
578
+ function validatePanelGroupLayout({
579
+ groupId,
580
+ panels,
581
+ nextSizes,
582
+ prevSizes,
583
+ units
584
+ }) {
585
+ // Clone because this method modifies
586
+ nextSizes = [...nextSizes];
587
+ const panelsArray = panelsMapToSortedArray(panels);
588
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
589
+ let remainingSize = 0;
590
+
591
+ // First, check all of the proposed sizes against the min/max constraints
592
+ for (let index = 0; index < panelsArray.length; index++) {
593
+ const panel = panelsArray[index];
594
+ const prevSize = prevSizes[index];
595
+ const nextSize = nextSizes[index];
596
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
597
+ if (nextSize != safeNextSize) {
598
+ remainingSize += nextSize - safeNextSize;
599
+ nextSizes[index] = safeNextSize;
600
+ {
601
+ console.error(`Invalid size (${nextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
602
+ }
603
+ }
604
+ }
605
+
606
+ // If there is additional, left over space, assign it to any panel(s) that permits it
607
+ // (It's not worth taking multiple additional passes to evenly distribute)
608
+ if (remainingSize.toFixed(3) !== "0.000") {
609
+ for (let index = 0; index < panelsArray.length; index++) {
610
+ const panel = panelsArray[index];
611
+ let {
612
+ maxSize,
613
+ minSize
614
+ } = panel.current;
615
+ if (units === "pixels") {
616
+ minSize = minSize / groupSizePixels * 100;
617
+ if (maxSize != null) {
618
+ maxSize = maxSize / groupSizePixels * 100;
619
+ }
620
+ }
621
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
622
+ if (size !== nextSizes[index]) {
623
+ remainingSize -= size - nextSizes[index];
624
+ nextSizes[index] = size;
625
+
626
+ // Fuzzy comparison to account for imprecise floating point math
627
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
628
+ break;
629
+ }
630
+ }
631
+ }
632
+ }
633
+
634
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
635
+ if (remainingSize.toFixed(3) !== "0.000") {
636
+ {
637
+ console.error(`"Invalid panel group configuration; default panel sizes should total 100% but was ${100 - remainingSize}%`);
638
+ }
639
+ }
640
+ return nextSizes;
425
641
  }
426
642
 
427
643
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -447,6 +663,7 @@ function useWindowSplitterPanelGroupBehavior({
447
663
  panels
448
664
  } = committedValuesRef.current;
449
665
  const groupElement = getPanelGroup(groupId);
666
+ assert(groupElement != null, `No group found for id "${groupId}"`);
450
667
  const {
451
668
  height,
452
669
  width
@@ -459,23 +676,28 @@ function useWindowSplitterPanelGroupBehavior({
459
676
  if (idBefore == null || idAfter == null) {
460
677
  return () => {};
461
678
  }
462
- let minSize = 0;
463
- let maxSize = 100;
679
+ let currentMinSize = 0;
680
+ let currentMaxSize = 100;
464
681
  let totalMinSize = 0;
465
682
  let totalMaxSize = 0;
466
683
 
467
684
  // A panel's effective min/max sizes also need to account for other panel's sizes.
468
685
  panelsArray.forEach(panelData => {
469
- if (panelData.current.id === idBefore) {
470
- maxSize = panelData.current.maxSize;
471
- minSize = panelData.current.minSize;
686
+ const {
687
+ id,
688
+ maxSize,
689
+ minSize
690
+ } = panelData.current;
691
+ if (id === idBefore) {
692
+ currentMinSize = minSize;
693
+ currentMaxSize = maxSize != null ? maxSize : 100;
472
694
  } else {
473
- totalMinSize += panelData.current.minSize;
474
- totalMaxSize += panelData.current.maxSize;
695
+ totalMinSize += minSize;
696
+ totalMaxSize += maxSize != null ? maxSize : 100;
475
697
  }
476
698
  });
477
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
478
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
699
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
700
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
479
701
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
480
702
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
481
703
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -499,7 +721,7 @@ function useWindowSplitterPanelGroupBehavior({
499
721
  } else {
500
722
  delta = -(direction === "horizontal" ? width : height);
501
723
  }
502
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
724
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
503
725
  if (sizes !== nextSizes) {
504
726
  setSizes(nextSizes);
505
727
  }
@@ -814,9 +1036,6 @@ const defaultStorage = {
814
1036
  // * dragHandleRect, sizes:
815
1037
  // When resizing is done via mouse/touch event– some initial state is stored
816
1038
  // 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
1039
  function PanelGroupWithForwardedRef({
821
1040
  autoSaveId,
822
1041
  children = null,
@@ -828,7 +1047,8 @@ function PanelGroupWithForwardedRef({
828
1047
  onLayout,
829
1048
  storage = defaultStorage,
830
1049
  style: styleFromProps = {},
831
- tagName: Type = "div"
1050
+ tagName: Type = "div",
1051
+ units = "percentages"
832
1052
  }) {
833
1053
  const groupId = useUniqueId(idFromProps);
834
1054
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -841,6 +1061,7 @@ function PanelGroupWithForwardedRef({
841
1061
  const devWarningsRef = useRef({
842
1062
  didLogDefaultSizeWarning: false,
843
1063
  didLogIdAndOrderWarning: false,
1064
+ didLogInvalidLayoutWarning: false,
844
1065
  prevPanelIds: []
845
1066
  });
846
1067
 
@@ -863,32 +1084,58 @@ function PanelGroupWithForwardedRef({
863
1084
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
864
1085
  const committedValuesRef = useRef({
865
1086
  direction,
1087
+ id: groupId,
866
1088
  panels,
867
- sizes
1089
+ sizes,
1090
+ units
868
1091
  });
869
1092
  useImperativeHandle(forwardedRef, () => ({
870
- getLayout: () => {
1093
+ getId: () => groupId,
1094
+ getLayout: unitsFromParams => {
871
1095
  const {
872
- sizes
1096
+ sizes,
1097
+ units: unitsFromProps
873
1098
  } = committedValuesRef.current;
874
- return sizes;
1099
+ const units = unitsFromParams ?? unitsFromProps;
1100
+ if (units === "pixels") {
1101
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1102
+ return sizes.map(size => size / 100 * groupSizePixels);
1103
+ } else {
1104
+ return sizes;
1105
+ }
875
1106
  },
876
- setLayout: sizes => {
877
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
878
- assert(total === 100, "Panel sizes must add up to 100%");
1107
+ setLayout: (sizes, unitsFromParams) => {
879
1108
  const {
880
- panels
1109
+ id: groupId,
1110
+ panels,
1111
+ sizes: prevSizes,
1112
+ units
881
1113
  } = committedValuesRef.current;
1114
+ if ((unitsFromParams || units) === "pixels") {
1115
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1116
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1117
+ }
882
1118
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
883
1119
  const panelsArray = panelsMapToSortedArray(panels);
884
- setSizes(sizes);
885
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1120
+ const nextSizes = validatePanelGroupLayout({
1121
+ groupId,
1122
+ panels,
1123
+ nextSizes: sizes,
1124
+ prevSizes,
1125
+ units
1126
+ });
1127
+ if (!areEqual(prevSizes, nextSizes)) {
1128
+ setSizes(nextSizes);
1129
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1130
+ }
886
1131
  }
887
- }), []);
1132
+ }), [groupId]);
888
1133
  useIsomorphicLayoutEffect(() => {
889
1134
  committedValuesRef.current.direction = direction;
1135
+ committedValuesRef.current.id = groupId;
890
1136
  committedValuesRef.current.panels = panels;
891
1137
  committedValuesRef.current.sizes = sizes;
1138
+ committedValuesRef.current.units = units;
892
1139
  });
893
1140
  useWindowSplitterPanelGroupBehavior({
894
1141
  committedValuesRef,
@@ -930,7 +1177,11 @@ function PanelGroupWithForwardedRef({
930
1177
  // Compute the initial sizes based on default weights.
931
1178
  // This assumes that panels register during initial mount (no conditional rendering)!
932
1179
  useIsomorphicLayoutEffect(() => {
933
- const sizes = committedValuesRef.current.sizes;
1180
+ const {
1181
+ id: groupId,
1182
+ sizes,
1183
+ units
1184
+ } = committedValuesRef.current;
934
1185
  if (sizes.length === panels.size) {
935
1186
  // Only compute (or restore) default sizes once per panel configuration.
936
1187
  return;
@@ -944,39 +1195,23 @@ function PanelGroupWithForwardedRef({
944
1195
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
945
1196
  }
946
1197
  if (defaultSizes != null) {
947
- setSizes(defaultSizes);
1198
+ // Validate saved sizes in case something has changed since last render
1199
+ // e.g. for pixel groups, this could be the size of the window
1200
+ const validatedSizes = validatePanelGroupLayout({
1201
+ groupId,
1202
+ panels,
1203
+ nextSizes: defaultSizes,
1204
+ prevSizes: defaultSizes,
1205
+ units
1206
+ });
1207
+ setSizes(validatedSizes);
948
1208
  } 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
- }
1209
+ const sizes = calculateDefaultLayout({
1210
+ groupId,
1211
+ panels,
1212
+ units
966
1213
  });
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
- }));
1214
+ setSizes(sizes);
980
1215
  }
981
1216
  }, [autoSaveId, panels, storage]);
982
1217
  useEffect(() => {
@@ -1014,6 +1249,48 @@ function PanelGroupWithForwardedRef({
1014
1249
  }
1015
1250
  }
1016
1251
  }, [autoSaveId, panels, sizes, storage]);
1252
+ useIsomorphicLayoutEffect(() => {
1253
+ // Pixel panel constraints need to be reassessed after a group resize
1254
+ // We can avoid the ResizeObserver overhead for relative layouts
1255
+ if (units === "pixels") {
1256
+ const resizeObserver = new ResizeObserver(() => {
1257
+ const {
1258
+ panels,
1259
+ sizes: prevSizes
1260
+ } = committedValuesRef.current;
1261
+ const nextSizes = validatePanelGroupLayout({
1262
+ groupId,
1263
+ panels,
1264
+ nextSizes: prevSizes,
1265
+ prevSizes,
1266
+ units
1267
+ });
1268
+ if (!areEqual(prevSizes, nextSizes)) {
1269
+ setSizes(nextSizes);
1270
+ }
1271
+ });
1272
+ resizeObserver.observe(getPanelGroup(groupId));
1273
+ return () => {
1274
+ resizeObserver.disconnect();
1275
+ };
1276
+ }
1277
+ }, [groupId, units]);
1278
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1279
+ const {
1280
+ panels,
1281
+ units: unitsFromProps
1282
+ } = committedValuesRef.current;
1283
+ const panelsArray = panelsMapToSortedArray(panels);
1284
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1285
+ const size = sizes[index];
1286
+ const units = unitsFromParams ?? unitsFromProps;
1287
+ if (units === "pixels") {
1288
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1289
+ return size / 100 * groupSizePixels;
1290
+ } else {
1291
+ return size;
1292
+ }
1293
+ }, [groupId, sizes]);
1017
1294
  const getPanelStyle = useCallback((id, defaultSize) => {
1018
1295
  const {
1019
1296
  panels
@@ -1047,6 +1324,10 @@ function PanelGroupWithForwardedRef({
1047
1324
  };
1048
1325
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1049
1326
  const registerPanel = useCallback((id, panelRef) => {
1327
+ const {
1328
+ units
1329
+ } = committedValuesRef.current;
1330
+ validatePanelProps(units, panelRef);
1050
1331
  setPanels(prevPanels => {
1051
1332
  if (prevPanels.has(id)) {
1052
1333
  return prevPanels;
@@ -1083,7 +1364,10 @@ function PanelGroupWithForwardedRef({
1083
1364
  }
1084
1365
  const size = isHorizontal ? rect.width : rect.height;
1085
1366
  const delta = movement / size * 100;
1086
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1367
+
1368
+ // If a validateLayout method has been provided
1369
+ // it's important to use it before updating the mouse cursor
1370
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1087
1371
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1088
1372
 
1089
1373
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1110,6 +1394,8 @@ function PanelGroupWithForwardedRef({
1110
1394
  }
1111
1395
  if (sizesChanged) {
1112
1396
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1397
+
1398
+ // It's okay to bypass in this case because we already validated above
1113
1399
  setSizes(nextSizes);
1114
1400
 
1115
1401
  // If resize change handlers have been declared, this is the time to call them.
@@ -1163,7 +1449,7 @@ function PanelGroupWithForwardedRef({
1163
1449
  }
1164
1450
  const isLastPanel = index === panelsArray.length - 1;
1165
1451
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1166
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1452
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1167
1453
  if (prevSizes !== nextSizes) {
1168
1454
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1169
1455
  setSizes(nextSizes);
@@ -1206,7 +1492,7 @@ function PanelGroupWithForwardedRef({
1206
1492
  }
1207
1493
  const isLastPanel = index === panelsArray.length - 1;
1208
1494
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1209
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1495
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1210
1496
  if (prevSizes !== nextSizes) {
1211
1497
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1212
1498
  setSizes(nextSizes);
@@ -1216,21 +1502,34 @@ function PanelGroupWithForwardedRef({
1216
1502
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1217
1503
  }
1218
1504
  }, []);
1219
- const resizePanel = useCallback((id, nextSize) => {
1505
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1220
1506
  const {
1507
+ id: groupId,
1221
1508
  panels,
1222
- sizes: prevSizes
1509
+ sizes: prevSizes,
1510
+ units
1223
1511
  } = committedValuesRef.current;
1512
+ if ((unitsFromParams || units) === "pixels") {
1513
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1514
+ nextSize = nextSize / groupSizePixels * 100;
1515
+ }
1224
1516
  const panel = panels.get(id);
1225
1517
  if (panel == null) {
1226
1518
  return;
1227
1519
  }
1228
- const {
1520
+ let {
1229
1521
  collapsedSize,
1230
1522
  collapsible,
1231
1523
  maxSize,
1232
1524
  minSize
1233
1525
  } = panel.current;
1526
+ if (units === "pixels") {
1527
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1528
+ minSize = minSize / groupSizePixels * 100;
1529
+ if (maxSize != null) {
1530
+ maxSize = maxSize / groupSizePixels * 100;
1531
+ }
1532
+ }
1234
1533
  const panelsArray = panelsMapToSortedArray(panels);
1235
1534
  const index = panelsArray.indexOf(panel);
1236
1535
  if (index < 0) {
@@ -1241,7 +1540,13 @@ function PanelGroupWithForwardedRef({
1241
1540
  return;
1242
1541
  }
1243
1542
  if (collapsible && nextSize === collapsedSize) ; else {
1244
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1543
+ const unsafeNextSize = nextSize;
1544
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1545
+ {
1546
+ if (unsafeNextSize !== nextSize) {
1547
+ console.error(`Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
1548
+ }
1549
+ }
1245
1550
  }
1246
1551
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1247
1552
  if (idBefore == null || idAfter == null) {
@@ -1249,7 +1554,7 @@ function PanelGroupWithForwardedRef({
1249
1554
  }
1250
1555
  const isLastPanel = index === panelsArray.length - 1;
1251
1556
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1252
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1557
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1253
1558
  if (prevSizes !== nextSizes) {
1254
1559
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1255
1560
  setSizes(nextSizes);
@@ -1264,6 +1569,7 @@ function PanelGroupWithForwardedRef({
1264
1569
  collapsePanel,
1265
1570
  direction,
1266
1571
  expandPanel,
1572
+ getPanelSize,
1267
1573
  getPanelStyle,
1268
1574
  groupId,
1269
1575
  registerPanel,
@@ -1285,8 +1591,9 @@ function PanelGroupWithForwardedRef({
1285
1591
  setActiveHandleId(null);
1286
1592
  initialDragStateRef.current = null;
1287
1593
  },
1594
+ units,
1288
1595
  unregisterPanel
1289
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1596
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1290
1597
  const style = {
1291
1598
  display: "flex",
1292
1599
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1301,6 +1608,7 @@ function PanelGroupWithForwardedRef({
1301
1608
  "data-panel-group": "",
1302
1609
  "data-panel-group-direction": direction,
1303
1610
  "data-panel-group-id": groupId,
1611
+ "data-panel-group-units": units,
1304
1612
  style: {
1305
1613
  ...style,
1306
1614
  ...styleFromProps
@@ -1453,3 +1761,4 @@ PanelResizeHandle.displayName = "PanelResizeHandle";
1453
1761
  exports.Panel = Panel;
1454
1762
  exports.PanelGroup = PanelGroup;
1455
1763
  exports.PanelResizeHandle = PanelResizeHandle;
1764
+ exports.getAvailableGroupSizePixels = getAvailableGroupSizePixels;