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;
162
157
  },
163
- resize: percentage => resizePanel(panelId, percentage)
164
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
158
+ getSize(units) {
159
+ return getPanelSize(panelId, units);
160
+ },
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,89 @@ 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
+ return sizes;
399
+ }
316
400
  function getBeforeAndAfterIds(id, panelsArray) {
317
401
  if (panelsArray.length < 2) {
318
402
  return [null, null];
@@ -326,6 +410,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
326
410
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
327
411
  return [idBefore, idAfter];
328
412
  }
413
+ function getAvailableGroupSizePixels(groupId) {
414
+ const panelGroupElement = getPanelGroup(groupId);
415
+ if (panelGroupElement == null) {
416
+ return NaN;
417
+ }
418
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
419
+ const resizeHandles = getResizeHandlesForGroup(groupId);
420
+ if (direction === "horizontal") {
421
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
422
+ return accumulated + handle.offsetWidth;
423
+ }, 0);
424
+ } else {
425
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
426
+ return accumulated + handle.offsetHeight;
427
+ }, 0);
428
+ }
429
+ }
329
430
 
330
431
  // This method returns a number between 1 and 100 representing
331
432
  // the % of the group's overall space this panel should occupy.
@@ -396,18 +497,24 @@ function panelsMapToSortedArray(panels) {
396
497
  }
397
498
  });
398
499
  }
399
- function safeResizePanel(panel, delta, prevSize, event) {
400
- const nextSizeUnsafe = prevSize + delta;
401
- const {
500
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
501
+ let {
402
502
  collapsedSize,
403
503
  collapsible,
404
504
  maxSize,
405
505
  minSize
406
506
  } = panel.current;
507
+ if (units === "pixels") {
508
+ collapsedSize = collapsedSize / groupSizePixels * 100;
509
+ if (maxSize != null) {
510
+ maxSize = maxSize / groupSizePixels * 100;
511
+ }
512
+ minSize = minSize / groupSizePixels * 100;
513
+ }
407
514
  if (collapsible) {
408
515
  if (prevSize > collapsedSize) {
409
516
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
410
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
517
+ if (nextSize <= minSize / 2 + collapsedSize) {
411
518
  return collapsedSize;
412
519
  }
413
520
  } else {
@@ -416,14 +523,97 @@ function safeResizePanel(panel, delta, prevSize, event) {
416
523
  // Keyboard events should expand a collapsed panel to the min size,
417
524
  // but mouse events should wait until the panel has reached its min size
418
525
  // to avoid a visual flickering when dragging between collapsed and min size.
419
- if (nextSizeUnsafe < minSize) {
526
+ if (nextSize < minSize) {
420
527
  return collapsedSize;
421
528
  }
422
529
  }
423
530
  }
424
531
  }
425
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
426
- return nextSize;
532
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
533
+ }
534
+ function validatePanelProps(units, panelData) {
535
+ const {
536
+ collapsible,
537
+ defaultSize,
538
+ maxSize,
539
+ minSize
540
+ } = panelData.current;
541
+
542
+ // Basic props validation
543
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
544
+ panelData.current.minSize = 0;
545
+ }
546
+ if (maxSize != null) {
547
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
548
+ panelData.current.maxSize = null;
549
+ }
550
+ }
551
+ if (defaultSize !== null) {
552
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
553
+ panelData.current.defaultSize = null;
554
+ } else if (defaultSize < minSize && !collapsible) {
555
+ panelData.current.defaultSize = minSize;
556
+ } else if (maxSize != null && defaultSize > maxSize) {
557
+ panelData.current.defaultSize = maxSize;
558
+ }
559
+ }
560
+ }
561
+ function validatePanelGroupLayout({
562
+ groupId,
563
+ panels,
564
+ nextSizes,
565
+ prevSizes,
566
+ units
567
+ }) {
568
+ // Clone because this method modifies
569
+ nextSizes = [...nextSizes];
570
+ const panelsArray = panelsMapToSortedArray(panels);
571
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
572
+ let remainingSize = 0;
573
+
574
+ // First, check all of the proposed sizes against the min/max constraints
575
+ for (let index = 0; index < panelsArray.length; index++) {
576
+ const panel = panelsArray[index];
577
+ const prevSize = prevSizes[index];
578
+ const nextSize = nextSizes[index];
579
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
580
+ if (nextSize != safeNextSize) {
581
+ remainingSize += nextSize - safeNextSize;
582
+ nextSizes[index] = safeNextSize;
583
+ }
584
+ }
585
+
586
+ // If there is additional, left over space, assign it to any panel(s) that permits it
587
+ // (It's not worth taking multiple additional passes to evenly distribute)
588
+ if (remainingSize.toFixed(3) !== "0.000") {
589
+ for (let index = 0; index < panelsArray.length; index++) {
590
+ const panel = panelsArray[index];
591
+ let {
592
+ maxSize,
593
+ minSize
594
+ } = panel.current;
595
+ if (units === "pixels") {
596
+ minSize = minSize / groupSizePixels * 100;
597
+ if (maxSize != null) {
598
+ maxSize = maxSize / groupSizePixels * 100;
599
+ }
600
+ }
601
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
602
+ if (size !== nextSizes[index]) {
603
+ remainingSize -= size - nextSizes[index];
604
+ nextSizes[index] = size;
605
+
606
+ // Fuzzy comparison to account for imprecise floating point math
607
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
608
+ break;
609
+ }
610
+ }
611
+ }
612
+ }
613
+
614
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
615
+ if (remainingSize.toFixed(3) !== "0.000") ;
616
+ return nextSizes;
427
617
  }
428
618
 
429
619
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -449,6 +639,7 @@ function useWindowSplitterPanelGroupBehavior({
449
639
  panels
450
640
  } = committedValuesRef.current;
451
641
  const groupElement = getPanelGroup(groupId);
642
+ assert(groupElement != null, `No group found for id "${groupId}"`);
452
643
  const {
453
644
  height,
454
645
  width
@@ -461,23 +652,28 @@ function useWindowSplitterPanelGroupBehavior({
461
652
  if (idBefore == null || idAfter == null) {
462
653
  return () => {};
463
654
  }
464
- let minSize = 0;
465
- let maxSize = 100;
655
+ let currentMinSize = 0;
656
+ let currentMaxSize = 100;
466
657
  let totalMinSize = 0;
467
658
  let totalMaxSize = 0;
468
659
 
469
660
  // A panel's effective min/max sizes also need to account for other panel's sizes.
470
661
  panelsArray.forEach(panelData => {
471
- if (panelData.current.id === idBefore) {
472
- maxSize = panelData.current.maxSize;
473
- minSize = panelData.current.minSize;
662
+ const {
663
+ id,
664
+ maxSize,
665
+ minSize
666
+ } = panelData.current;
667
+ if (id === idBefore) {
668
+ currentMinSize = minSize;
669
+ currentMaxSize = maxSize != null ? maxSize : 100;
474
670
  } else {
475
- totalMinSize += panelData.current.minSize;
476
- totalMaxSize += panelData.current.maxSize;
671
+ totalMinSize += minSize;
672
+ totalMaxSize += maxSize != null ? maxSize : 100;
477
673
  }
478
674
  });
479
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
480
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
675
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
676
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
481
677
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
482
678
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
483
679
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -501,7 +697,7 @@ function useWindowSplitterPanelGroupBehavior({
501
697
  } else {
502
698
  delta = -(direction === "horizontal" ? width : height);
503
699
  }
504
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
700
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
505
701
  if (sizes !== nextSizes) {
506
702
  setSizes(nextSizes);
507
703
  }
@@ -816,9 +1012,6 @@ const defaultStorage = {
816
1012
  // * dragHandleRect, sizes:
817
1013
  // When resizing is done via mouse/touch event– some initial state is stored
818
1014
  // 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
1015
  function PanelGroupWithForwardedRef({
823
1016
  autoSaveId,
824
1017
  children = null,
@@ -830,7 +1023,8 @@ function PanelGroupWithForwardedRef({
830
1023
  onLayout,
831
1024
  storage = defaultStorage,
832
1025
  style: styleFromProps = {},
833
- tagName: Type = "div"
1026
+ tagName: Type = "div",
1027
+ units = "percentages"
834
1028
  }) {
835
1029
  const groupId = useUniqueId(idFromProps);
836
1030
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -843,6 +1037,7 @@ function PanelGroupWithForwardedRef({
843
1037
  useRef({
844
1038
  didLogDefaultSizeWarning: false,
845
1039
  didLogIdAndOrderWarning: false,
1040
+ didLogInvalidLayoutWarning: false,
846
1041
  prevPanelIds: []
847
1042
  });
848
1043
 
@@ -865,32 +1060,58 @@ function PanelGroupWithForwardedRef({
865
1060
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
866
1061
  const committedValuesRef = useRef({
867
1062
  direction,
1063
+ id: groupId,
868
1064
  panels,
869
- sizes
1065
+ sizes,
1066
+ units
870
1067
  });
871
1068
  useImperativeHandle(forwardedRef, () => ({
872
- getLayout: () => {
1069
+ getId: () => groupId,
1070
+ getLayout: unitsFromParams => {
873
1071
  const {
874
- sizes
1072
+ sizes,
1073
+ units: unitsFromProps
875
1074
  } = committedValuesRef.current;
876
- return sizes;
1075
+ const units = unitsFromParams ?? unitsFromProps;
1076
+ if (units === "pixels") {
1077
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1078
+ return sizes.map(size => size / 100 * groupSizePixels);
1079
+ } else {
1080
+ return sizes;
1081
+ }
877
1082
  },
878
- setLayout: sizes => {
879
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
880
- assert(total === 100, "Panel sizes must add up to 100%");
1083
+ setLayout: (sizes, unitsFromParams) => {
881
1084
  const {
882
- panels
1085
+ id: groupId,
1086
+ panels,
1087
+ sizes: prevSizes,
1088
+ units
883
1089
  } = committedValuesRef.current;
1090
+ if ((unitsFromParams || units) === "pixels") {
1091
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1092
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1093
+ }
884
1094
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
885
1095
  const panelsArray = panelsMapToSortedArray(panels);
886
- setSizes(sizes);
887
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1096
+ const nextSizes = validatePanelGroupLayout({
1097
+ groupId,
1098
+ panels,
1099
+ nextSizes: sizes,
1100
+ prevSizes,
1101
+ units
1102
+ });
1103
+ if (!areEqual(prevSizes, nextSizes)) {
1104
+ setSizes(nextSizes);
1105
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1106
+ }
888
1107
  }
889
- }), []);
1108
+ }), [groupId]);
890
1109
  useIsomorphicLayoutEffect(() => {
891
1110
  committedValuesRef.current.direction = direction;
1111
+ committedValuesRef.current.id = groupId;
892
1112
  committedValuesRef.current.panels = panels;
893
1113
  committedValuesRef.current.sizes = sizes;
1114
+ committedValuesRef.current.units = units;
894
1115
  });
895
1116
  useWindowSplitterPanelGroupBehavior({
896
1117
  committedValuesRef,
@@ -932,7 +1153,11 @@ function PanelGroupWithForwardedRef({
932
1153
  // Compute the initial sizes based on default weights.
933
1154
  // This assumes that panels register during initial mount (no conditional rendering)!
934
1155
  useIsomorphicLayoutEffect(() => {
935
- const sizes = committedValuesRef.current.sizes;
1156
+ const {
1157
+ id: groupId,
1158
+ sizes,
1159
+ units
1160
+ } = committedValuesRef.current;
936
1161
  if (sizes.length === panels.size) {
937
1162
  // Only compute (or restore) default sizes once per panel configuration.
938
1163
  return;
@@ -946,39 +1171,23 @@ function PanelGroupWithForwardedRef({
946
1171
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
947
1172
  }
948
1173
  if (defaultSizes != null) {
949
- setSizes(defaultSizes);
1174
+ // Validate saved sizes in case something has changed since last render
1175
+ // e.g. for pixel groups, this could be the size of the window
1176
+ const validatedSizes = validatePanelGroupLayout({
1177
+ groupId,
1178
+ panels,
1179
+ nextSizes: defaultSizes,
1180
+ prevSizes: defaultSizes,
1181
+ units
1182
+ });
1183
+ setSizes(validatedSizes);
950
1184
  } 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
- }
1185
+ const sizes = calculateDefaultLayout({
1186
+ groupId,
1187
+ panels,
1188
+ units
968
1189
  });
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
- }));
1190
+ setSizes(sizes);
982
1191
  }
983
1192
  }, [autoSaveId, panels, storage]);
984
1193
  useEffect(() => {
@@ -996,6 +1205,48 @@ function PanelGroupWithForwardedRef({
996
1205
  debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
997
1206
  }
998
1207
  }, [autoSaveId, panels, sizes, storage]);
1208
+ useIsomorphicLayoutEffect(() => {
1209
+ // Pixel panel constraints need to be reassessed after a group resize
1210
+ // We can avoid the ResizeObserver overhead for relative layouts
1211
+ if (units === "pixels") {
1212
+ const resizeObserver = new ResizeObserver(() => {
1213
+ const {
1214
+ panels,
1215
+ sizes: prevSizes
1216
+ } = committedValuesRef.current;
1217
+ const nextSizes = validatePanelGroupLayout({
1218
+ groupId,
1219
+ panels,
1220
+ nextSizes: prevSizes,
1221
+ prevSizes,
1222
+ units
1223
+ });
1224
+ if (!areEqual(prevSizes, nextSizes)) {
1225
+ setSizes(nextSizes);
1226
+ }
1227
+ });
1228
+ resizeObserver.observe(getPanelGroup(groupId));
1229
+ return () => {
1230
+ resizeObserver.disconnect();
1231
+ };
1232
+ }
1233
+ }, [groupId, units]);
1234
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1235
+ const {
1236
+ panels,
1237
+ units: unitsFromProps
1238
+ } = committedValuesRef.current;
1239
+ const panelsArray = panelsMapToSortedArray(panels);
1240
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1241
+ const size = sizes[index];
1242
+ const units = unitsFromParams ?? unitsFromProps;
1243
+ if (units === "pixels") {
1244
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1245
+ return size / 100 * groupSizePixels;
1246
+ } else {
1247
+ return size;
1248
+ }
1249
+ }, [groupId, sizes]);
999
1250
  const getPanelStyle = useCallback((id, defaultSize) => {
1000
1251
  const {
1001
1252
  panels
@@ -1026,6 +1277,10 @@ function PanelGroupWithForwardedRef({
1026
1277
  };
1027
1278
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1028
1279
  const registerPanel = useCallback((id, panelRef) => {
1280
+ const {
1281
+ units
1282
+ } = committedValuesRef.current;
1283
+ validatePanelProps(units, panelRef);
1029
1284
  setPanels(prevPanels => {
1030
1285
  if (prevPanels.has(id)) {
1031
1286
  return prevPanels;
@@ -1062,7 +1317,10 @@ function PanelGroupWithForwardedRef({
1062
1317
  }
1063
1318
  const size = isHorizontal ? rect.width : rect.height;
1064
1319
  const delta = movement / size * 100;
1065
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1320
+
1321
+ // If a validateLayout method has been provided
1322
+ // it's important to use it before updating the mouse cursor
1323
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1066
1324
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1067
1325
 
1068
1326
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1089,6 +1347,8 @@ function PanelGroupWithForwardedRef({
1089
1347
  }
1090
1348
  if (sizesChanged) {
1091
1349
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1350
+
1351
+ // It's okay to bypass in this case because we already validated above
1092
1352
  setSizes(nextSizes);
1093
1353
 
1094
1354
  // If resize change handlers have been declared, this is the time to call them.
@@ -1142,7 +1402,7 @@ function PanelGroupWithForwardedRef({
1142
1402
  }
1143
1403
  const isLastPanel = index === panelsArray.length - 1;
1144
1404
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1145
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1405
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1146
1406
  if (prevSizes !== nextSizes) {
1147
1407
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1148
1408
  setSizes(nextSizes);
@@ -1185,7 +1445,7 @@ function PanelGroupWithForwardedRef({
1185
1445
  }
1186
1446
  const isLastPanel = index === panelsArray.length - 1;
1187
1447
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1188
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1448
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1189
1449
  if (prevSizes !== nextSizes) {
1190
1450
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1191
1451
  setSizes(nextSizes);
@@ -1195,21 +1455,34 @@ function PanelGroupWithForwardedRef({
1195
1455
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1196
1456
  }
1197
1457
  }, []);
1198
- const resizePanel = useCallback((id, nextSize) => {
1458
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1199
1459
  const {
1460
+ id: groupId,
1200
1461
  panels,
1201
- sizes: prevSizes
1462
+ sizes: prevSizes,
1463
+ units
1202
1464
  } = committedValuesRef.current;
1465
+ if ((unitsFromParams || units) === "pixels") {
1466
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1467
+ nextSize = nextSize / groupSizePixels * 100;
1468
+ }
1203
1469
  const panel = panels.get(id);
1204
1470
  if (panel == null) {
1205
1471
  return;
1206
1472
  }
1207
- const {
1473
+ let {
1208
1474
  collapsedSize,
1209
1475
  collapsible,
1210
1476
  maxSize,
1211
1477
  minSize
1212
1478
  } = panel.current;
1479
+ if (units === "pixels") {
1480
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1481
+ minSize = minSize / groupSizePixels * 100;
1482
+ if (maxSize != null) {
1483
+ maxSize = maxSize / groupSizePixels * 100;
1484
+ }
1485
+ }
1213
1486
  const panelsArray = panelsMapToSortedArray(panels);
1214
1487
  const index = panelsArray.indexOf(panel);
1215
1488
  if (index < 0) {
@@ -1220,7 +1493,7 @@ function PanelGroupWithForwardedRef({
1220
1493
  return;
1221
1494
  }
1222
1495
  if (collapsible && nextSize === collapsedSize) ; else {
1223
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1496
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1224
1497
  }
1225
1498
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1226
1499
  if (idBefore == null || idAfter == null) {
@@ -1228,7 +1501,7 @@ function PanelGroupWithForwardedRef({
1228
1501
  }
1229
1502
  const isLastPanel = index === panelsArray.length - 1;
1230
1503
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1231
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1504
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1232
1505
  if (prevSizes !== nextSizes) {
1233
1506
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1234
1507
  setSizes(nextSizes);
@@ -1243,6 +1516,7 @@ function PanelGroupWithForwardedRef({
1243
1516
  collapsePanel,
1244
1517
  direction,
1245
1518
  expandPanel,
1519
+ getPanelSize,
1246
1520
  getPanelStyle,
1247
1521
  groupId,
1248
1522
  registerPanel,
@@ -1264,8 +1538,9 @@ function PanelGroupWithForwardedRef({
1264
1538
  setActiveHandleId(null);
1265
1539
  initialDragStateRef.current = null;
1266
1540
  },
1541
+ units,
1267
1542
  unregisterPanel
1268
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1543
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1269
1544
  const style = {
1270
1545
  display: "flex",
1271
1546
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1280,6 +1555,7 @@ function PanelGroupWithForwardedRef({
1280
1555
  "data-panel-group": "",
1281
1556
  "data-panel-group-direction": direction,
1282
1557
  "data-panel-group-id": groupId,
1558
+ "data-panel-group-units": units,
1283
1559
  style: {
1284
1560
  ...style,
1285
1561
  ...styleFromProps
@@ -1432,3 +1708,4 @@ PanelResizeHandle.displayName = "PanelResizeHandle";
1432
1708
  exports.Panel = Panel;
1433
1709
  exports.PanelGroup = PanelGroup;
1434
1710
  exports.PanelResizeHandle = PanelResizeHandle;
1711
+ exports.getAvailableGroupSizePixels = getAvailableGroupSizePixels;