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
@@ -45,8 +45,8 @@ function PanelWithForwardedRef({
45
45
  defaultSize = null,
46
46
  forwardedRef,
47
47
  id: idFromProps = null,
48
- maxSize = 100,
49
- minSize = 10,
48
+ maxSize = null,
49
+ minSize,
50
50
  onCollapse = null,
51
51
  onResize = null,
52
52
  order = null,
@@ -61,11 +61,22 @@ function PanelWithForwardedRef({
61
61
  const {
62
62
  collapsePanel,
63
63
  expandPanel,
64
+ getPanelSize,
64
65
  getPanelStyle,
65
66
  registerPanel,
66
67
  resizePanel,
68
+ units,
67
69
  unregisterPanel
68
70
  } = context;
71
+ if (minSize == null) {
72
+ if (units === "percentages") {
73
+ // Mimics legacy default value for percentage based panel groups
74
+ minSize = 10;
75
+ } else {
76
+ // There is no meaningful minimum pixel default we can provide
77
+ minSize = 0;
78
+ }
79
+ }
69
80
 
70
81
  // Use a ref to guard against users passing inline props
71
82
  const callbacksRef = useRef({
@@ -76,22 +87,6 @@ function PanelWithForwardedRef({
76
87
  callbacksRef.current.onCollapse = onCollapse;
77
88
  callbacksRef.current.onResize = onResize;
78
89
  });
79
-
80
- // Basic props validation
81
- if (minSize < 0 || minSize > 100) {
82
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
83
- } else if (maxSize < 0 || maxSize > 100) {
84
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
85
- } else {
86
- if (defaultSize !== null) {
87
- if (defaultSize < 0 || defaultSize > 100) {
88
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
89
- } else if (minSize > defaultSize && !collapsible) {
90
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
91
- defaultSize = minSize;
92
- }
93
- }
94
- }
95
90
  const style = getPanelStyle(panelId, defaultSize);
96
91
  const committedValuesRef = useRef({
97
92
  size: parseSizeFromStyle(style)
@@ -131,11 +126,14 @@ function PanelWithForwardedRef({
131
126
  getCollapsed() {
132
127
  return committedValuesRef.current.size === 0;
133
128
  },
134
- getSize() {
135
- return committedValuesRef.current.size;
129
+ getId() {
130
+ return panelId;
131
+ },
132
+ getSize(units) {
133
+ return getPanelSize(panelId, units);
136
134
  },
137
- resize: percentage => resizePanel(panelId, percentage)
138
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
135
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
136
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
139
137
  return createElement(Type, {
140
138
  children,
141
139
  className: classNameFromProps,
@@ -171,7 +169,13 @@ function parseSizeFromStyle(style) {
171
169
 
172
170
  const PRECISION = 10;
173
171
 
174
- function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse, initialDragState) {
172
+ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
173
+ const {
174
+ id: groupId,
175
+ panels,
176
+ units
177
+ } = committedValues;
178
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
175
179
  const {
176
180
  sizes: initialSizes
177
181
  } = initialDragState || {};
@@ -179,9 +183,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
179
183
  // If we're resizing by mouse or touch, use the initial sizes as a base.
180
184
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
181
185
  const baseSizes = initialSizes || prevSizes;
182
- if (delta === 0) {
183
- return baseSizes;
184
- }
185
186
  const panelsArray = panelsMapToSortedArray(panels);
186
187
  const nextSizes = baseSizes.concat();
187
188
  let deltaApplied = 0;
@@ -196,11 +197,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
196
197
 
197
198
  // Max-bounds check the panel being expanded first.
198
199
  {
199
- const pivotId = delta < 0 ? idAfter : idBefore;
200
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
200
201
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
201
202
  const panel = panelsArray[index];
202
203
  const baseSize = baseSizes[index];
203
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
204
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
204
205
  if (baseSize === nextSize) {
205
206
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
206
207
  return baseSizes;
@@ -208,29 +209,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
208
209
  if (nextSize === 0 && baseSize > 0) {
209
210
  panelSizeBeforeCollapse.set(pivotId, baseSize);
210
211
  }
211
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
212
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
212
213
  }
213
214
  }
214
- let pivotId = delta < 0 ? idBefore : idAfter;
215
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
215
216
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
216
217
  while (true) {
217
218
  const panel = panelsArray[index];
218
219
  const baseSize = baseSizes[index];
219
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
220
- const nextSize = safeResizePanel(panel, 0 - deltaRemaining, baseSize, event);
220
+ const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
221
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
221
222
  if (baseSize !== nextSize) {
222
223
  if (nextSize === 0 && baseSize > 0) {
223
224
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
224
225
  }
225
226
  deltaApplied += baseSize - nextSize;
226
227
  nextSizes[index] = nextSize;
227
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
228
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
228
229
  numeric: true
229
230
  }) >= 0) {
230
231
  break;
231
232
  }
232
233
  }
233
- if (delta < 0) {
234
+ if (deltaPixels < 0) {
234
235
  if (--index < 0) {
235
236
  break;
236
237
  }
@@ -248,7 +249,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
248
249
  }
249
250
 
250
251
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
251
- pivotId = delta < 0 ? idAfter : idBefore;
252
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
252
253
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
253
254
  nextSizes[index] = baseSizes[index] + deltaApplied;
254
255
  return nextSizes;
@@ -287,6 +288,93 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
287
288
  }
288
289
  });
289
290
  }
291
+ function calculateDefaultLayout({
292
+ groupId,
293
+ panels,
294
+ units
295
+ }) {
296
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
297
+ const panelsArray = panelsMapToSortedArray(panels);
298
+ const sizes = Array(panelsArray.length);
299
+ let numPanelsWithSizes = 0;
300
+ let remainingSize = 100;
301
+
302
+ // Assigning default sizes requires a couple of passes:
303
+ // First, all panels with defaultSize should be set as-is
304
+ for (let index = 0; index < panelsArray.length; index++) {
305
+ const panel = panelsArray[index];
306
+ const {
307
+ defaultSize
308
+ } = panel.current;
309
+ if (defaultSize != null) {
310
+ numPanelsWithSizes++;
311
+ sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
312
+ remainingSize -= sizes[index];
313
+ }
314
+ }
315
+
316
+ // Remaining total size should be distributed evenly between panels
317
+ // This may require two passes, depending on min/max constraints
318
+ for (let index = 0; index < panelsArray.length; index++) {
319
+ const panel = panelsArray[index];
320
+ let {
321
+ defaultSize,
322
+ id,
323
+ maxSize,
324
+ minSize
325
+ } = panel.current;
326
+ if (defaultSize != null) {
327
+ continue;
328
+ }
329
+ if (units === "pixels") {
330
+ minSize = minSize / groupSizePixels * 100;
331
+ if (maxSize != null) {
332
+ maxSize = maxSize / groupSizePixels * 100;
333
+ }
334
+ }
335
+ const remainingPanels = panelsArray.length - numPanelsWithSizes;
336
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
337
+ sizes[index] = size;
338
+ numPanelsWithSizes++;
339
+ remainingSize -= size;
340
+ }
341
+
342
+ // If there is additional, left over space, assign it to any panel(s) that permits it
343
+ // (It's not worth taking multiple additional passes to evenly distribute)
344
+ if (remainingSize !== 0) {
345
+ for (let index = 0; index < panelsArray.length; index++) {
346
+ const panel = panelsArray[index];
347
+ let {
348
+ maxSize,
349
+ minSize
350
+ } = panel.current;
351
+ if (units === "pixels") {
352
+ minSize = minSize / groupSizePixels * 100;
353
+ if (maxSize != null) {
354
+ maxSize = maxSize / groupSizePixels * 100;
355
+ }
356
+ }
357
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
358
+ if (size !== sizes[index]) {
359
+ remainingSize -= size - sizes[index];
360
+ sizes[index] = size;
361
+
362
+ // Fuzzy comparison to account for imprecise floating point math
363
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
364
+ break;
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ // Finally, if there is still left-over size, log an error
371
+ if (Math.abs(remainingSize).toFixed(3) !== "0.000") {
372
+ {
373
+ 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.`);
374
+ }
375
+ }
376
+ return sizes;
377
+ }
290
378
  function getBeforeAndAfterIds(id, panelsArray) {
291
379
  if (panelsArray.length < 2) {
292
380
  return [null, null];
@@ -300,6 +388,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
300
388
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
301
389
  return [idBefore, idAfter];
302
390
  }
391
+ function getAvailableGroupSizePixels(groupId) {
392
+ const panelGroupElement = getPanelGroup(groupId);
393
+ if (panelGroupElement == null) {
394
+ return NaN;
395
+ }
396
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
397
+ const resizeHandles = getResizeHandlesForGroup(groupId);
398
+ if (direction === "horizontal") {
399
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
400
+ return accumulated + handle.offsetWidth;
401
+ }, 0);
402
+ } else {
403
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
404
+ return accumulated + handle.offsetHeight;
405
+ }, 0);
406
+ }
407
+ }
303
408
 
304
409
  // This method returns a number between 1 and 100 representing
305
410
  // the % of the group's overall space this panel should occupy.
@@ -370,18 +475,24 @@ function panelsMapToSortedArray(panels) {
370
475
  }
371
476
  });
372
477
  }
373
- function safeResizePanel(panel, delta, prevSize, event) {
374
- const nextSizeUnsafe = prevSize + delta;
375
- const {
478
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
479
+ let {
376
480
  collapsedSize,
377
481
  collapsible,
378
482
  maxSize,
379
483
  minSize
380
484
  } = panel.current;
485
+ if (units === "pixels") {
486
+ collapsedSize = collapsedSize / groupSizePixels * 100;
487
+ if (maxSize != null) {
488
+ maxSize = maxSize / groupSizePixels * 100;
489
+ }
490
+ minSize = minSize / groupSizePixels * 100;
491
+ }
381
492
  if (collapsible) {
382
493
  if (prevSize > collapsedSize) {
383
494
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
384
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
495
+ if (nextSize <= minSize / 2 + collapsedSize) {
385
496
  return collapsedSize;
386
497
  }
387
498
  } else {
@@ -390,14 +501,119 @@ function safeResizePanel(panel, delta, prevSize, event) {
390
501
  // Keyboard events should expand a collapsed panel to the min size,
391
502
  // but mouse events should wait until the panel has reached its min size
392
503
  // to avoid a visual flickering when dragging between collapsed and min size.
393
- if (nextSizeUnsafe < minSize) {
504
+ if (nextSize < minSize) {
394
505
  return collapsedSize;
395
506
  }
396
507
  }
397
508
  }
398
509
  }
399
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
400
- return nextSize;
510
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
511
+ }
512
+ function validatePanelProps(units, panelData) {
513
+ const {
514
+ collapsible,
515
+ defaultSize,
516
+ maxSize,
517
+ minSize
518
+ } = panelData.current;
519
+
520
+ // Basic props validation
521
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
522
+ {
523
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
524
+ }
525
+ panelData.current.minSize = 0;
526
+ }
527
+ if (maxSize != null) {
528
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
529
+ {
530
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
531
+ }
532
+ panelData.current.maxSize = null;
533
+ }
534
+ }
535
+ if (defaultSize !== null) {
536
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
537
+ {
538
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
539
+ }
540
+ panelData.current.defaultSize = null;
541
+ } else if (defaultSize < minSize && !collapsible) {
542
+ {
543
+ console.error(`Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`);
544
+ }
545
+ panelData.current.defaultSize = minSize;
546
+ } else if (maxSize != null && defaultSize > maxSize) {
547
+ {
548
+ console.error(`Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`);
549
+ }
550
+ panelData.current.defaultSize = maxSize;
551
+ }
552
+ }
553
+ }
554
+ function validatePanelGroupLayout({
555
+ groupId,
556
+ panels,
557
+ nextSizes,
558
+ prevSizes,
559
+ units
560
+ }) {
561
+ // Clone because this method modifies
562
+ nextSizes = [...nextSizes];
563
+ const panelsArray = panelsMapToSortedArray(panels);
564
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
565
+ let remainingSize = 0;
566
+
567
+ // First, check all of the proposed sizes against the min/max constraints
568
+ for (let index = 0; index < panelsArray.length; index++) {
569
+ const panel = panelsArray[index];
570
+ const prevSize = prevSizes[index];
571
+ const nextSize = nextSizes[index];
572
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
573
+ if (nextSize != safeNextSize) {
574
+ remainingSize += nextSize - safeNextSize;
575
+ nextSizes[index] = safeNextSize;
576
+ {
577
+ console.error(`Invalid size (${nextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
578
+ }
579
+ }
580
+ }
581
+
582
+ // If there is additional, left over space, assign it to any panel(s) that permits it
583
+ // (It's not worth taking multiple additional passes to evenly distribute)
584
+ if (remainingSize.toFixed(3) !== "0.000") {
585
+ for (let index = 0; index < panelsArray.length; index++) {
586
+ const panel = panelsArray[index];
587
+ let {
588
+ maxSize,
589
+ minSize
590
+ } = panel.current;
591
+ if (units === "pixels") {
592
+ minSize = minSize / groupSizePixels * 100;
593
+ if (maxSize != null) {
594
+ maxSize = maxSize / groupSizePixels * 100;
595
+ }
596
+ }
597
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
598
+ if (size !== nextSizes[index]) {
599
+ remainingSize -= size - nextSizes[index];
600
+ nextSizes[index] = size;
601
+
602
+ // Fuzzy comparison to account for imprecise floating point math
603
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
604
+ break;
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
611
+ if (remainingSize.toFixed(3) !== "0.000") {
612
+ {
613
+ console.error(`"Invalid panel group configuration; default panel sizes should total 100% but was ${100 - remainingSize}%`);
614
+ }
615
+ }
616
+ return nextSizes;
401
617
  }
402
618
 
403
619
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -423,6 +639,7 @@ function useWindowSplitterPanelGroupBehavior({
423
639
  panels
424
640
  } = committedValuesRef.current;
425
641
  const groupElement = getPanelGroup(groupId);
642
+ assert(groupElement != null, `No group found for id "${groupId}"`);
426
643
  const {
427
644
  height,
428
645
  width
@@ -435,23 +652,28 @@ function useWindowSplitterPanelGroupBehavior({
435
652
  if (idBefore == null || idAfter == null) {
436
653
  return () => {};
437
654
  }
438
- let minSize = 0;
439
- let maxSize = 100;
655
+ let currentMinSize = 0;
656
+ let currentMaxSize = 100;
440
657
  let totalMinSize = 0;
441
658
  let totalMaxSize = 0;
442
659
 
443
660
  // A panel's effective min/max sizes also need to account for other panel's sizes.
444
661
  panelsArray.forEach(panelData => {
445
- if (panelData.current.id === idBefore) {
446
- maxSize = panelData.current.maxSize;
447
- 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;
448
670
  } else {
449
- totalMinSize += panelData.current.minSize;
450
- totalMaxSize += panelData.current.maxSize;
671
+ totalMinSize += minSize;
672
+ totalMaxSize += maxSize != null ? maxSize : 100;
451
673
  }
452
674
  });
453
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
454
- 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);
455
677
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
456
678
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
457
679
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -475,7 +697,7 @@ function useWindowSplitterPanelGroupBehavior({
475
697
  } else {
476
698
  delta = -(direction === "horizontal" ? width : height);
477
699
  }
478
- 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);
479
701
  if (sizes !== nextSizes) {
480
702
  setSizes(nextSizes);
481
703
  }
@@ -790,9 +1012,6 @@ const defaultStorage = {
790
1012
  // * dragHandleRect, sizes:
791
1013
  // When resizing is done via mouse/touch event– some initial state is stored
792
1014
  // so that any panels that contract will also expand if drag direction is reversed.
793
- // TODO
794
- // Within an active drag, remember original positions to refine more easily on expand.
795
- // Look at what the Chrome devtools Sources does.
796
1015
  function PanelGroupWithForwardedRef({
797
1016
  autoSaveId,
798
1017
  children = null,
@@ -804,7 +1023,8 @@ function PanelGroupWithForwardedRef({
804
1023
  onLayout,
805
1024
  storage = defaultStorage,
806
1025
  style: styleFromProps = {},
807
- tagName: Type = "div"
1026
+ tagName: Type = "div",
1027
+ units = "percentages"
808
1028
  }) {
809
1029
  const groupId = useUniqueId(idFromProps);
810
1030
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -817,6 +1037,7 @@ function PanelGroupWithForwardedRef({
817
1037
  const devWarningsRef = useRef({
818
1038
  didLogDefaultSizeWarning: false,
819
1039
  didLogIdAndOrderWarning: false,
1040
+ didLogInvalidLayoutWarning: false,
820
1041
  prevPanelIds: []
821
1042
  });
822
1043
 
@@ -839,32 +1060,58 @@ function PanelGroupWithForwardedRef({
839
1060
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
840
1061
  const committedValuesRef = useRef({
841
1062
  direction,
1063
+ id: groupId,
842
1064
  panels,
843
- sizes
1065
+ sizes,
1066
+ units
844
1067
  });
845
1068
  useImperativeHandle(forwardedRef, () => ({
846
- getLayout: () => {
1069
+ getId: () => groupId,
1070
+ getLayout: unitsFromParams => {
847
1071
  const {
848
- sizes
1072
+ sizes,
1073
+ units: unitsFromProps
849
1074
  } = committedValuesRef.current;
850
- 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
+ }
851
1082
  },
852
- setLayout: sizes => {
853
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
854
- assert(total === 100, "Panel sizes must add up to 100%");
1083
+ setLayout: (sizes, unitsFromParams) => {
855
1084
  const {
856
- panels
1085
+ id: groupId,
1086
+ panels,
1087
+ sizes: prevSizes,
1088
+ units
857
1089
  } = committedValuesRef.current;
1090
+ if ((unitsFromParams || units) === "pixels") {
1091
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1092
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1093
+ }
858
1094
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
859
1095
  const panelsArray = panelsMapToSortedArray(panels);
860
- setSizes(sizes);
861
- 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
+ }
862
1107
  }
863
- }), []);
1108
+ }), [groupId]);
864
1109
  useIsomorphicLayoutEffect(() => {
865
1110
  committedValuesRef.current.direction = direction;
1111
+ committedValuesRef.current.id = groupId;
866
1112
  committedValuesRef.current.panels = panels;
867
1113
  committedValuesRef.current.sizes = sizes;
1114
+ committedValuesRef.current.units = units;
868
1115
  });
869
1116
  useWindowSplitterPanelGroupBehavior({
870
1117
  committedValuesRef,
@@ -906,7 +1153,11 @@ function PanelGroupWithForwardedRef({
906
1153
  // Compute the initial sizes based on default weights.
907
1154
  // This assumes that panels register during initial mount (no conditional rendering)!
908
1155
  useIsomorphicLayoutEffect(() => {
909
- const sizes = committedValuesRef.current.sizes;
1156
+ const {
1157
+ id: groupId,
1158
+ sizes,
1159
+ units
1160
+ } = committedValuesRef.current;
910
1161
  if (sizes.length === panels.size) {
911
1162
  // Only compute (or restore) default sizes once per panel configuration.
912
1163
  return;
@@ -920,39 +1171,23 @@ function PanelGroupWithForwardedRef({
920
1171
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
921
1172
  }
922
1173
  if (defaultSizes != null) {
923
- 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);
924
1184
  } else {
925
- const panelsArray = panelsMapToSortedArray(panels);
926
- let panelsWithNullDefaultSize = 0;
927
- let totalDefaultSize = 0;
928
- let totalMinSize = 0;
929
-
930
- // TODO
931
- // Implicit default size calculations below do not account for inferred min/max size values.
932
- // 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.
933
- // For now, these logic edge cases are left to the user to handle via props.
934
-
935
- panelsArray.forEach(panel => {
936
- totalMinSize += panel.current.minSize;
937
- if (panel.current.defaultSize === null) {
938
- panelsWithNullDefaultSize++;
939
- } else {
940
- totalDefaultSize += panel.current.defaultSize;
941
- }
1185
+ const sizes = calculateDefaultLayout({
1186
+ groupId,
1187
+ panels,
1188
+ units
942
1189
  });
943
- if (totalDefaultSize > 100) {
944
- throw new Error(`Default panel sizes cannot exceed 100%`);
945
- } else if (panelsArray.length > 1 && panelsWithNullDefaultSize === 0 && totalDefaultSize !== 100) {
946
- throw new Error(`Invalid default sizes specified for panels`);
947
- } else if (totalMinSize > 100) {
948
- throw new Error(`Minimum panel sizes cannot exceed 100%`);
949
- }
950
- setSizes(panelsArray.map(panel => {
951
- if (panel.current.defaultSize === null) {
952
- return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
953
- }
954
- return panel.current.defaultSize;
955
- }));
1190
+ setSizes(sizes);
956
1191
  }
957
1192
  }, [autoSaveId, panels, storage]);
958
1193
  useEffect(() => {
@@ -990,6 +1225,48 @@ function PanelGroupWithForwardedRef({
990
1225
  }
991
1226
  }
992
1227
  }, [autoSaveId, panels, sizes, storage]);
1228
+ useIsomorphicLayoutEffect(() => {
1229
+ // Pixel panel constraints need to be reassessed after a group resize
1230
+ // We can avoid the ResizeObserver overhead for relative layouts
1231
+ if (units === "pixels") {
1232
+ const resizeObserver = new ResizeObserver(() => {
1233
+ const {
1234
+ panels,
1235
+ sizes: prevSizes
1236
+ } = committedValuesRef.current;
1237
+ const nextSizes = validatePanelGroupLayout({
1238
+ groupId,
1239
+ panels,
1240
+ nextSizes: prevSizes,
1241
+ prevSizes,
1242
+ units
1243
+ });
1244
+ if (!areEqual(prevSizes, nextSizes)) {
1245
+ setSizes(nextSizes);
1246
+ }
1247
+ });
1248
+ resizeObserver.observe(getPanelGroup(groupId));
1249
+ return () => {
1250
+ resizeObserver.disconnect();
1251
+ };
1252
+ }
1253
+ }, [groupId, units]);
1254
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1255
+ const {
1256
+ panels,
1257
+ units: unitsFromProps
1258
+ } = committedValuesRef.current;
1259
+ const panelsArray = panelsMapToSortedArray(panels);
1260
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1261
+ const size = sizes[index];
1262
+ const units = unitsFromParams ?? unitsFromProps;
1263
+ if (units === "pixels") {
1264
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1265
+ return size / 100 * groupSizePixels;
1266
+ } else {
1267
+ return size;
1268
+ }
1269
+ }, [groupId, sizes]);
993
1270
  const getPanelStyle = useCallback((id, defaultSize) => {
994
1271
  const {
995
1272
  panels
@@ -1023,6 +1300,10 @@ function PanelGroupWithForwardedRef({
1023
1300
  };
1024
1301
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1025
1302
  const registerPanel = useCallback((id, panelRef) => {
1303
+ const {
1304
+ units
1305
+ } = committedValuesRef.current;
1306
+ validatePanelProps(units, panelRef);
1026
1307
  setPanels(prevPanels => {
1027
1308
  if (prevPanels.has(id)) {
1028
1309
  return prevPanels;
@@ -1059,7 +1340,10 @@ function PanelGroupWithForwardedRef({
1059
1340
  }
1060
1341
  const size = isHorizontal ? rect.width : rect.height;
1061
1342
  const delta = movement / size * 100;
1062
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1343
+
1344
+ // If a validateLayout method has been provided
1345
+ // it's important to use it before updating the mouse cursor
1346
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1063
1347
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1064
1348
 
1065
1349
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1086,6 +1370,8 @@ function PanelGroupWithForwardedRef({
1086
1370
  }
1087
1371
  if (sizesChanged) {
1088
1372
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1373
+
1374
+ // It's okay to bypass in this case because we already validated above
1089
1375
  setSizes(nextSizes);
1090
1376
 
1091
1377
  // If resize change handlers have been declared, this is the time to call them.
@@ -1139,7 +1425,7 @@ function PanelGroupWithForwardedRef({
1139
1425
  }
1140
1426
  const isLastPanel = index === panelsArray.length - 1;
1141
1427
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1142
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1428
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1143
1429
  if (prevSizes !== nextSizes) {
1144
1430
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1145
1431
  setSizes(nextSizes);
@@ -1182,7 +1468,7 @@ function PanelGroupWithForwardedRef({
1182
1468
  }
1183
1469
  const isLastPanel = index === panelsArray.length - 1;
1184
1470
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1185
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1471
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1186
1472
  if (prevSizes !== nextSizes) {
1187
1473
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1188
1474
  setSizes(nextSizes);
@@ -1192,21 +1478,34 @@ function PanelGroupWithForwardedRef({
1192
1478
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1193
1479
  }
1194
1480
  }, []);
1195
- const resizePanel = useCallback((id, nextSize) => {
1481
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1196
1482
  const {
1483
+ id: groupId,
1197
1484
  panels,
1198
- sizes: prevSizes
1485
+ sizes: prevSizes,
1486
+ units
1199
1487
  } = committedValuesRef.current;
1488
+ if ((unitsFromParams || units) === "pixels") {
1489
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1490
+ nextSize = nextSize / groupSizePixels * 100;
1491
+ }
1200
1492
  const panel = panels.get(id);
1201
1493
  if (panel == null) {
1202
1494
  return;
1203
1495
  }
1204
- const {
1496
+ let {
1205
1497
  collapsedSize,
1206
1498
  collapsible,
1207
1499
  maxSize,
1208
1500
  minSize
1209
1501
  } = panel.current;
1502
+ if (units === "pixels") {
1503
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1504
+ minSize = minSize / groupSizePixels * 100;
1505
+ if (maxSize != null) {
1506
+ maxSize = maxSize / groupSizePixels * 100;
1507
+ }
1508
+ }
1210
1509
  const panelsArray = panelsMapToSortedArray(panels);
1211
1510
  const index = panelsArray.indexOf(panel);
1212
1511
  if (index < 0) {
@@ -1217,7 +1516,13 @@ function PanelGroupWithForwardedRef({
1217
1516
  return;
1218
1517
  }
1219
1518
  if (collapsible && nextSize === collapsedSize) ; else {
1220
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1519
+ const unsafeNextSize = nextSize;
1520
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1521
+ {
1522
+ if (unsafeNextSize !== nextSize) {
1523
+ console.error(`Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
1524
+ }
1525
+ }
1221
1526
  }
1222
1527
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1223
1528
  if (idBefore == null || idAfter == null) {
@@ -1225,7 +1530,7 @@ function PanelGroupWithForwardedRef({
1225
1530
  }
1226
1531
  const isLastPanel = index === panelsArray.length - 1;
1227
1532
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1228
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1533
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1229
1534
  if (prevSizes !== nextSizes) {
1230
1535
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1231
1536
  setSizes(nextSizes);
@@ -1240,6 +1545,7 @@ function PanelGroupWithForwardedRef({
1240
1545
  collapsePanel,
1241
1546
  direction,
1242
1547
  expandPanel,
1548
+ getPanelSize,
1243
1549
  getPanelStyle,
1244
1550
  groupId,
1245
1551
  registerPanel,
@@ -1261,8 +1567,9 @@ function PanelGroupWithForwardedRef({
1261
1567
  setActiveHandleId(null);
1262
1568
  initialDragStateRef.current = null;
1263
1569
  },
1570
+ units,
1264
1571
  unregisterPanel
1265
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1572
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1266
1573
  const style = {
1267
1574
  display: "flex",
1268
1575
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1277,6 +1584,7 @@ function PanelGroupWithForwardedRef({
1277
1584
  "data-panel-group": "",
1278
1585
  "data-panel-group-direction": direction,
1279
1586
  "data-panel-group-id": groupId,
1587
+ "data-panel-group-units": units,
1280
1588
  style: {
1281
1589
  ...style,
1282
1590
  ...styleFromProps
@@ -1426,4 +1734,4 @@ function PanelResizeHandle({
1426
1734
  }
1427
1735
  PanelResizeHandle.displayName = "PanelResizeHandle";
1428
1736
 
1429
- export { Panel, PanelGroup, PanelResizeHandle };
1737
+ export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };