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
@@ -47,8 +47,8 @@ function PanelWithForwardedRef({
47
47
  defaultSize = null,
48
48
  forwardedRef,
49
49
  id: idFromProps = null,
50
- maxSize = 100,
51
- minSize = 10,
50
+ maxSize = null,
51
+ minSize,
52
52
  onCollapse = null,
53
53
  onResize = null,
54
54
  order = null,
@@ -63,11 +63,22 @@ function PanelWithForwardedRef({
63
63
  const {
64
64
  collapsePanel,
65
65
  expandPanel,
66
+ getPanelSize,
66
67
  getPanelStyle,
67
68
  registerPanel,
68
69
  resizePanel,
70
+ units,
69
71
  unregisterPanel
70
72
  } = context;
73
+ if (minSize == null) {
74
+ if (units === "percentages") {
75
+ // Mimics legacy default value for percentage based panel groups
76
+ minSize = 10;
77
+ } else {
78
+ // There is no meaningful minimum pixel default we can provide
79
+ minSize = 0;
80
+ }
81
+ }
71
82
 
72
83
  // Use a ref to guard against users passing inline props
73
84
  const callbacksRef = useRef({
@@ -78,22 +89,6 @@ function PanelWithForwardedRef({
78
89
  callbacksRef.current.onCollapse = onCollapse;
79
90
  callbacksRef.current.onResize = onResize;
80
91
  });
81
-
82
- // Basic props validation
83
- if (minSize < 0 || minSize > 100) {
84
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
85
- } else if (maxSize < 0 || maxSize > 100) {
86
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
87
- } else {
88
- if (defaultSize !== null) {
89
- if (defaultSize < 0 || defaultSize > 100) {
90
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
91
- } else if (minSize > defaultSize && !collapsible) {
92
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
93
- defaultSize = minSize;
94
- }
95
- }
96
- }
97
92
  const style = getPanelStyle(panelId, defaultSize);
98
93
  const committedValuesRef = useRef({
99
94
  size: parseSizeFromStyle(style)
@@ -133,11 +128,14 @@ function PanelWithForwardedRef({
133
128
  getCollapsed() {
134
129
  return committedValuesRef.current.size === 0;
135
130
  },
136
- getSize() {
137
- return committedValuesRef.current.size;
131
+ getId() {
132
+ return panelId;
133
+ },
134
+ getSize(units) {
135
+ return getPanelSize(panelId, units);
138
136
  },
139
- resize: percentage => resizePanel(panelId, percentage)
140
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
137
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
138
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
141
139
  return createElement(Type, {
142
140
  children,
143
141
  className: classNameFromProps,
@@ -173,7 +171,13 @@ function parseSizeFromStyle(style) {
173
171
 
174
172
  const PRECISION = 10;
175
173
 
176
- function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse, initialDragState) {
174
+ function adjustByDelta(event, committedValues, idBefore, idAfter, deltaPixels, prevSizes, panelSizeBeforeCollapse, initialDragState) {
175
+ const {
176
+ id: groupId,
177
+ panels,
178
+ units
179
+ } = committedValues;
180
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
177
181
  const {
178
182
  sizes: initialSizes
179
183
  } = initialDragState || {};
@@ -181,9 +185,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
181
185
  // If we're resizing by mouse or touch, use the initial sizes as a base.
182
186
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
183
187
  const baseSizes = initialSizes || prevSizes;
184
- if (delta === 0) {
185
- return baseSizes;
186
- }
187
188
  const panelsArray = panelsMapToSortedArray(panels);
188
189
  const nextSizes = baseSizes.concat();
189
190
  let deltaApplied = 0;
@@ -198,11 +199,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
198
199
 
199
200
  // Max-bounds check the panel being expanded first.
200
201
  {
201
- const pivotId = delta < 0 ? idAfter : idBefore;
202
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
202
203
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
203
204
  const panel = panelsArray[index];
204
205
  const baseSize = baseSizes[index];
205
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
206
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
206
207
  if (baseSize === nextSize) {
207
208
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
208
209
  return baseSizes;
@@ -210,29 +211,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
210
211
  if (nextSize === 0 && baseSize > 0) {
211
212
  panelSizeBeforeCollapse.set(pivotId, baseSize);
212
213
  }
213
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
214
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
214
215
  }
215
216
  }
216
- let pivotId = delta < 0 ? idBefore : idAfter;
217
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
217
218
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
218
219
  while (true) {
219
220
  const panel = panelsArray[index];
220
221
  const baseSize = baseSizes[index];
221
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
222
- const nextSize = safeResizePanel(panel, 0 - deltaRemaining, baseSize, event);
222
+ const deltaRemaining = Math.abs(deltaPixels) - Math.abs(deltaApplied);
223
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize - deltaRemaining, event);
223
224
  if (baseSize !== nextSize) {
224
225
  if (nextSize === 0 && baseSize > 0) {
225
226
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
226
227
  }
227
228
  deltaApplied += baseSize - nextSize;
228
229
  nextSizes[index] = nextSize;
229
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
230
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
230
231
  numeric: true
231
232
  }) >= 0) {
232
233
  break;
233
234
  }
234
235
  }
235
- if (delta < 0) {
236
+ if (deltaPixels < 0) {
236
237
  if (--index < 0) {
237
238
  break;
238
239
  }
@@ -250,7 +251,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
250
251
  }
251
252
 
252
253
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
253
- pivotId = delta < 0 ? idAfter : idBefore;
254
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
254
255
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
255
256
  nextSizes[index] = baseSizes[index] + deltaApplied;
256
257
  return nextSizes;
@@ -289,6 +290,93 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
289
290
  }
290
291
  });
291
292
  }
293
+ function calculateDefaultLayout({
294
+ groupId,
295
+ panels,
296
+ units
297
+ }) {
298
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
299
+ const panelsArray = panelsMapToSortedArray(panels);
300
+ const sizes = Array(panelsArray.length);
301
+ let numPanelsWithSizes = 0;
302
+ let remainingSize = 100;
303
+
304
+ // Assigning default sizes requires a couple of passes:
305
+ // First, all panels with defaultSize should be set as-is
306
+ for (let index = 0; index < panelsArray.length; index++) {
307
+ const panel = panelsArray[index];
308
+ const {
309
+ defaultSize
310
+ } = panel.current;
311
+ if (defaultSize != null) {
312
+ numPanelsWithSizes++;
313
+ sizes[index] = units === "pixels" ? defaultSize / groupSizePixels * 100 : defaultSize;
314
+ remainingSize -= sizes[index];
315
+ }
316
+ }
317
+
318
+ // Remaining total size should be distributed evenly between panels
319
+ // This may require two passes, depending on min/max constraints
320
+ for (let index = 0; index < panelsArray.length; index++) {
321
+ const panel = panelsArray[index];
322
+ let {
323
+ defaultSize,
324
+ id,
325
+ maxSize,
326
+ minSize
327
+ } = panel.current;
328
+ if (defaultSize != null) {
329
+ continue;
330
+ }
331
+ if (units === "pixels") {
332
+ minSize = minSize / groupSizePixels * 100;
333
+ if (maxSize != null) {
334
+ maxSize = maxSize / groupSizePixels * 100;
335
+ }
336
+ }
337
+ const remainingPanels = panelsArray.length - numPanelsWithSizes;
338
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, remainingSize / remainingPanels));
339
+ sizes[index] = size;
340
+ numPanelsWithSizes++;
341
+ remainingSize -= size;
342
+ }
343
+
344
+ // If there is additional, left over space, assign it to any panel(s) that permits it
345
+ // (It's not worth taking multiple additional passes to evenly distribute)
346
+ if (remainingSize !== 0) {
347
+ for (let index = 0; index < panelsArray.length; index++) {
348
+ const panel = panelsArray[index];
349
+ let {
350
+ maxSize,
351
+ minSize
352
+ } = panel.current;
353
+ if (units === "pixels") {
354
+ minSize = minSize / groupSizePixels * 100;
355
+ if (maxSize != null) {
356
+ maxSize = maxSize / groupSizePixels * 100;
357
+ }
358
+ }
359
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, sizes[index] + remainingSize));
360
+ if (size !== sizes[index]) {
361
+ remainingSize -= size - sizes[index];
362
+ sizes[index] = size;
363
+
364
+ // Fuzzy comparison to account for imprecise floating point math
365
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ // Finally, if there is still left-over size, log an error
373
+ if (Math.abs(remainingSize).toFixed(3) !== "0.000") {
374
+ {
375
+ 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.`);
376
+ }
377
+ }
378
+ return sizes;
379
+ }
292
380
  function getBeforeAndAfterIds(id, panelsArray) {
293
381
  if (panelsArray.length < 2) {
294
382
  return [null, null];
@@ -302,6 +390,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
302
390
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
303
391
  return [idBefore, idAfter];
304
392
  }
393
+ function getAvailableGroupSizePixels(groupId) {
394
+ const panelGroupElement = getPanelGroup(groupId);
395
+ if (panelGroupElement == null) {
396
+ return NaN;
397
+ }
398
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
399
+ const resizeHandles = getResizeHandlesForGroup(groupId);
400
+ if (direction === "horizontal") {
401
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
402
+ return accumulated + handle.offsetWidth;
403
+ }, 0);
404
+ } else {
405
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
406
+ return accumulated + handle.offsetHeight;
407
+ }, 0);
408
+ }
409
+ }
305
410
 
306
411
  // This method returns a number between 1 and 100 representing
307
412
  // the % of the group's overall space this panel should occupy.
@@ -372,18 +477,24 @@ function panelsMapToSortedArray(panels) {
372
477
  }
373
478
  });
374
479
  }
375
- function safeResizePanel(panel, delta, prevSize, event) {
376
- const nextSizeUnsafe = prevSize + delta;
377
- const {
480
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
481
+ let {
378
482
  collapsedSize,
379
483
  collapsible,
380
484
  maxSize,
381
485
  minSize
382
486
  } = panel.current;
487
+ if (units === "pixels") {
488
+ collapsedSize = collapsedSize / groupSizePixels * 100;
489
+ if (maxSize != null) {
490
+ maxSize = maxSize / groupSizePixels * 100;
491
+ }
492
+ minSize = minSize / groupSizePixels * 100;
493
+ }
383
494
  if (collapsible) {
384
495
  if (prevSize > collapsedSize) {
385
496
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
386
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
497
+ if (nextSize <= minSize / 2 + collapsedSize) {
387
498
  return collapsedSize;
388
499
  }
389
500
  } else {
@@ -392,14 +503,119 @@ function safeResizePanel(panel, delta, prevSize, event) {
392
503
  // Keyboard events should expand a collapsed panel to the min size,
393
504
  // but mouse events should wait until the panel has reached its min size
394
505
  // to avoid a visual flickering when dragging between collapsed and min size.
395
- if (nextSizeUnsafe < minSize) {
506
+ if (nextSize < minSize) {
396
507
  return collapsedSize;
397
508
  }
398
509
  }
399
510
  }
400
511
  }
401
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
402
- return nextSize;
512
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
513
+ }
514
+ function validatePanelProps(units, panelData) {
515
+ const {
516
+ collapsible,
517
+ defaultSize,
518
+ maxSize,
519
+ minSize
520
+ } = panelData.current;
521
+
522
+ // Basic props validation
523
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
524
+ {
525
+ console.error(`Invalid Panel minSize provided, ${minSize}`);
526
+ }
527
+ panelData.current.minSize = 0;
528
+ }
529
+ if (maxSize != null) {
530
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
531
+ {
532
+ console.error(`Invalid Panel maxSize provided, ${maxSize}`);
533
+ }
534
+ panelData.current.maxSize = null;
535
+ }
536
+ }
537
+ if (defaultSize !== null) {
538
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
539
+ {
540
+ console.error(`Invalid Panel defaultSize provided, ${defaultSize}`);
541
+ }
542
+ panelData.current.defaultSize = null;
543
+ } else if (defaultSize < minSize && !collapsible) {
544
+ {
545
+ console.error(`Panel minSize (${minSize}) cannot be greater than defaultSize (${defaultSize})`);
546
+ }
547
+ panelData.current.defaultSize = minSize;
548
+ } else if (maxSize != null && defaultSize > maxSize) {
549
+ {
550
+ console.error(`Panel maxSize (${maxSize}) cannot be less than defaultSize (${defaultSize})`);
551
+ }
552
+ panelData.current.defaultSize = maxSize;
553
+ }
554
+ }
555
+ }
556
+ function validatePanelGroupLayout({
557
+ groupId,
558
+ panels,
559
+ nextSizes,
560
+ prevSizes,
561
+ units
562
+ }) {
563
+ // Clone because this method modifies
564
+ nextSizes = [...nextSizes];
565
+ const panelsArray = panelsMapToSortedArray(panels);
566
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
567
+ let remainingSize = 0;
568
+
569
+ // First, check all of the proposed sizes against the min/max constraints
570
+ for (let index = 0; index < panelsArray.length; index++) {
571
+ const panel = panelsArray[index];
572
+ const prevSize = prevSizes[index];
573
+ const nextSize = nextSizes[index];
574
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
575
+ if (nextSize != safeNextSize) {
576
+ remainingSize += nextSize - safeNextSize;
577
+ nextSizes[index] = safeNextSize;
578
+ {
579
+ console.error(`Invalid size (${nextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
580
+ }
581
+ }
582
+ }
583
+
584
+ // If there is additional, left over space, assign it to any panel(s) that permits it
585
+ // (It's not worth taking multiple additional passes to evenly distribute)
586
+ if (remainingSize.toFixed(3) !== "0.000") {
587
+ for (let index = 0; index < panelsArray.length; index++) {
588
+ const panel = panelsArray[index];
589
+ let {
590
+ maxSize,
591
+ minSize
592
+ } = panel.current;
593
+ if (units === "pixels") {
594
+ minSize = minSize / groupSizePixels * 100;
595
+ if (maxSize != null) {
596
+ maxSize = maxSize / groupSizePixels * 100;
597
+ }
598
+ }
599
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
600
+ if (size !== nextSizes[index]) {
601
+ remainingSize -= size - nextSizes[index];
602
+ nextSizes[index] = size;
603
+
604
+ // Fuzzy comparison to account for imprecise floating point math
605
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
606
+ break;
607
+ }
608
+ }
609
+ }
610
+ }
611
+
612
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
613
+ if (remainingSize.toFixed(3) !== "0.000") {
614
+ {
615
+ console.error(`"Invalid panel group configuration; default panel sizes should total 100% but was ${100 - remainingSize}%`);
616
+ }
617
+ }
618
+ return nextSizes;
403
619
  }
404
620
 
405
621
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -425,6 +641,7 @@ function useWindowSplitterPanelGroupBehavior({
425
641
  panels
426
642
  } = committedValuesRef.current;
427
643
  const groupElement = getPanelGroup(groupId);
644
+ assert(groupElement != null, `No group found for id "${groupId}"`);
428
645
  const {
429
646
  height,
430
647
  width
@@ -437,23 +654,28 @@ function useWindowSplitterPanelGroupBehavior({
437
654
  if (idBefore == null || idAfter == null) {
438
655
  return () => {};
439
656
  }
440
- let minSize = 0;
441
- let maxSize = 100;
657
+ let currentMinSize = 0;
658
+ let currentMaxSize = 100;
442
659
  let totalMinSize = 0;
443
660
  let totalMaxSize = 0;
444
661
 
445
662
  // A panel's effective min/max sizes also need to account for other panel's sizes.
446
663
  panelsArray.forEach(panelData => {
447
- if (panelData.current.id === idBefore) {
448
- maxSize = panelData.current.maxSize;
449
- minSize = panelData.current.minSize;
664
+ const {
665
+ id,
666
+ maxSize,
667
+ minSize
668
+ } = panelData.current;
669
+ if (id === idBefore) {
670
+ currentMinSize = minSize;
671
+ currentMaxSize = maxSize != null ? maxSize : 100;
450
672
  } else {
451
- totalMinSize += panelData.current.minSize;
452
- totalMaxSize += panelData.current.maxSize;
673
+ totalMinSize += minSize;
674
+ totalMaxSize += maxSize != null ? maxSize : 100;
453
675
  }
454
676
  });
455
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
456
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
677
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
678
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
457
679
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
458
680
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
459
681
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -477,7 +699,7 @@ function useWindowSplitterPanelGroupBehavior({
477
699
  } else {
478
700
  delta = -(direction === "horizontal" ? width : height);
479
701
  }
480
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
702
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
481
703
  if (sizes !== nextSizes) {
482
704
  setSizes(nextSizes);
483
705
  }
@@ -792,9 +1014,6 @@ const defaultStorage = {
792
1014
  // * dragHandleRect, sizes:
793
1015
  // When resizing is done via mouse/touch event– some initial state is stored
794
1016
  // so that any panels that contract will also expand if drag direction is reversed.
795
- // TODO
796
- // Within an active drag, remember original positions to refine more easily on expand.
797
- // Look at what the Chrome devtools Sources does.
798
1017
  function PanelGroupWithForwardedRef({
799
1018
  autoSaveId,
800
1019
  children = null,
@@ -806,7 +1025,8 @@ function PanelGroupWithForwardedRef({
806
1025
  onLayout,
807
1026
  storage = defaultStorage,
808
1027
  style: styleFromProps = {},
809
- tagName: Type = "div"
1028
+ tagName: Type = "div",
1029
+ units = "percentages"
810
1030
  }) {
811
1031
  const groupId = useUniqueId(idFromProps);
812
1032
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -819,6 +1039,7 @@ function PanelGroupWithForwardedRef({
819
1039
  const devWarningsRef = useRef({
820
1040
  didLogDefaultSizeWarning: false,
821
1041
  didLogIdAndOrderWarning: false,
1042
+ didLogInvalidLayoutWarning: false,
822
1043
  prevPanelIds: []
823
1044
  });
824
1045
 
@@ -841,32 +1062,58 @@ function PanelGroupWithForwardedRef({
841
1062
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
842
1063
  const committedValuesRef = useRef({
843
1064
  direction,
1065
+ id: groupId,
844
1066
  panels,
845
- sizes
1067
+ sizes,
1068
+ units
846
1069
  });
847
1070
  useImperativeHandle(forwardedRef, () => ({
848
- getLayout: () => {
1071
+ getId: () => groupId,
1072
+ getLayout: unitsFromParams => {
849
1073
  const {
850
- sizes
1074
+ sizes,
1075
+ units: unitsFromProps
851
1076
  } = committedValuesRef.current;
852
- return sizes;
1077
+ const units = unitsFromParams ?? unitsFromProps;
1078
+ if (units === "pixels") {
1079
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1080
+ return sizes.map(size => size / 100 * groupSizePixels);
1081
+ } else {
1082
+ return sizes;
1083
+ }
853
1084
  },
854
- setLayout: sizes => {
855
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
856
- assert(total === 100, "Panel sizes must add up to 100%");
1085
+ setLayout: (sizes, unitsFromParams) => {
857
1086
  const {
858
- panels
1087
+ id: groupId,
1088
+ panels,
1089
+ sizes: prevSizes,
1090
+ units
859
1091
  } = committedValuesRef.current;
1092
+ if ((unitsFromParams || units) === "pixels") {
1093
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1094
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1095
+ }
860
1096
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
861
1097
  const panelsArray = panelsMapToSortedArray(panels);
862
- setSizes(sizes);
863
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1098
+ const nextSizes = validatePanelGroupLayout({
1099
+ groupId,
1100
+ panels,
1101
+ nextSizes: sizes,
1102
+ prevSizes,
1103
+ units
1104
+ });
1105
+ if (!areEqual(prevSizes, nextSizes)) {
1106
+ setSizes(nextSizes);
1107
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1108
+ }
864
1109
  }
865
- }), []);
1110
+ }), [groupId]);
866
1111
  useIsomorphicLayoutEffect(() => {
867
1112
  committedValuesRef.current.direction = direction;
1113
+ committedValuesRef.current.id = groupId;
868
1114
  committedValuesRef.current.panels = panels;
869
1115
  committedValuesRef.current.sizes = sizes;
1116
+ committedValuesRef.current.units = units;
870
1117
  });
871
1118
  useWindowSplitterPanelGroupBehavior({
872
1119
  committedValuesRef,
@@ -908,7 +1155,11 @@ function PanelGroupWithForwardedRef({
908
1155
  // Compute the initial sizes based on default weights.
909
1156
  // This assumes that panels register during initial mount (no conditional rendering)!
910
1157
  useIsomorphicLayoutEffect(() => {
911
- const sizes = committedValuesRef.current.sizes;
1158
+ const {
1159
+ id: groupId,
1160
+ sizes,
1161
+ units
1162
+ } = committedValuesRef.current;
912
1163
  if (sizes.length === panels.size) {
913
1164
  // Only compute (or restore) default sizes once per panel configuration.
914
1165
  return;
@@ -922,39 +1173,23 @@ function PanelGroupWithForwardedRef({
922
1173
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
923
1174
  }
924
1175
  if (defaultSizes != null) {
925
- setSizes(defaultSizes);
1176
+ // Validate saved sizes in case something has changed since last render
1177
+ // e.g. for pixel groups, this could be the size of the window
1178
+ const validatedSizes = validatePanelGroupLayout({
1179
+ groupId,
1180
+ panels,
1181
+ nextSizes: defaultSizes,
1182
+ prevSizes: defaultSizes,
1183
+ units
1184
+ });
1185
+ setSizes(validatedSizes);
926
1186
  } else {
927
- const panelsArray = panelsMapToSortedArray(panels);
928
- let panelsWithNullDefaultSize = 0;
929
- let totalDefaultSize = 0;
930
- let totalMinSize = 0;
931
-
932
- // TODO
933
- // Implicit default size calculations below do not account for inferred min/max size values.
934
- // 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.
935
- // For now, these logic edge cases are left to the user to handle via props.
936
-
937
- panelsArray.forEach(panel => {
938
- totalMinSize += panel.current.minSize;
939
- if (panel.current.defaultSize === null) {
940
- panelsWithNullDefaultSize++;
941
- } else {
942
- totalDefaultSize += panel.current.defaultSize;
943
- }
1187
+ const sizes = calculateDefaultLayout({
1188
+ groupId,
1189
+ panels,
1190
+ units
944
1191
  });
945
- if (totalDefaultSize > 100) {
946
- throw new Error(`Default panel sizes cannot exceed 100%`);
947
- } else if (panelsArray.length > 1 && panelsWithNullDefaultSize === 0 && totalDefaultSize !== 100) {
948
- throw new Error(`Invalid default sizes specified for panels`);
949
- } else if (totalMinSize > 100) {
950
- throw new Error(`Minimum panel sizes cannot exceed 100%`);
951
- }
952
- setSizes(panelsArray.map(panel => {
953
- if (panel.current.defaultSize === null) {
954
- return (100 - totalDefaultSize) / panelsWithNullDefaultSize;
955
- }
956
- return panel.current.defaultSize;
957
- }));
1192
+ setSizes(sizes);
958
1193
  }
959
1194
  }, [autoSaveId, panels, storage]);
960
1195
  useEffect(() => {
@@ -992,6 +1227,48 @@ function PanelGroupWithForwardedRef({
992
1227
  }
993
1228
  }
994
1229
  }, [autoSaveId, panels, sizes, storage]);
1230
+ useIsomorphicLayoutEffect(() => {
1231
+ // Pixel panel constraints need to be reassessed after a group resize
1232
+ // We can avoid the ResizeObserver overhead for relative layouts
1233
+ if (units === "pixels") {
1234
+ const resizeObserver = new ResizeObserver(() => {
1235
+ const {
1236
+ panels,
1237
+ sizes: prevSizes
1238
+ } = committedValuesRef.current;
1239
+ const nextSizes = validatePanelGroupLayout({
1240
+ groupId,
1241
+ panels,
1242
+ nextSizes: prevSizes,
1243
+ prevSizes,
1244
+ units
1245
+ });
1246
+ if (!areEqual(prevSizes, nextSizes)) {
1247
+ setSizes(nextSizes);
1248
+ }
1249
+ });
1250
+ resizeObserver.observe(getPanelGroup(groupId));
1251
+ return () => {
1252
+ resizeObserver.disconnect();
1253
+ };
1254
+ }
1255
+ }, [groupId, units]);
1256
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1257
+ const {
1258
+ panels,
1259
+ units: unitsFromProps
1260
+ } = committedValuesRef.current;
1261
+ const panelsArray = panelsMapToSortedArray(panels);
1262
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1263
+ const size = sizes[index];
1264
+ const units = unitsFromParams ?? unitsFromProps;
1265
+ if (units === "pixels") {
1266
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1267
+ return size / 100 * groupSizePixels;
1268
+ } else {
1269
+ return size;
1270
+ }
1271
+ }, [groupId, sizes]);
995
1272
  const getPanelStyle = useCallback((id, defaultSize) => {
996
1273
  const {
997
1274
  panels
@@ -1030,6 +1307,10 @@ function PanelGroupWithForwardedRef({
1030
1307
  };
1031
1308
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1032
1309
  const registerPanel = useCallback((id, panelRef) => {
1310
+ const {
1311
+ units
1312
+ } = committedValuesRef.current;
1313
+ validatePanelProps(units, panelRef);
1033
1314
  setPanels(prevPanels => {
1034
1315
  if (prevPanels.has(id)) {
1035
1316
  return prevPanels;
@@ -1066,7 +1347,10 @@ function PanelGroupWithForwardedRef({
1066
1347
  }
1067
1348
  const size = isHorizontal ? rect.width : rect.height;
1068
1349
  const delta = movement / size * 100;
1069
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1350
+
1351
+ // If a validateLayout method has been provided
1352
+ // it's important to use it before updating the mouse cursor
1353
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1070
1354
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1071
1355
 
1072
1356
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1093,6 +1377,8 @@ function PanelGroupWithForwardedRef({
1093
1377
  }
1094
1378
  if (sizesChanged) {
1095
1379
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1380
+
1381
+ // It's okay to bypass in this case because we already validated above
1096
1382
  setSizes(nextSizes);
1097
1383
 
1098
1384
  // If resize change handlers have been declared, this is the time to call them.
@@ -1146,7 +1432,7 @@ function PanelGroupWithForwardedRef({
1146
1432
  }
1147
1433
  const isLastPanel = index === panelsArray.length - 1;
1148
1434
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1149
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1435
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1150
1436
  if (prevSizes !== nextSizes) {
1151
1437
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1152
1438
  setSizes(nextSizes);
@@ -1189,7 +1475,7 @@ function PanelGroupWithForwardedRef({
1189
1475
  }
1190
1476
  const isLastPanel = index === panelsArray.length - 1;
1191
1477
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1192
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1478
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1193
1479
  if (prevSizes !== nextSizes) {
1194
1480
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1195
1481
  setSizes(nextSizes);
@@ -1199,21 +1485,34 @@ function PanelGroupWithForwardedRef({
1199
1485
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1200
1486
  }
1201
1487
  }, []);
1202
- const resizePanel = useCallback((id, nextSize) => {
1488
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1203
1489
  const {
1490
+ id: groupId,
1204
1491
  panels,
1205
- sizes: prevSizes
1492
+ sizes: prevSizes,
1493
+ units
1206
1494
  } = committedValuesRef.current;
1495
+ if ((unitsFromParams || units) === "pixels") {
1496
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1497
+ nextSize = nextSize / groupSizePixels * 100;
1498
+ }
1207
1499
  const panel = panels.get(id);
1208
1500
  if (panel == null) {
1209
1501
  return;
1210
1502
  }
1211
- const {
1503
+ let {
1212
1504
  collapsedSize,
1213
1505
  collapsible,
1214
1506
  maxSize,
1215
1507
  minSize
1216
1508
  } = panel.current;
1509
+ if (units === "pixels") {
1510
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1511
+ minSize = minSize / groupSizePixels * 100;
1512
+ if (maxSize != null) {
1513
+ maxSize = maxSize / groupSizePixels * 100;
1514
+ }
1515
+ }
1217
1516
  const panelsArray = panelsMapToSortedArray(panels);
1218
1517
  const index = panelsArray.indexOf(panel);
1219
1518
  if (index < 0) {
@@ -1224,7 +1523,13 @@ function PanelGroupWithForwardedRef({
1224
1523
  return;
1225
1524
  }
1226
1525
  if (collapsible && nextSize === collapsedSize) ; else {
1227
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1526
+ const unsafeNextSize = nextSize;
1527
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1528
+ {
1529
+ if (unsafeNextSize !== nextSize) {
1530
+ console.error(`Invalid size (${unsafeNextSize}) specified for Panel "${panel.current.id}" given the panel's min/max size constraints`);
1531
+ }
1532
+ }
1228
1533
  }
1229
1534
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1230
1535
  if (idBefore == null || idAfter == null) {
@@ -1232,7 +1537,7 @@ function PanelGroupWithForwardedRef({
1232
1537
  }
1233
1538
  const isLastPanel = index === panelsArray.length - 1;
1234
1539
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1235
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1540
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1236
1541
  if (prevSizes !== nextSizes) {
1237
1542
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1238
1543
  setSizes(nextSizes);
@@ -1247,6 +1552,7 @@ function PanelGroupWithForwardedRef({
1247
1552
  collapsePanel,
1248
1553
  direction,
1249
1554
  expandPanel,
1555
+ getPanelSize,
1250
1556
  getPanelStyle,
1251
1557
  groupId,
1252
1558
  registerPanel,
@@ -1268,8 +1574,9 @@ function PanelGroupWithForwardedRef({
1268
1574
  setActiveHandleId(null);
1269
1575
  initialDragStateRef.current = null;
1270
1576
  },
1577
+ units,
1271
1578
  unregisterPanel
1272
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1579
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1273
1580
  const style = {
1274
1581
  display: "flex",
1275
1582
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1284,6 +1591,7 @@ function PanelGroupWithForwardedRef({
1284
1591
  "data-panel-group": "",
1285
1592
  "data-panel-group-direction": direction,
1286
1593
  "data-panel-group-id": groupId,
1594
+ "data-panel-group-units": units,
1287
1595
  style: {
1288
1596
  ...style,
1289
1597
  ...styleFromProps
@@ -1433,4 +1741,4 @@ function PanelResizeHandle({
1433
1741
  }
1434
1742
  PanelResizeHandle.displayName = "PanelResizeHandle";
1435
1743
 
1436
- export { Panel, PanelGroup, PanelResizeHandle };
1744
+ export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };