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
@@ -71,8 +71,8 @@ function PanelWithForwardedRef({
71
71
  defaultSize = null,
72
72
  forwardedRef,
73
73
  id: idFromProps = null,
74
- maxSize = 100,
75
- minSize = 10,
74
+ maxSize = null,
75
+ minSize,
76
76
  onCollapse = null,
77
77
  onResize = null,
78
78
  order = null,
@@ -87,11 +87,22 @@ function PanelWithForwardedRef({
87
87
  const {
88
88
  collapsePanel,
89
89
  expandPanel,
90
+ getPanelSize,
90
91
  getPanelStyle,
91
92
  registerPanel,
92
93
  resizePanel,
94
+ units,
93
95
  unregisterPanel
94
96
  } = context;
97
+ if (minSize == null) {
98
+ if (units === "percentages") {
99
+ // Mimics legacy default value for percentage based panel groups
100
+ minSize = 10;
101
+ } else {
102
+ // There is no meaningful minimum pixel default we can provide
103
+ minSize = 0;
104
+ }
105
+ }
95
106
 
96
107
  // Use a ref to guard against users passing inline props
97
108
  const callbacksRef = useRef({
@@ -102,22 +113,6 @@ function PanelWithForwardedRef({
102
113
  callbacksRef.current.onCollapse = onCollapse;
103
114
  callbacksRef.current.onResize = onResize;
104
115
  });
105
-
106
- // Basic props validation
107
- if (minSize < 0 || minSize > 100) {
108
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
109
- } else if (maxSize < 0 || maxSize > 100) {
110
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
111
- } else {
112
- if (defaultSize !== null) {
113
- if (defaultSize < 0 || defaultSize > 100) {
114
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
115
- } else if (minSize > defaultSize && !collapsible) {
116
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
117
- defaultSize = minSize;
118
- }
119
- }
120
- }
121
116
  const style = getPanelStyle(panelId, defaultSize);
122
117
  const committedValuesRef = useRef({
123
118
  size: parseSizeFromStyle(style)
@@ -157,11 +152,14 @@ function PanelWithForwardedRef({
157
152
  getCollapsed() {
158
153
  return committedValuesRef.current.size === 0;
159
154
  },
160
- getSize() {
161
- return committedValuesRef.current.size;
155
+ getId() {
156
+ return panelId;
157
+ },
158
+ getSize(units) {
159
+ return getPanelSize(panelId, units);
162
160
  },
163
- resize: percentage => resizePanel(panelId, percentage)
164
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
161
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
162
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
165
163
  return createElement(Type, {
166
164
  children,
167
165
  className: classNameFromProps,
@@ -197,7 +195,13 @@ function parseSizeFromStyle(style) {
197
195
 
198
196
  const PRECISION = 10;
199
197
 
200
- function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse, initialDragState) {
198
+ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
199
+ const {
200
+ id: groupId,
201
+ panels,
202
+ units
203
+ } = committedValues;
204
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
201
205
  const {
202
206
  sizes: initialSizes
203
207
  } = initialDragState || {};
@@ -205,9 +209,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
205
209
  // If we're resizing by mouse or touch, use the initial sizes as a base.
206
210
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
207
211
  const baseSizes = initialSizes || prevSizes;
208
- if (delta === 0) {
209
- return baseSizes;
210
- }
211
212
  const panelsArray = panelsMapToSortedArray(panels);
212
213
  const nextSizes = baseSizes.concat();
213
214
  let deltaApplied = 0;
@@ -222,11 +223,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
222
223
 
223
224
  // Max-bounds check the panel being expanded first.
224
225
  {
225
- const pivotId = delta < 0 ? idAfter : idBefore;
226
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
226
227
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
227
228
  const panel = panelsArray[index];
228
229
  const baseSize = baseSizes[index];
229
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
230
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
230
231
  if (baseSize === nextSize) {
231
232
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
232
233
  return baseSizes;
@@ -234,29 +235,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
234
235
  if (nextSize === 0 && baseSize > 0) {
235
236
  panelSizeBeforeCollapse.set(pivotId, baseSize);
236
237
  }
237
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
238
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
238
239
  }
239
240
  }
240
- let pivotId = delta < 0 ? idBefore : idAfter;
241
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
241
242
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
242
243
  while (true) {
243
244
  const panel = panelsArray[index];
244
245
  const baseSize = baseSizes[index];
245
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
246
- const nextSize = safeResizePanel(panel, 0 - deltaRemaining, baseSize, event);
246
+ const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
247
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
247
248
  if (baseSize !== nextSize) {
248
249
  if (nextSize === 0 && baseSize > 0) {
249
250
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
250
251
  }
251
252
  deltaApplied += baseSize - nextSize;
252
253
  nextSizes[index] = nextSize;
253
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
254
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
254
255
  numeric: true
255
256
  }) >= 0) {
256
257
  break;
257
258
  }
258
259
  }
259
- if (delta < 0) {
260
+ if (deltaPixels < 0) {
260
261
  if (--index < 0) {
261
262
  break;
262
263
  }
@@ -274,7 +275,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
274
275
  }
275
276
 
276
277
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
277
- pivotId = delta < 0 ? idAfter : idBefore;
278
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
278
279
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
279
280
  nextSizes[index] = baseSizes[index] + deltaApplied;
280
281
  return nextSizes;
@@ -313,6 +314,93 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
313
314
  }
314
315
  });
315
316
  }
317
+ function calculateDefaultLayout({
318
+ groupId,
319
+ panels,
320
+ units
321
+ }) {
322
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
323
+ const panelsArray = panelsMapToSortedArray(panels);
324
+ const sizes = Array(panelsArray.length);
325
+ let numPanelsWithSizes = 0;
326
+ let remainingSize = 100;
327
+
328
+ // Assigning default sizes requires a couple of passes:
329
+ // First, all panels with defaultSize should be set as-is
330
+ for (let index = 0; index < panelsArray.length; index++) {
331
+ const panel = panelsArray[index];
332
+ const {
333
+ defaultSize
334
+ } = panel.current;
335
+ if (defaultSize != null) {
336
+ numPanelsWithSizes++;
337
+ sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
338
+ remainingSize -= sizes[index];
339
+ }
340
+ }
341
+
342
+ // Remaining total size should be distributed evenly between panels
343
+ // This may require two passes, depending on min/max constraints
344
+ for (let index = 0; index < panelsArray.length; index++) {
345
+ const panel = panelsArray[index];
346
+ let {
347
+ defaultSize,
348
+ id,
349
+ maxSize,
350
+ minSize
351
+ } = panel.current;
352
+ if (defaultSize != null) {
353
+ continue;
354
+ }
355
+ if (units === "pixels") {
356
+ minSize = minSize / groupSizePixels * 100;
357
+ if (maxSize != null) {
358
+ maxSize = maxSize / groupSizePixels * 100;
359
+ }
360
+ }
361
+ const remainingPanels = panelsArray.length - numPanelsWithSizes;
362
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
363
+ sizes[index] = size;
364
+ numPanelsWithSizes++;
365
+ remainingSize -= size;
366
+ }
367
+
368
+ // If there is additional, left over space, assign it to any panel(s) that permits it
369
+ // (It's not worth taking multiple additional passes to evenly distribute)
370
+ if (remainingSize !== 0) {
371
+ for (let index = 0; index < panelsArray.length; index++) {
372
+ const panel = panelsArray[index];
373
+ let {
374
+ maxSize,
375
+ minSize
376
+ } = panel.current;
377
+ if (units === "pixels") {
378
+ minSize = minSize / groupSizePixels * 100;
379
+ if (maxSize != null) {
380
+ maxSize = maxSize / groupSizePixels * 100;
381
+ }
382
+ }
383
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
384
+ if (size !== sizes[index]) {
385
+ remainingSize -= size - sizes[index];
386
+ sizes[index] = size;
387
+
388
+ // Fuzzy comparison to account for imprecise floating point math
389
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ // Finally, if there is still left-over size, log an error
397
+ if (Math.abs(remainingSize).toFixed(3) !== "0.000") {
398
+ {
399
+ 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.`);
400
+ }
401
+ }
402
+ return sizes;
403
+ }
316
404
  function getBeforeAndAfterIds(id, panelsArray) {
317
405
  if (panelsArray.length < 2) {
318
406
  return [null, null];
@@ -326,6 +414,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
326
414
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
327
415
  return [idBefore, idAfter];
328
416
  }
417
+ function getAvailableGroupSizePixels(groupId) {
418
+ const panelGroupElement = getPanelGroup(groupId);
419
+ if (panelGroupElement == null) {
420
+ return NaN;
421
+ }
422
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
423
+ const resizeHandles = getResizeHandlesForGroup(groupId);
424
+ if (direction === "horizontal") {
425
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
426
+ return accumulated + handle.offsetWidth;
427
+ }, 0);
428
+ } else {
429
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
430
+ return accumulated + handle.offsetHeight;
431
+ }, 0);
432
+ }
433
+ }
329
434
 
330
435
  // This method returns a number between 1 and 100 representing
331
436
  // the % of the group's overall space this panel should occupy.
@@ -396,18 +501,24 @@ function panelsMapToSortedArray(panels) {
396
501
  }
397
502
  });
398
503
  }
399
- function safeResizePanel(panel, delta, prevSize, event) {
400
- const nextSizeUnsafe = prevSize + delta;
401
- const {
504
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
505
+ let {
402
506
  collapsedSize,
403
507
  collapsible,
404
508
  maxSize,
405
509
  minSize
406
510
  } = panel.current;
511
+ if (units === "pixels") {
512
+ collapsedSize = collapsedSize / groupSizePixels * 100;
513
+ if (maxSize != null) {
514
+ maxSize = maxSize / groupSizePixels * 100;
515
+ }
516
+ minSize = minSize / groupSizePixels * 100;
517
+ }
407
518
  if (collapsible) {
408
519
  if (prevSize > collapsedSize) {
409
520
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
410
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
521
+ if (nextSize <= minSize / 2 + collapsedSize) {
411
522
  return collapsedSize;
412
523
  }
413
524
  } else {
@@ -416,14 +527,119 @@ function safeResizePanel(panel, delta, prevSize, event) {
416
527
  // Keyboard events should expand a collapsed panel to the min size,
417
528
  // but mouse events should wait until the panel has reached its min size
418
529
  // to avoid a visual flickering when dragging between collapsed and min size.
419
- if (nextSizeUnsafe < minSize) {
530
+ if (nextSize < minSize) {
420
531
  return collapsedSize;
421
532
  }
422
533
  }
423
534
  }
424
535
  }
425
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
426
- return nextSize;
536
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
537
+ }
538
+ function validatePanelProps(units, panelData) {
539
+ const {
540
+ collapsible,
541
+ defaultSize,
542
+ maxSize,
543
+ minSize
544
+ } = panelData.current;
545
+
546
+ // Basic props validation
547
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
548
+ {
549
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
550
+ }
551
+ panelData.current.minSize = 0;
552
+ }
553
+ if (maxSize != null) {
554
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
555
+ {
556
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
557
+ }
558
+ panelData.current.maxSize = null;
559
+ }
560
+ }
561
+ if (defaultSize !== null) {
562
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
563
+ {
564
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
565
+ }
566
+ panelData.current.defaultSize = null;
567
+ } else if (defaultSize < minSize && !collapsible) {
568
+ {
569
+ console.error(`Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`);
570
+ }
571
+ panelData.current.defaultSize = minSize;
572
+ } else if (maxSize != null && defaultSize > maxSize) {
573
+ {
574
+ console.error(`Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`);
575
+ }
576
+ panelData.current.defaultSize = maxSize;
577
+ }
578
+ }
579
+ }
580
+ function validatePanelGroupLayout({
581
+ groupId,
582
+ panels,
583
+ nextSizes,
584
+ prevSizes,
585
+ units
586
+ }) {
587
+ // Clone because this method modifies
588
+ nextSizes = [...nextSizes];
589
+ const panelsArray = panelsMapToSortedArray(panels);
590
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
591
+ let remainingSize = 0;
592
+
593
+ // First, check all of the proposed sizes against the min/max constraints
594
+ for (let index = 0; index < panelsArray.length; index++) {
595
+ const panel = panelsArray[index];
596
+ const prevSize = prevSizes[index];
597
+ const nextSize = nextSizes[index];
598
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
599
+ if (nextSize != safeNextSize) {
600
+ remainingSize += nextSize - safeNextSize;
601
+ nextSizes[index] = safeNextSize;
602
+ {
603
+ console.error(`Invalid size (${nextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
604
+ }
605
+ }
606
+ }
607
+
608
+ // If there is additional, left over space, assign it to any panel(s) that permits it
609
+ // (It's not worth taking multiple additional passes to evenly distribute)
610
+ if (remainingSize.toFixed(3) !== "0.000") {
611
+ for (let index = 0; index < panelsArray.length; index++) {
612
+ const panel = panelsArray[index];
613
+ let {
614
+ maxSize,
615
+ minSize
616
+ } = panel.current;
617
+ if (units === "pixels") {
618
+ minSize = minSize / groupSizePixels * 100;
619
+ if (maxSize != null) {
620
+ maxSize = maxSize / groupSizePixels * 100;
621
+ }
622
+ }
623
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
624
+ if (size !== nextSizes[index]) {
625
+ remainingSize -= size - nextSizes[index];
626
+ nextSizes[index] = size;
627
+
628
+ // Fuzzy comparison to account for imprecise floating point math
629
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
630
+ break;
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
637
+ if (remainingSize.toFixed(3) !== "0.000") {
638
+ {
639
+ console.error(`"Invalid panel group configuration; default panel sizes should total 100% but was ${100 - remainingSize}%`);
640
+ }
641
+ }
642
+ return nextSizes;
427
643
  }
428
644
 
429
645
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -449,6 +665,7 @@ function useWindowSplitterPanelGroupBehavior({
449
665
  panels
450
666
  } = committedValuesRef.current;
451
667
  const groupElement = getPanelGroup(groupId);
668
+ assert(groupElement != null, `No group found for id "${groupId}"`);
452
669
  const {
453
670
  height,
454
671
  width
@@ -461,23 +678,28 @@ function useWindowSplitterPanelGroupBehavior({
461
678
  if (idBefore == null || idAfter == null) {
462
679
  return () => {};
463
680
  }
464
- let minSize = 0;
465
- let maxSize = 100;
681
+ let currentMinSize = 0;
682
+ let currentMaxSize = 100;
466
683
  let totalMinSize = 0;
467
684
  let totalMaxSize = 0;
468
685
 
469
686
  // A panel's effective min/max sizes also need to account for other panel's sizes.
470
687
  panelsArray.forEach(panelData => {
471
- if (panelData.current.id === idBefore) {
472
- maxSize = panelData.current.maxSize;
473
- minSize = panelData.current.minSize;
688
+ const {
689
+ id,
690
+ maxSize,
691
+ minSize
692
+ } = panelData.current;
693
+ if (id === idBefore) {
694
+ currentMinSize = minSize;
695
+ currentMaxSize = maxSize != null ? maxSize : 100;
474
696
  } else {
475
- totalMinSize += panelData.current.minSize;
476
- totalMaxSize += panelData.current.maxSize;
697
+ totalMinSize += minSize;
698
+ totalMaxSize += maxSize != null ? maxSize : 100;
477
699
  }
478
700
  });
479
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
480
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
701
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
702
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
481
703
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
482
704
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
483
705
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -501,7 +723,7 @@ function useWindowSplitterPanelGroupBehavior({
501
723
  } else {
502
724
  delta = -(direction === "horizontal" ? width : height);
503
725
  }
504
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
726
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
505
727
  if (sizes !== nextSizes) {
506
728
  setSizes(nextSizes);
507
729
  }
@@ -816,9 +1038,6 @@ const defaultStorage = {
816
1038
  // * dragHandleRect, sizes:
817
1039
  // When resizing is done via mouse/touch event– some initial state is stored
818
1040
  // so that any panels that contract will also expand if drag direction is reversed.
819
- // TODO
820
- // Within an active drag, remember original positions to refine more easily on expand.
821
- // Look at what the Chrome devtools Sources does.
822
1041
  function PanelGroupWithForwardedRef({
823
1042
  autoSaveId,
824
1043
  children = null,
@@ -830,7 +1049,8 @@ function PanelGroupWithForwardedRef({
830
1049
  onLayout,
831
1050
  storage = defaultStorage,
832
1051
  style: styleFromProps = {},
833
- tagName: Type = "div"
1052
+ tagName: Type = "div",
1053
+ units = "percentages"
834
1054
  }) {
835
1055
  const groupId = useUniqueId(idFromProps);
836
1056
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -843,6 +1063,7 @@ function PanelGroupWithForwardedRef({
843
1063
  const devWarningsRef = useRef({
844
1064
  didLogDefaultSizeWarning: false,
845
1065
  didLogIdAndOrderWarning: false,
1066
+ didLogInvalidLayoutWarning: false,
846
1067
  prevPanelIds: []
847
1068
  });
848
1069
 
@@ -865,32 +1086,58 @@ function PanelGroupWithForwardedRef({
865
1086
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
866
1087
  const committedValuesRef = useRef({
867
1088
  direction,
1089
+ id: groupId,
868
1090
  panels,
869
- sizes
1091
+ sizes,
1092
+ units
870
1093
  });
871
1094
  useImperativeHandle(forwardedRef, () => ({
872
- getLayout: () => {
1095
+ getId: () => groupId,
1096
+ getLayout: unitsFromParams => {
873
1097
  const {
874
- sizes
1098
+ sizes,
1099
+ units: unitsFromProps
875
1100
  } = committedValuesRef.current;
876
- return sizes;
1101
+ const units = unitsFromParams ?? unitsFromProps;
1102
+ if (units === "pixels") {
1103
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1104
+ return sizes.map(size => size / 100 * groupSizePixels);
1105
+ } else {
1106
+ return sizes;
1107
+ }
877
1108
  },
878
- setLayout: sizes => {
879
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
880
- assert(total === 100, "Panel sizes must add up to 100%");
1109
+ setLayout: (sizes, unitsFromParams) => {
881
1110
  const {
882
- panels
1111
+ id: groupId,
1112
+ panels,
1113
+ sizes: prevSizes,
1114
+ units
883
1115
  } = committedValuesRef.current;
1116
+ if ((unitsFromParams || units) === "pixels") {
1117
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1118
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1119
+ }
884
1120
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
885
1121
  const panelsArray = panelsMapToSortedArray(panels);
886
- setSizes(sizes);
887
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1122
+ const nextSizes = validatePanelGroupLayout({
1123
+ groupId,
1124
+ panels,
1125
+ nextSizes: sizes,
1126
+ prevSizes,
1127
+ units
1128
+ });
1129
+ if (!areEqual(prevSizes, nextSizes)) {
1130
+ setSizes(nextSizes);
1131
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1132
+ }
888
1133
  }
889
- }), []);
1134
+ }), [groupId]);
890
1135
  useIsomorphicLayoutEffect(() => {
891
1136
  committedValuesRef.current.direction = direction;
1137
+ committedValuesRef.current.id = groupId;
892
1138
  committedValuesRef.current.panels = panels;
893
1139
  committedValuesRef.current.sizes = sizes;
1140
+ committedValuesRef.current.units = units;
894
1141
  });
895
1142
  useWindowSplitterPanelGroupBehavior({
896
1143
  committedValuesRef,
@@ -932,7 +1179,11 @@ function PanelGroupWithForwardedRef({
932
1179
  // Compute the initial sizes based on default weights.
933
1180
  // This assumes that panels register during initial mount (no conditional rendering)!
934
1181
  useIsomorphicLayoutEffect(() => {
935
- const sizes = committedValuesRef.current.sizes;
1182
+ const {
1183
+ id: groupId,
1184
+ sizes,
1185
+ units
1186
+ } = committedValuesRef.current;
936
1187
  if (sizes.length === panels.size) {
937
1188
  // Only compute (or restore) default sizes once per panel configuration.
938
1189
  return;
@@ -946,39 +1197,23 @@ function PanelGroupWithForwardedRef({
946
1197
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
947
1198
  }
948
1199
  if (defaultSizes != null) {
949
- setSizes(defaultSizes);
1200
+ // Validate saved sizes in case something has changed since last render
1201
+ // e.g. for pixel groups, this could be the size of the window
1202
+ const validatedSizes = validatePanelGroupLayout({
1203
+ groupId,
1204
+ panels,
1205
+ nextSizes: defaultSizes,
1206
+ prevSizes: defaultSizes,
1207
+ units
1208
+ });
1209
+ setSizes(validatedSizes);
950
1210
  } else {
951
- const panelsArray = panelsMapToSortedArray(panels);
952
- let panelsWithNullDefaultSize = 0;
953
- let totalDefaultSize = 0;
954
- let totalMinSize = 0;
955
-
956
- // TODO
957
- // Implicit default size calculations below do not account for inferred min/max size values.
958
- // 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.
959
- // For now, these logic edge cases are left to the user to handle via props.
960
-
961
- panelsArray.forEach(panel => {
962
- totalMinSize += panel.current.minSize;
963
- if (panel.current.defaultSize === null) {
964
- panelsWithNullDefaultSize++;
965
- } else {
966
- totalDefaultSize += panel.current.defaultSize;
967
- }
1211
+ const sizes = calculateDefaultLayout({
1212
+ groupId,
1213
+ panels,
1214
+ units
968
1215
  });
969
- if (totalDefaultSize > 100) {
970
- throw new Error(`Default panel sizes cannot exceed 100%`);
971
- } else if (panelsArray.length > 1 && panelsWithNullDefaultSize === 0 && totalDefaultSize !== 100) {
972
- throw new Error(`Invalid default sizes specified for panels`);
973
- } else if (totalMinSize > 100) {
974
- throw new Error(`Minimum panel sizes cannot exceed 100%`);
975
- }
976
- setSizes(panelsArray.map(panel => {
977
- if (panel.current.defaultSize === null) {
978
- return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
979
- }
980
- return panel.current.defaultSize;
981
- }));
1216
+ setSizes(sizes);
982
1217
  }
983
1218
  }, [autoSaveId, panels, storage]);
984
1219
  useEffect(() => {
@@ -1016,6 +1251,48 @@ function PanelGroupWithForwardedRef({
1016
1251
  }
1017
1252
  }
1018
1253
  }, [autoSaveId, panels, sizes, storage]);
1254
+ useIsomorphicLayoutEffect(() => {
1255
+ // Pixel panel constraints need to be reassessed after a group resize
1256
+ // We can avoid the ResizeObserver overhead for relative layouts
1257
+ if (units === "pixels") {
1258
+ const resizeObserver = new ResizeObserver(() => {
1259
+ const {
1260
+ panels,
1261
+ sizes: prevSizes
1262
+ } = committedValuesRef.current;
1263
+ const nextSizes = validatePanelGroupLayout({
1264
+ groupId,
1265
+ panels,
1266
+ nextSizes: prevSizes,
1267
+ prevSizes,
1268
+ units
1269
+ });
1270
+ if (!areEqual(prevSizes, nextSizes)) {
1271
+ setSizes(nextSizes);
1272
+ }
1273
+ });
1274
+ resizeObserver.observe(getPanelGroup(groupId));
1275
+ return () => {
1276
+ resizeObserver.disconnect();
1277
+ };
1278
+ }
1279
+ }, [groupId, units]);
1280
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1281
+ const {
1282
+ panels,
1283
+ units: unitsFromProps
1284
+ } = committedValuesRef.current;
1285
+ const panelsArray = panelsMapToSortedArray(panels);
1286
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1287
+ const size = sizes[index];
1288
+ const units = unitsFromParams ?? unitsFromProps;
1289
+ if (units === "pixels") {
1290
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1291
+ return size / 100 * groupSizePixels;
1292
+ } else {
1293
+ return size;
1294
+ }
1295
+ }, [groupId, sizes]);
1019
1296
  const getPanelStyle = useCallback((id, defaultSize) => {
1020
1297
  const {
1021
1298
  panels
@@ -1054,6 +1331,10 @@ function PanelGroupWithForwardedRef({
1054
1331
  };
1055
1332
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1056
1333
  const registerPanel = useCallback((id, panelRef) => {
1334
+ const {
1335
+ units
1336
+ } = committedValuesRef.current;
1337
+ validatePanelProps(units, panelRef);
1057
1338
  setPanels(prevPanels => {
1058
1339
  if (prevPanels.has(id)) {
1059
1340
  return prevPanels;
@@ -1090,7 +1371,10 @@ function PanelGroupWithForwardedRef({
1090
1371
  }
1091
1372
  const size = isHorizontal ? rect.width : rect.height;
1092
1373
  const delta = movement / size * 100;
1093
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1374
+
1375
+ // If a validateLayout method has been provided
1376
+ // it's important to use it before updating the mouse cursor
1377
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1094
1378
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1095
1379
 
1096
1380
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1117,6 +1401,8 @@ function PanelGroupWithForwardedRef({
1117
1401
  }
1118
1402
  if (sizesChanged) {
1119
1403
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1404
+
1405
+ // It's okay to bypass in this case because we already validated above
1120
1406
  setSizes(nextSizes);
1121
1407
 
1122
1408
  // If resize change handlers have been declared, this is the time to call them.
@@ -1170,7 +1456,7 @@ function PanelGroupWithForwardedRef({
1170
1456
  }
1171
1457
  const isLastPanel = index === panelsArray.length - 1;
1172
1458
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1173
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1459
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1174
1460
  if (prevSizes !== nextSizes) {
1175
1461
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1176
1462
  setSizes(nextSizes);
@@ -1213,7 +1499,7 @@ function PanelGroupWithForwardedRef({
1213
1499
  }
1214
1500
  const isLastPanel = index === panelsArray.length - 1;
1215
1501
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1216
- 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);
1217
1503
  if (prevSizes !== nextSizes) {
1218
1504
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1219
1505
  setSizes(nextSizes);
@@ -1223,21 +1509,34 @@ function PanelGroupWithForwardedRef({
1223
1509
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1224
1510
  }
1225
1511
  }, []);
1226
- const resizePanel = useCallback((id, nextSize) => {
1512
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1227
1513
  const {
1514
+ id: groupId,
1228
1515
  panels,
1229
- sizes: prevSizes
1516
+ sizes: prevSizes,
1517
+ units
1230
1518
  } = committedValuesRef.current;
1519
+ if ((unitsFromParams || units) === "pixels") {
1520
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1521
+ nextSize = nextSize / groupSizePixels * 100;
1522
+ }
1231
1523
  const panel = panels.get(id);
1232
1524
  if (panel == null) {
1233
1525
  return;
1234
1526
  }
1235
- const {
1527
+ let {
1236
1528
  collapsedSize,
1237
1529
  collapsible,
1238
1530
  maxSize,
1239
1531
  minSize
1240
1532
  } = panel.current;
1533
+ if (units === "pixels") {
1534
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1535
+ minSize = minSize / groupSizePixels * 100;
1536
+ if (maxSize != null) {
1537
+ maxSize = maxSize / groupSizePixels * 100;
1538
+ }
1539
+ }
1241
1540
  const panelsArray = panelsMapToSortedArray(panels);
1242
1541
  const index = panelsArray.indexOf(panel);
1243
1542
  if (index < 0) {
@@ -1248,7 +1547,13 @@ function PanelGroupWithForwardedRef({
1248
1547
  return;
1249
1548
  }
1250
1549
  if (collapsible && nextSize === collapsedSize) ; else {
1251
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1550
+ const unsafeNextSize = nextSize;
1551
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1552
+ {
1553
+ if (unsafeNextSize !== nextSize) {
1554
+ console.error(`Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
1555
+ }
1556
+ }
1252
1557
  }
1253
1558
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1254
1559
  if (idBefore == null || idAfter == null) {
@@ -1256,7 +1561,7 @@ function PanelGroupWithForwardedRef({
1256
1561
  }
1257
1562
  const isLastPanel = index === panelsArray.length - 1;
1258
1563
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1259
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1564
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1260
1565
  if (prevSizes !== nextSizes) {
1261
1566
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1262
1567
  setSizes(nextSizes);
@@ -1271,6 +1576,7 @@ function PanelGroupWithForwardedRef({
1271
1576
  collapsePanel,
1272
1577
  direction,
1273
1578
  expandPanel,
1579
+ getPanelSize,
1274
1580
  getPanelStyle,
1275
1581
  groupId,
1276
1582
  registerPanel,
@@ -1292,8 +1598,9 @@ function PanelGroupWithForwardedRef({
1292
1598
  setActiveHandleId(null);
1293
1599
  initialDragStateRef.current = null;
1294
1600
  },
1601
+ units,
1295
1602
  unregisterPanel
1296
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1603
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1297
1604
  const style = {
1298
1605
  display: "flex",
1299
1606
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1308,6 +1615,7 @@ function PanelGroupWithForwardedRef({
1308
1615
  "data-panel-group": "",
1309
1616
  "data-panel-group-direction": direction,
1310
1617
  "data-panel-group-id": groupId,
1618
+ "data-panel-group-units": units,
1311
1619
  style: {
1312
1620
  ...style,
1313
1621
  ...styleFromProps
@@ -1460,3 +1768,4 @@ PanelResizeHandle.displayName = "PanelResizeHandle";
1460
1768
  exports.Panel = Panel;
1461
1769
  exports.PanelGroup = PanelGroup;
1462
1770
  exports.PanelResizeHandle = PanelResizeHandle;
1771
+ exports.getAvailableGroupSizePixels = getAvailableGroupSizePixels;