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;
136
131
  },
137
- resize: percentage => resizePanel(panelId, percentage)
138
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
132
+ getSize(units) {
133
+ return getPanelSize(panelId, units);
134
+ },
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,89 @@ 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
+ return sizes;
373
+ }
290
374
  function getBeforeAndAfterIds(id, panelsArray) {
291
375
  if (panelsArray.length < 2) {
292
376
  return [null, null];
@@ -300,6 +384,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
300
384
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
301
385
  return [idBefore, idAfter];
302
386
  }
387
+ function getAvailableGroupSizePixels(groupId) {
388
+ const panelGroupElement = getPanelGroup(groupId);
389
+ if (panelGroupElement == null) {
390
+ return NaN;
391
+ }
392
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
393
+ const resizeHandles = getResizeHandlesForGroup(groupId);
394
+ if (direction === "horizontal") {
395
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
396
+ return accumulated + handle.offsetWidth;
397
+ }, 0);
398
+ } else {
399
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
400
+ return accumulated + handle.offsetHeight;
401
+ }, 0);
402
+ }
403
+ }
303
404
 
304
405
  // This method returns a number between 1 and 100 representing
305
406
  // the % of the group's overall space this panel should occupy.
@@ -370,18 +471,24 @@ function panelsMapToSortedArray(panels) {
370
471
  }
371
472
  });
372
473
  }
373
- function safeResizePanel(panel, delta, prevSize, event) {
374
- const nextSizeUnsafe = prevSize + delta;
375
- const {
474
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
475
+ let {
376
476
  collapsedSize,
377
477
  collapsible,
378
478
  maxSize,
379
479
  minSize
380
480
  } = panel.current;
481
+ if (units === "pixels") {
482
+ collapsedSize = collapsedSize / groupSizePixels * 100;
483
+ if (maxSize != null) {
484
+ maxSize = maxSize / groupSizePixels * 100;
485
+ }
486
+ minSize = minSize / groupSizePixels * 100;
487
+ }
381
488
  if (collapsible) {
382
489
  if (prevSize > collapsedSize) {
383
490
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
384
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
491
+ if (nextSize <= minSize / 2 + collapsedSize) {
385
492
  return collapsedSize;
386
493
  }
387
494
  } else {
@@ -390,14 +497,97 @@ function safeResizePanel(panel, delta, prevSize, event) {
390
497
  // Keyboard events should expand a collapsed panel to the min size,
391
498
  // but mouse events should wait until the panel has reached its min size
392
499
  // to avoid a visual flickering when dragging between collapsed and min size.
393
- if (nextSizeUnsafe < minSize) {
500
+ if (nextSize < minSize) {
394
501
  return collapsedSize;
395
502
  }
396
503
  }
397
504
  }
398
505
  }
399
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
400
- return nextSize;
506
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
507
+ }
508
+ function validatePanelProps(units, panelData) {
509
+ const {
510
+ collapsible,
511
+ defaultSize,
512
+ maxSize,
513
+ minSize
514
+ } = panelData.current;
515
+
516
+ // Basic props validation
517
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
518
+ panelData.current.minSize = 0;
519
+ }
520
+ if (maxSize != null) {
521
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
522
+ panelData.current.maxSize = null;
523
+ }
524
+ }
525
+ if (defaultSize !== null) {
526
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
527
+ panelData.current.defaultSize = null;
528
+ } else if (defaultSize < minSize && !collapsible) {
529
+ panelData.current.defaultSize = minSize;
530
+ } else if (maxSize != null && defaultSize > maxSize) {
531
+ panelData.current.defaultSize = maxSize;
532
+ }
533
+ }
534
+ }
535
+ function validatePanelGroupLayout({
536
+ groupId,
537
+ panels,
538
+ nextSizes,
539
+ prevSizes,
540
+ units
541
+ }) {
542
+ // Clone because this method modifies
543
+ nextSizes = [...nextSizes];
544
+ const panelsArray = panelsMapToSortedArray(panels);
545
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
546
+ let remainingSize = 0;
547
+
548
+ // First, check all of the proposed sizes against the min/max constraints
549
+ for (let index = 0; index < panelsArray.length; index++) {
550
+ const panel = panelsArray[index];
551
+ const prevSize = prevSizes[index];
552
+ const nextSize = nextSizes[index];
553
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
554
+ if (nextSize != safeNextSize) {
555
+ remainingSize += nextSize - safeNextSize;
556
+ nextSizes[index] = safeNextSize;
557
+ }
558
+ }
559
+
560
+ // If there is additional, left over space, assign it to any panel(s) that permits it
561
+ // (It's not worth taking multiple additional passes to evenly distribute)
562
+ if (remainingSize.toFixed(3) !== "0.000") {
563
+ for (let index = 0; index < panelsArray.length; index++) {
564
+ const panel = panelsArray[index];
565
+ let {
566
+ maxSize,
567
+ minSize
568
+ } = panel.current;
569
+ if (units === "pixels") {
570
+ minSize = minSize / groupSizePixels * 100;
571
+ if (maxSize != null) {
572
+ maxSize = maxSize / groupSizePixels * 100;
573
+ }
574
+ }
575
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
576
+ if (size !== nextSizes[index]) {
577
+ remainingSize -= size - nextSizes[index];
578
+ nextSizes[index] = size;
579
+
580
+ // Fuzzy comparison to account for imprecise floating point math
581
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
589
+ if (remainingSize.toFixed(3) !== "0.000") ;
590
+ return nextSizes;
401
591
  }
402
592
 
403
593
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -423,6 +613,7 @@ function useWindowSplitterPanelGroupBehavior({
423
613
  panels
424
614
  } = committedValuesRef.current;
425
615
  const groupElement = getPanelGroup(groupId);
616
+ assert(groupElement != null, `No group found for id "${groupId}"`);
426
617
  const {
427
618
  height,
428
619
  width
@@ -435,23 +626,28 @@ function useWindowSplitterPanelGroupBehavior({
435
626
  if (idBefore == null || idAfter == null) {
436
627
  return () => {};
437
628
  }
438
- let minSize = 0;
439
- let maxSize = 100;
629
+ let currentMinSize = 0;
630
+ let currentMaxSize = 100;
440
631
  let totalMinSize = 0;
441
632
  let totalMaxSize = 0;
442
633
 
443
634
  // A panel's effective min/max sizes also need to account for other panel's sizes.
444
635
  panelsArray.forEach(panelData => {
445
- if (panelData.current.id === idBefore) {
446
- maxSize = panelData.current.maxSize;
447
- minSize = panelData.current.minSize;
636
+ const {
637
+ id,
638
+ maxSize,
639
+ minSize
640
+ } = panelData.current;
641
+ if (id === idBefore) {
642
+ currentMinSize = minSize;
643
+ currentMaxSize = maxSize != null ? maxSize : 100;
448
644
  } else {
449
- totalMinSize += panelData.current.minSize;
450
- totalMaxSize += panelData.current.maxSize;
645
+ totalMinSize += minSize;
646
+ totalMaxSize += maxSize != null ? maxSize : 100;
451
647
  }
452
648
  });
453
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
454
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
649
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
650
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
455
651
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
456
652
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
457
653
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -475,7 +671,7 @@ function useWindowSplitterPanelGroupBehavior({
475
671
  } else {
476
672
  delta = -(direction === "horizontal" ? width : height);
477
673
  }
478
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
674
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
479
675
  if (sizes !== nextSizes) {
480
676
  setSizes(nextSizes);
481
677
  }
@@ -790,9 +986,6 @@ const defaultStorage = {
790
986
  // * dragHandleRect, sizes:
791
987
  // When resizing is done via mouse/touch event– some initial state is stored
792
988
  // 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
989
  function PanelGroupWithForwardedRef({
797
990
  autoSaveId,
798
991
  children = null,
@@ -804,7 +997,8 @@ function PanelGroupWithForwardedRef({
804
997
  onLayout,
805
998
  storage = defaultStorage,
806
999
  style: styleFromProps = {},
807
- tagName: Type = "div"
1000
+ tagName: Type = "div",
1001
+ units = "percentages"
808
1002
  }) {
809
1003
  const groupId = useUniqueId(idFromProps);
810
1004
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -817,6 +1011,7 @@ function PanelGroupWithForwardedRef({
817
1011
  useRef({
818
1012
  didLogDefaultSizeWarning: false,
819
1013
  didLogIdAndOrderWarning: false,
1014
+ didLogInvalidLayoutWarning: false,
820
1015
  prevPanelIds: []
821
1016
  });
822
1017
 
@@ -839,32 +1034,58 @@ function PanelGroupWithForwardedRef({
839
1034
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
840
1035
  const committedValuesRef = useRef({
841
1036
  direction,
1037
+ id: groupId,
842
1038
  panels,
843
- sizes
1039
+ sizes,
1040
+ units
844
1041
  });
845
1042
  useImperativeHandle(forwardedRef, () => ({
846
- getLayout: () => {
1043
+ getId: () => groupId,
1044
+ getLayout: unitsFromParams => {
847
1045
  const {
848
- sizes
1046
+ sizes,
1047
+ units: unitsFromProps
849
1048
  } = committedValuesRef.current;
850
- return sizes;
1049
+ const units = unitsFromParams ?? unitsFromProps;
1050
+ if (units === "pixels") {
1051
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1052
+ return sizes.map(size => size / 100 * groupSizePixels);
1053
+ } else {
1054
+ return sizes;
1055
+ }
851
1056
  },
852
- setLayout: sizes => {
853
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
854
- assert(total === 100, "Panel sizes must add up to 100%");
1057
+ setLayout: (sizes, unitsFromParams) => {
855
1058
  const {
856
- panels
1059
+ id: groupId,
1060
+ panels,
1061
+ sizes: prevSizes,
1062
+ units
857
1063
  } = committedValuesRef.current;
1064
+ if ((unitsFromParams || units) === "pixels") {
1065
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1066
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1067
+ }
858
1068
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
859
1069
  const panelsArray = panelsMapToSortedArray(panels);
860
- setSizes(sizes);
861
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1070
+ const nextSizes = validatePanelGroupLayout({
1071
+ groupId,
1072
+ panels,
1073
+ nextSizes: sizes,
1074
+ prevSizes,
1075
+ units
1076
+ });
1077
+ if (!areEqual(prevSizes, nextSizes)) {
1078
+ setSizes(nextSizes);
1079
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1080
+ }
862
1081
  }
863
- }), []);
1082
+ }), [groupId]);
864
1083
  useIsomorphicLayoutEffect(() => {
865
1084
  committedValuesRef.current.direction = direction;
1085
+ committedValuesRef.current.id = groupId;
866
1086
  committedValuesRef.current.panels = panels;
867
1087
  committedValuesRef.current.sizes = sizes;
1088
+ committedValuesRef.current.units = units;
868
1089
  });
869
1090
  useWindowSplitterPanelGroupBehavior({
870
1091
  committedValuesRef,
@@ -906,7 +1127,11 @@ function PanelGroupWithForwardedRef({
906
1127
  // Compute the initial sizes based on default weights.
907
1128
  // This assumes that panels register during initial mount (no conditional rendering)!
908
1129
  useIsomorphicLayoutEffect(() => {
909
- const sizes = committedValuesRef.current.sizes;
1130
+ const {
1131
+ id: groupId,
1132
+ sizes,
1133
+ units
1134
+ } = committedValuesRef.current;
910
1135
  if (sizes.length === panels.size) {
911
1136
  // Only compute (or restore) default sizes once per panel configuration.
912
1137
  return;
@@ -920,39 +1145,23 @@ function PanelGroupWithForwardedRef({
920
1145
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
921
1146
  }
922
1147
  if (defaultSizes != null) {
923
- setSizes(defaultSizes);
1148
+ // Validate saved sizes in case something has changed since last render
1149
+ // e.g. for pixel groups, this could be the size of the window
1150
+ const validatedSizes = validatePanelGroupLayout({
1151
+ groupId,
1152
+ panels,
1153
+ nextSizes: defaultSizes,
1154
+ prevSizes: defaultSizes,
1155
+ units
1156
+ });
1157
+ setSizes(validatedSizes);
924
1158
  } 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
- }
1159
+ const sizes = calculateDefaultLayout({
1160
+ groupId,
1161
+ panels,
1162
+ units
942
1163
  });
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
- }));
1164
+ setSizes(sizes);
956
1165
  }
957
1166
  }, [autoSaveId, panels, storage]);
958
1167
  useEffect(() => {
@@ -970,6 +1179,48 @@ function PanelGroupWithForwardedRef({
970
1179
  debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
971
1180
  }
972
1181
  }, [autoSaveId, panels, sizes, storage]);
1182
+ useIsomorphicLayoutEffect(() => {
1183
+ // Pixel panel constraints need to be reassessed after a group resize
1184
+ // We can avoid the ResizeObserver overhead for relative layouts
1185
+ if (units === "pixels") {
1186
+ const resizeObserver = new ResizeObserver(() => {
1187
+ const {
1188
+ panels,
1189
+ sizes: prevSizes
1190
+ } = committedValuesRef.current;
1191
+ const nextSizes = validatePanelGroupLayout({
1192
+ groupId,
1193
+ panels,
1194
+ nextSizes: prevSizes,
1195
+ prevSizes,
1196
+ units
1197
+ });
1198
+ if (!areEqual(prevSizes, nextSizes)) {
1199
+ setSizes(nextSizes);
1200
+ }
1201
+ });
1202
+ resizeObserver.observe(getPanelGroup(groupId));
1203
+ return () => {
1204
+ resizeObserver.disconnect();
1205
+ };
1206
+ }
1207
+ }, [groupId, units]);
1208
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1209
+ const {
1210
+ panels,
1211
+ units: unitsFromProps
1212
+ } = committedValuesRef.current;
1213
+ const panelsArray = panelsMapToSortedArray(panels);
1214
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1215
+ const size = sizes[index];
1216
+ const units = unitsFromParams ?? unitsFromProps;
1217
+ if (units === "pixels") {
1218
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1219
+ return size / 100 * groupSizePixels;
1220
+ } else {
1221
+ return size;
1222
+ }
1223
+ }, [groupId, sizes]);
973
1224
  const getPanelStyle = useCallback((id, defaultSize) => {
974
1225
  const {
975
1226
  panels
@@ -1000,6 +1251,10 @@ function PanelGroupWithForwardedRef({
1000
1251
  };
1001
1252
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1002
1253
  const registerPanel = useCallback((id, panelRef) => {
1254
+ const {
1255
+ units
1256
+ } = committedValuesRef.current;
1257
+ validatePanelProps(units, panelRef);
1003
1258
  setPanels(prevPanels => {
1004
1259
  if (prevPanels.has(id)) {
1005
1260
  return prevPanels;
@@ -1036,7 +1291,10 @@ function PanelGroupWithForwardedRef({
1036
1291
  }
1037
1292
  const size = isHorizontal ? rect.width : rect.height;
1038
1293
  const delta = movement / size * 100;
1039
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1294
+
1295
+ // If a validateLayout method has been provided
1296
+ // it's important to use it before updating the mouse cursor
1297
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1040
1298
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1041
1299
 
1042
1300
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1063,6 +1321,8 @@ function PanelGroupWithForwardedRef({
1063
1321
  }
1064
1322
  if (sizesChanged) {
1065
1323
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1324
+
1325
+ // It's okay to bypass in this case because we already validated above
1066
1326
  setSizes(nextSizes);
1067
1327
 
1068
1328
  // If resize change handlers have been declared, this is the time to call them.
@@ -1116,7 +1376,7 @@ function PanelGroupWithForwardedRef({
1116
1376
  }
1117
1377
  const isLastPanel = index === panelsArray.length - 1;
1118
1378
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1119
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1379
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1120
1380
  if (prevSizes !== nextSizes) {
1121
1381
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1122
1382
  setSizes(nextSizes);
@@ -1159,7 +1419,7 @@ function PanelGroupWithForwardedRef({
1159
1419
  }
1160
1420
  const isLastPanel = index === panelsArray.length - 1;
1161
1421
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1162
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1422
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1163
1423
  if (prevSizes !== nextSizes) {
1164
1424
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1165
1425
  setSizes(nextSizes);
@@ -1169,21 +1429,34 @@ function PanelGroupWithForwardedRef({
1169
1429
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1170
1430
  }
1171
1431
  }, []);
1172
- const resizePanel = useCallback((id, nextSize) => {
1432
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1173
1433
  const {
1434
+ id: groupId,
1174
1435
  panels,
1175
- sizes: prevSizes
1436
+ sizes: prevSizes,
1437
+ units
1176
1438
  } = committedValuesRef.current;
1439
+ if ((unitsFromParams || units) === "pixels") {
1440
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1441
+ nextSize = nextSize / groupSizePixels * 100;
1442
+ }
1177
1443
  const panel = panels.get(id);
1178
1444
  if (panel == null) {
1179
1445
  return;
1180
1446
  }
1181
- const {
1447
+ let {
1182
1448
  collapsedSize,
1183
1449
  collapsible,
1184
1450
  maxSize,
1185
1451
  minSize
1186
1452
  } = panel.current;
1453
+ if (units === "pixels") {
1454
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1455
+ minSize = minSize / groupSizePixels * 100;
1456
+ if (maxSize != null) {
1457
+ maxSize = maxSize / groupSizePixels * 100;
1458
+ }
1459
+ }
1187
1460
  const panelsArray = panelsMapToSortedArray(panels);
1188
1461
  const index = panelsArray.indexOf(panel);
1189
1462
  if (index < 0) {
@@ -1194,7 +1467,7 @@ function PanelGroupWithForwardedRef({
1194
1467
  return;
1195
1468
  }
1196
1469
  if (collapsible && nextSize === collapsedSize) ; else {
1197
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1470
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1198
1471
  }
1199
1472
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1200
1473
  if (idBefore == null || idAfter == null) {
@@ -1202,7 +1475,7 @@ function PanelGroupWithForwardedRef({
1202
1475
  }
1203
1476
  const isLastPanel = index === panelsArray.length - 1;
1204
1477
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1205
- 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);
1206
1479
  if (prevSizes !== nextSizes) {
1207
1480
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1208
1481
  setSizes(nextSizes);
@@ -1217,6 +1490,7 @@ function PanelGroupWithForwardedRef({
1217
1490
  collapsePanel,
1218
1491
  direction,
1219
1492
  expandPanel,
1493
+ getPanelSize,
1220
1494
  getPanelStyle,
1221
1495
  groupId,
1222
1496
  registerPanel,
@@ -1238,8 +1512,9 @@ function PanelGroupWithForwardedRef({
1238
1512
  setActiveHandleId(null);
1239
1513
  initialDragStateRef.current = null;
1240
1514
  },
1515
+ units,
1241
1516
  unregisterPanel
1242
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1517
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1243
1518
  const style = {
1244
1519
  display: "flex",
1245
1520
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1254,6 +1529,7 @@ function PanelGroupWithForwardedRef({
1254
1529
  "data-panel-group": "",
1255
1530
  "data-panel-group-direction": direction,
1256
1531
  "data-panel-group-id": groupId,
1532
+ "data-panel-group-units": units,
1257
1533
  style: {
1258
1534
  ...style,
1259
1535
  ...styleFromProps
@@ -1403,4 +1679,4 @@ function PanelResizeHandle({
1403
1679
  }
1404
1680
  PanelResizeHandle.displayName = "PanelResizeHandle";
1405
1681
 
1406
- export { Panel, PanelGroup, PanelResizeHandle };
1682
+ export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };