react-resizable-panels 0.0.53 → 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 (41) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/declarations/src/Panel.d.ts +6 -5
  3. package/dist/declarations/src/PanelGroup.d.ts +8 -4
  4. package/dist/declarations/src/PanelResizeHandle.d.ts +5 -2
  5. package/dist/declarations/src/index.d.ts +9 -8
  6. package/dist/declarations/src/types.d.ts +4 -2
  7. package/dist/declarations/src/utils/group.d.ts +29 -0
  8. package/dist/react-resizable-panels.browser.cjs.js +1709 -0
  9. package/dist/react-resizable-panels.browser.cjs.mjs +6 -0
  10. package/dist/react-resizable-panels.browser.development.cjs.js +1764 -0
  11. package/dist/react-resizable-panels.browser.development.cjs.mjs +6 -0
  12. package/dist/react-resizable-panels.browser.development.esm.js +1737 -0
  13. package/dist/react-resizable-panels.browser.esm.js +1682 -0
  14. package/dist/react-resizable-panels.cjs.js +395 -126
  15. package/dist/react-resizable-panels.cjs.js.map +1 -0
  16. package/dist/react-resizable-panels.cjs.mjs +2 -1
  17. package/dist/react-resizable-panels.development.cjs.js +452 -135
  18. package/dist/react-resizable-panels.development.cjs.mjs +6 -0
  19. package/dist/react-resizable-panels.development.esm.js +452 -136
  20. package/dist/react-resizable-panels.development.node.cjs.js +1579 -0
  21. package/dist/react-resizable-panels.development.node.cjs.mjs +6 -0
  22. package/dist/react-resizable-panels.development.node.esm.js +1552 -0
  23. package/dist/react-resizable-panels.esm.js +395 -127
  24. package/dist/react-resizable-panels.esm.js.map +1 -0
  25. package/dist/react-resizable-panels.node.cjs.js +1523 -0
  26. package/dist/react-resizable-panels.node.cjs.mjs +6 -0
  27. package/dist/react-resizable-panels.node.esm.js +1496 -0
  28. package/package.json +26 -1
  29. package/src/Panel.ts +37 -37
  30. package/src/PanelContexts.ts +5 -6
  31. package/src/PanelGroup.ts +269 -121
  32. package/src/PanelResizeHandle.ts +1 -4
  33. package/src/env-conditions/browser.ts +1 -0
  34. package/src/env-conditions/node.ts +1 -0
  35. package/src/env-conditions/unknown.ts +1 -0
  36. package/src/hooks/useIsomorphicEffect.ts +2 -9
  37. package/src/hooks/useWindowSplitterBehavior.ts +14 -11
  38. package/src/index.ts +11 -3
  39. package/src/types.ts +3 -1
  40. package/src/utils/group.ts +327 -28
  41. package/src/utils/ssr.ts +0 -7
@@ -1,5 +1,7 @@
1
1
  import * as React from 'react';
2
2
 
3
+ const isBrowser = typeof window !== "undefined";
4
+
3
5
  // This module exists to work around Webpack issue https://github.com/webpack/webpack/issues/14814
4
6
 
5
7
  // eslint-disable-next-line no-restricted-imports
@@ -21,8 +23,7 @@ const {
21
23
  // `toString()` prevents bundlers from trying to `import { useId } from 'react'`
22
24
  const useId = React["useId".toString()];
23
25
 
24
- const canUseEffectHooks = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
25
- const useIsomorphicLayoutEffect = canUseEffectHooks ? useLayoutEffect : () => {};
26
+ const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : () => {};
26
27
 
27
28
  const wrappedUseId = typeof useId === "function" ? useId : () => null;
28
29
  let counter = 0;
@@ -36,10 +37,6 @@ function useUniqueId(idFromParams = null) {
36
37
  }
37
38
 
38
39
  const PanelGroupContext = createContext(null);
39
-
40
- // Workaround for Parcel scope hoisting (which renames objects/functions).
41
- // Casting to :any is required to avoid corrupting the generated TypeScript types.
42
- // See github.com/parcel-bundler/parcel/issues/8724
43
40
  PanelGroupContext.displayName = "PanelGroupContext";
44
41
 
45
42
  function PanelWithForwardedRef({
@@ -50,8 +47,8 @@ function PanelWithForwardedRef({
50
47
  defaultSize = null,
51
48
  forwardedRef,
52
49
  id: idFromProps = null,
53
- maxSize = 100,
54
- minSize = 10,
50
+ maxSize = null,
51
+ minSize,
55
52
  onCollapse = null,
56
53
  onResize = null,
57
54
  order = null,
@@ -66,11 +63,22 @@ function PanelWithForwardedRef({
66
63
  const {
67
64
  collapsePanel,
68
65
  expandPanel,
66
+ getPanelSize,
69
67
  getPanelStyle,
70
68
  registerPanel,
71
69
  resizePanel,
70
+ units,
72
71
  unregisterPanel
73
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
+ }
74
82
 
75
83
  // Use a ref to guard against users passing inline props
76
84
  const callbacksRef = useRef({
@@ -81,22 +89,6 @@ function PanelWithForwardedRef({
81
89
  callbacksRef.current.onCollapse = onCollapse;
82
90
  callbacksRef.current.onResize = onResize;
83
91
  });
84
-
85
- // Basic props validation
86
- if (minSize < 0 || minSize > 100) {
87
- throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`);
88
- } else if (maxSize < 0 || maxSize > 100) {
89
- throw Error(`Panel maxSize must be between 0 and 100, but was ${maxSize}`);
90
- } else {
91
- if (defaultSize !== null) {
92
- if (defaultSize < 0 || defaultSize > 100) {
93
- throw Error(`Panel defaultSize must be between 0 and 100, but was ${defaultSize}`);
94
- } else if (minSize > defaultSize && !collapsible) {
95
- console.error(`Panel minSize ${minSize} cannot be greater than defaultSize ${defaultSize}`);
96
- defaultSize = minSize;
97
- }
98
- }
99
- }
100
92
  const style = getPanelStyle(panelId, defaultSize);
101
93
  const committedValuesRef = useRef({
102
94
  size: parseSizeFromStyle(style)
@@ -107,6 +99,7 @@ function PanelWithForwardedRef({
107
99
  collapsible,
108
100
  defaultSize,
109
101
  id: panelId,
102
+ idWasAutoGenerated: idFromProps == null,
110
103
  maxSize,
111
104
  minSize,
112
105
  order
@@ -118,6 +111,7 @@ function PanelWithForwardedRef({
118
111
  panelDataRef.current.collapsible = collapsible;
119
112
  panelDataRef.current.defaultSize = defaultSize;
120
113
  panelDataRef.current.id = panelId;
114
+ panelDataRef.current.idWasAutoGenerated = idFromProps == null;
121
115
  panelDataRef.current.maxSize = maxSize;
122
116
  panelDataRef.current.minSize = minSize;
123
117
  panelDataRef.current.order = order;
@@ -134,11 +128,14 @@ function PanelWithForwardedRef({
134
128
  getCollapsed() {
135
129
  return committedValuesRef.current.size === 0;
136
130
  },
137
- getSize() {
138
- return committedValuesRef.current.size;
131
+ getId() {
132
+ return panelId;
139
133
  },
140
- resize: percentage => resizePanel(panelId, percentage)
141
- }), [collapsePanel, expandPanel, panelId, resizePanel]);
134
+ getSize(units) {
135
+ return getPanelSize(panelId, units);
136
+ },
137
+ resize: (percentage, units) => resizePanel(panelId, percentage, units)
138
+ }), [collapsePanel, expandPanel, getPanelSize, panelId, resizePanel]);
142
139
  return createElement(Type, {
143
140
  children,
144
141
  className: classNameFromProps,
@@ -157,10 +154,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
157
154
  ...props,
158
155
  forwardedRef: ref
159
156
  }));
160
-
161
- // Workaround for Parcel scope hoisting (which renames objects/functions).
162
- // Casting to :any is required to avoid corrupting the generated TypeScript types.
163
- // See github.com/parcel-bundler/parcel/issues/8724
164
157
  PanelWithForwardedRef.displayName = "Panel";
165
158
  Panel.displayName = "forwardRef(Panel)";
166
159
 
@@ -178,7 +171,13 @@ function parseSizeFromStyle(style) {
178
171
 
179
172
  const PRECISION = 10;
180
173
 
181
- 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;
182
181
  const {
183
182
  sizes: initialSizes
184
183
  } = initialDragState || {};
@@ -186,9 +185,6 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
186
185
  // If we're resizing by mouse or touch, use the initial sizes as a base.
187
186
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
188
187
  const baseSizes = initialSizes || prevSizes;
189
- if (delta === 0) {
190
- return baseSizes;
191
- }
192
188
  const panelsArray = panelsMapToSortedArray(panels);
193
189
  const nextSizes = baseSizes.concat();
194
190
  let deltaApplied = 0;
@@ -203,11 +199,11 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
203
199
 
204
200
  // Max-bounds check the panel being expanded first.
205
201
  {
206
- const pivotId = delta < 0 ? idAfter : idBefore;
202
+ const pivotId = deltaPixels < 0 ? idAfter : idBefore;
207
203
  const index = panelsArray.findIndex(panel => panel.current.id === pivotId);
208
204
  const panel = panelsArray[index];
209
205
  const baseSize = baseSizes[index];
210
- const nextSize = safeResizePanel(panel, Math.abs(delta), baseSize, event);
206
+ const nextSize = safeResizePanel(units, groupSizePixels, panel, baseSize, baseSize + Math.abs(deltaPixels), event);
211
207
  if (baseSize === nextSize) {
212
208
  // If there's no room for the pivot panel to grow, we can ignore this drag update.
213
209
  return baseSizes;
@@ -215,29 +211,29 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
215
211
  if (nextSize === 0 && baseSize > 0) {
216
212
  panelSizeBeforeCollapse.set(pivotId, baseSize);
217
213
  }
218
- delta = delta < 0 ? baseSize - nextSize : nextSize - baseSize;
214
+ deltaPixels = deltaPixels < 0 ? baseSize - nextSize : nextSize - baseSize;
219
215
  }
220
216
  }
221
- let pivotId = delta < 0 ? idBefore : idAfter;
217
+ let pivotId = deltaPixels < 0 ? idBefore : idAfter;
222
218
  let index = panelsArray.findIndex(panel => panel.current.id === pivotId);
223
219
  while (true) {
224
220
  const panel = panelsArray[index];
225
221
  const baseSize = baseSizes[index];
226
- const deltaRemaining = Math.abs(delta) - Math.abs(deltaApplied);
227
- 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);
228
224
  if (baseSize !== nextSize) {
229
225
  if (nextSize === 0 && baseSize > 0) {
230
226
  panelSizeBeforeCollapse.set(panel.current.id, baseSize);
231
227
  }
232
228
  deltaApplied += baseSize - nextSize;
233
229
  nextSizes[index] = nextSize;
234
- if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(delta).toPrecision(PRECISION), undefined, {
230
+ if (deltaApplied.toPrecision(PRECISION).localeCompare(Math.abs(deltaPixels).toPrecision(PRECISION), undefined, {
235
231
  numeric: true
236
232
  }) >= 0) {
237
233
  break;
238
234
  }
239
235
  }
240
- if (delta < 0) {
236
+ if (deltaPixels < 0) {
241
237
  if (--index < 0) {
242
238
  break;
243
239
  }
@@ -255,7 +251,7 @@ function adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panel
255
251
  }
256
252
 
257
253
  // Adjust the pivot panel before, but only by the amount that surrounding panels were able to shrink/contract.
258
- pivotId = delta < 0 ? idAfter : idBefore;
254
+ pivotId = deltaPixels < 0 ? idAfter : idBefore;
259
255
  index = panelsArray.findIndex(panel => panel.current.id === pivotId);
260
256
  nextSizes[index] = baseSizes[index] + deltaApplied;
261
257
  return nextSizes;
@@ -294,6 +290,89 @@ function callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap) {
294
290
  }
295
291
  });
296
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
+ return sizes;
375
+ }
297
376
  function getBeforeAndAfterIds(id, panelsArray) {
298
377
  if (panelsArray.length < 2) {
299
378
  return [null, null];
@@ -307,6 +386,23 @@ function getBeforeAndAfterIds(id, panelsArray) {
307
386
  const idAfter = isLastPanel ? id : panelsArray[index + 1].current.id;
308
387
  return [idBefore, idAfter];
309
388
  }
389
+ function getAvailableGroupSizePixels(groupId) {
390
+ const panelGroupElement = getPanelGroup(groupId);
391
+ if (panelGroupElement == null) {
392
+ return NaN;
393
+ }
394
+ const direction = panelGroupElement.getAttribute("data-panel-group-direction");
395
+ const resizeHandles = getResizeHandlesForGroup(groupId);
396
+ if (direction === "horizontal") {
397
+ return panelGroupElement.offsetWidth - resizeHandles.reduce((accumulated, handle) => {
398
+ return accumulated + handle.offsetWidth;
399
+ }, 0);
400
+ } else {
401
+ return panelGroupElement.offsetHeight - resizeHandles.reduce((accumulated, handle) => {
402
+ return accumulated + handle.offsetHeight;
403
+ }, 0);
404
+ }
405
+ }
310
406
 
311
407
  // This method returns a number between 1 and 100 representing
312
408
  // the % of the group's overall space this panel should occupy.
@@ -377,18 +473,24 @@ function panelsMapToSortedArray(panels) {
377
473
  }
378
474
  });
379
475
  }
380
- function safeResizePanel(panel, delta, prevSize, event) {
381
- const nextSizeUnsafe = prevSize + delta;
382
- const {
476
+ function safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize, event = null) {
477
+ let {
383
478
  collapsedSize,
384
479
  collapsible,
385
480
  maxSize,
386
481
  minSize
387
482
  } = panel.current;
483
+ if (units === "pixels") {
484
+ collapsedSize = collapsedSize / groupSizePixels * 100;
485
+ if (maxSize != null) {
486
+ maxSize = maxSize / groupSizePixels * 100;
487
+ }
488
+ minSize = minSize / groupSizePixels * 100;
489
+ }
388
490
  if (collapsible) {
389
491
  if (prevSize > collapsedSize) {
390
492
  // Mimic VS COde behavior; collapse a panel if it's smaller than half of its min-size
391
- if (nextSizeUnsafe <= minSize / 2 + collapsedSize) {
493
+ if (nextSize <= minSize / 2 + collapsedSize) {
392
494
  return collapsedSize;
393
495
  }
394
496
  } else {
@@ -397,14 +499,97 @@ function safeResizePanel(panel, delta, prevSize, event) {
397
499
  // Keyboard events should expand a collapsed panel to the min size,
398
500
  // but mouse events should wait until the panel has reached its min size
399
501
  // to avoid a visual flickering when dragging between collapsed and min size.
400
- if (nextSizeUnsafe < minSize) {
502
+ if (nextSize < minSize) {
401
503
  return collapsedSize;
402
504
  }
403
505
  }
404
506
  }
405
507
  }
406
- const nextSize = Math.min(maxSize, Math.max(minSize, nextSizeUnsafe));
407
- return nextSize;
508
+ return Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
509
+ }
510
+ function validatePanelProps(units, panelData) {
511
+ const {
512
+ collapsible,
513
+ defaultSize,
514
+ maxSize,
515
+ minSize
516
+ } = panelData.current;
517
+
518
+ // Basic props validation
519
+ if (minSize < 0 || units === "percentages" && minSize > 100) {
520
+ panelData.current.minSize = 0;
521
+ }
522
+ if (maxSize != null) {
523
+ if (maxSize < 0 || units === "percentages" && maxSize > 100) {
524
+ panelData.current.maxSize = null;
525
+ }
526
+ }
527
+ if (defaultSize !== null) {
528
+ if (defaultSize < 0 || units === "percentages" && defaultSize > 100) {
529
+ panelData.current.defaultSize = null;
530
+ } else if (defaultSize < minSize && !collapsible) {
531
+ panelData.current.defaultSize = minSize;
532
+ } else if (maxSize != null && defaultSize > maxSize) {
533
+ panelData.current.defaultSize = maxSize;
534
+ }
535
+ }
536
+ }
537
+ function validatePanelGroupLayout({
538
+ groupId,
539
+ panels,
540
+ nextSizes,
541
+ prevSizes,
542
+ units
543
+ }) {
544
+ // Clone because this method modifies
545
+ nextSizes = [...nextSizes];
546
+ const panelsArray = panelsMapToSortedArray(panels);
547
+ const groupSizePixels = units === "pixels" ? getAvailableGroupSizePixels(groupId) : NaN;
548
+ let remainingSize = 0;
549
+
550
+ // First, check all of the proposed sizes against the min/max constraints
551
+ for (let index = 0; index < panelsArray.length; index++) {
552
+ const panel = panelsArray[index];
553
+ const prevSize = prevSizes[index];
554
+ const nextSize = nextSizes[index];
555
+ const safeNextSize = safeResizePanel(units, groupSizePixels, panel, prevSize, nextSize);
556
+ if (nextSize != safeNextSize) {
557
+ remainingSize += nextSize - safeNextSize;
558
+ nextSizes[index] = safeNextSize;
559
+ }
560
+ }
561
+
562
+ // If there is additional, left over space, assign it to any panel(s) that permits it
563
+ // (It's not worth taking multiple additional passes to evenly distribute)
564
+ if (remainingSize.toFixed(3) !== "0.000") {
565
+ for (let index = 0; index < panelsArray.length; index++) {
566
+ const panel = panelsArray[index];
567
+ let {
568
+ maxSize,
569
+ minSize
570
+ } = panel.current;
571
+ if (units === "pixels") {
572
+ minSize = minSize / groupSizePixels * 100;
573
+ if (maxSize != null) {
574
+ maxSize = maxSize / groupSizePixels * 100;
575
+ }
576
+ }
577
+ const size = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSizes[index] + remainingSize));
578
+ if (size !== nextSizes[index]) {
579
+ remainingSize -= size - nextSizes[index];
580
+ nextSizes[index] = size;
581
+
582
+ // Fuzzy comparison to account for imprecise floating point math
583
+ if (Math.abs(remainingSize).toFixed(3) === "0.000") {
584
+ break;
585
+ }
586
+ }
587
+ }
588
+ }
589
+
590
+ // If we still have remainder, the requested layout wasn't valid and we should warn about it
591
+ if (remainingSize.toFixed(3) !== "0.000") ;
592
+ return nextSizes;
408
593
  }
409
594
 
410
595
  function assert(expectedCondition, message = "Assertion failed!") {
@@ -430,6 +615,7 @@ function useWindowSplitterPanelGroupBehavior({
430
615
  panels
431
616
  } = committedValuesRef.current;
432
617
  const groupElement = getPanelGroup(groupId);
618
+ assert(groupElement != null, `No group found for id "${groupId}"`);
433
619
  const {
434
620
  height,
435
621
  width
@@ -442,23 +628,28 @@ function useWindowSplitterPanelGroupBehavior({
442
628
  if (idBefore == null || idAfter == null) {
443
629
  return () => {};
444
630
  }
445
- let minSize = 0;
446
- let maxSize = 100;
631
+ let currentMinSize = 0;
632
+ let currentMaxSize = 100;
447
633
  let totalMinSize = 0;
448
634
  let totalMaxSize = 0;
449
635
 
450
636
  // A panel's effective min/max sizes also need to account for other panel's sizes.
451
637
  panelsArray.forEach(panelData => {
452
- if (panelData.current.id === idBefore) {
453
- maxSize = panelData.current.maxSize;
454
- minSize = panelData.current.minSize;
638
+ const {
639
+ id,
640
+ maxSize,
641
+ minSize
642
+ } = panelData.current;
643
+ if (id === idBefore) {
644
+ currentMinSize = minSize;
645
+ currentMaxSize = maxSize != null ? maxSize : 100;
455
646
  } else {
456
- totalMinSize += panelData.current.minSize;
457
- totalMaxSize += panelData.current.maxSize;
647
+ totalMinSize += minSize;
648
+ totalMaxSize += maxSize != null ? maxSize : 100;
458
649
  }
459
650
  });
460
- const ariaValueMax = Math.min(maxSize, 100 - totalMinSize);
461
- const ariaValueMin = Math.max(minSize, (panelsArray.length - 1) * 100 - totalMaxSize);
651
+ const ariaValueMax = Math.min(currentMaxSize, 100 - totalMinSize);
652
+ const ariaValueMin = Math.max(currentMinSize, (panelsArray.length - 1) * 100 - totalMaxSize);
462
653
  const flexGrow = getFlexGrow(panels, idBefore, sizes);
463
654
  handle.setAttribute("aria-valuemax", "" + Math.round(ariaValueMax));
464
655
  handle.setAttribute("aria-valuemin", "" + Math.round(ariaValueMin));
@@ -482,7 +673,7 @@ function useWindowSplitterPanelGroupBehavior({
482
673
  } else {
483
674
  delta = -(direction === "horizontal" ? width : height);
484
675
  }
485
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
676
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, sizes, panelSizeBeforeCollapse.current, null);
486
677
  if (sizes !== nextSizes) {
487
678
  setSizes(nextSizes);
488
679
  }
@@ -797,9 +988,6 @@ const defaultStorage = {
797
988
  // * dragHandleRect, sizes:
798
989
  // When resizing is done via mouse/touch event– some initial state is stored
799
990
  // so that any panels that contract will also expand if drag direction is reversed.
800
- // TODO
801
- // Within an active drag, remember original positions to refine more easily on expand.
802
- // Look at what the Chrome devtools Sources does.
803
991
  function PanelGroupWithForwardedRef({
804
992
  autoSaveId,
805
993
  children = null,
@@ -811,7 +999,8 @@ function PanelGroupWithForwardedRef({
811
999
  onLayout,
812
1000
  storage = defaultStorage,
813
1001
  style: styleFromProps = {},
814
- tagName: Type = "div"
1002
+ tagName: Type = "div",
1003
+ units = "percentages"
815
1004
  }) {
816
1005
  const groupId = useUniqueId(idFromProps);
817
1006
  const [activeHandleId, setActiveHandleId] = useState(null);
@@ -821,6 +1010,12 @@ function PanelGroupWithForwardedRef({
821
1010
  // We store the initial Panel sizes in this ref, and apply move deltas to them instead of to the current sizes.
822
1011
  // This has the benefit of causing force-collapsed panels to spring back open if drag is reversed.
823
1012
  const initialDragStateRef = useRef(null);
1013
+ useRef({
1014
+ didLogDefaultSizeWarning: false,
1015
+ didLogIdAndOrderWarning: false,
1016
+ didLogInvalidLayoutWarning: false,
1017
+ prevPanelIds: []
1018
+ });
824
1019
 
825
1020
  // Use a ref to guard against users passing inline props
826
1021
  const callbacksRef = useRef({
@@ -841,32 +1036,58 @@ function PanelGroupWithForwardedRef({
841
1036
  // Store committed values to avoid unnecessarily re-running memoization/effects functions.
842
1037
  const committedValuesRef = useRef({
843
1038
  direction,
1039
+ id: groupId,
844
1040
  panels,
845
- sizes
1041
+ sizes,
1042
+ units
846
1043
  });
847
1044
  useImperativeHandle(forwardedRef, () => ({
848
- getLayout: () => {
1045
+ getId: () => groupId,
1046
+ getLayout: unitsFromParams => {
849
1047
  const {
850
- sizes
1048
+ sizes,
1049
+ units: unitsFromProps
851
1050
  } = committedValuesRef.current;
852
- return sizes;
1051
+ const units = unitsFromParams ?? unitsFromProps;
1052
+ if (units === "pixels") {
1053
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1054
+ return sizes.map(size => size / 100 * groupSizePixels);
1055
+ } else {
1056
+ return sizes;
1057
+ }
853
1058
  },
854
- setLayout: sizes => {
855
- const total = sizes.reduce((accumulated, current) => accumulated + current, 0);
856
- assert(total === 100, "Panel sizes must add up to 100%");
1059
+ setLayout: (sizes, unitsFromParams) => {
857
1060
  const {
858
- panels
1061
+ id: groupId,
1062
+ panels,
1063
+ sizes: prevSizes,
1064
+ units
859
1065
  } = committedValuesRef.current;
1066
+ if ((unitsFromParams || units) === "pixels") {
1067
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1068
+ sizes = sizes.map(size => size / groupSizePixels * 100);
1069
+ }
860
1070
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
861
1071
  const panelsArray = panelsMapToSortedArray(panels);
862
- setSizes(sizes);
863
- callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap);
1072
+ const nextSizes = validatePanelGroupLayout({
1073
+ groupId,
1074
+ panels,
1075
+ nextSizes: sizes,
1076
+ prevSizes,
1077
+ units
1078
+ });
1079
+ if (!areEqual(prevSizes, nextSizes)) {
1080
+ setSizes(nextSizes);
1081
+ callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1082
+ }
864
1083
  }
865
- }), []);
1084
+ }), [groupId]);
866
1085
  useIsomorphicLayoutEffect(() => {
867
1086
  committedValuesRef.current.direction = direction;
1087
+ committedValuesRef.current.id = groupId;
868
1088
  committedValuesRef.current.panels = panels;
869
1089
  committedValuesRef.current.sizes = sizes;
1090
+ committedValuesRef.current.units = units;
870
1091
  });
871
1092
  useWindowSplitterPanelGroupBehavior({
872
1093
  committedValuesRef,
@@ -908,7 +1129,11 @@ function PanelGroupWithForwardedRef({
908
1129
  // Compute the initial sizes based on default weights.
909
1130
  // This assumes that panels register during initial mount (no conditional rendering)!
910
1131
  useIsomorphicLayoutEffect(() => {
911
- const sizes = committedValuesRef.current.sizes;
1132
+ const {
1133
+ id: groupId,
1134
+ sizes,
1135
+ units
1136
+ } = committedValuesRef.current;
912
1137
  if (sizes.length === panels.size) {
913
1138
  // Only compute (or restore) default sizes once per panel configuration.
914
1139
  return;
@@ -922,39 +1147,23 @@ function PanelGroupWithForwardedRef({
922
1147
  defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage);
923
1148
  }
924
1149
  if (defaultSizes != null) {
925
- setSizes(defaultSizes);
1150
+ // Validate saved sizes in case something has changed since last render
1151
+ // e.g. for pixel groups, this could be the size of the window
1152
+ const validatedSizes = validatePanelGroupLayout({
1153
+ groupId,
1154
+ panels,
1155
+ nextSizes: defaultSizes,
1156
+ prevSizes: defaultSizes,
1157
+ units
1158
+ });
1159
+ setSizes(validatedSizes);
926
1160
  } 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
- }
1161
+ const sizes = calculateDefaultLayout({
1162
+ groupId,
1163
+ panels,
1164
+ units
944
1165
  });
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
- }));
1166
+ setSizes(sizes);
958
1167
  }
959
1168
  }, [autoSaveId, panels, storage]);
960
1169
  useEffect(() => {
@@ -972,6 +1181,48 @@ function PanelGroupWithForwardedRef({
972
1181
  debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage);
973
1182
  }
974
1183
  }, [autoSaveId, panels, sizes, storage]);
1184
+ useIsomorphicLayoutEffect(() => {
1185
+ // Pixel panel constraints need to be reassessed after a group resize
1186
+ // We can avoid the ResizeObserver overhead for relative layouts
1187
+ if (units === "pixels") {
1188
+ const resizeObserver = new ResizeObserver(() => {
1189
+ const {
1190
+ panels,
1191
+ sizes: prevSizes
1192
+ } = committedValuesRef.current;
1193
+ const nextSizes = validatePanelGroupLayout({
1194
+ groupId,
1195
+ panels,
1196
+ nextSizes: prevSizes,
1197
+ prevSizes,
1198
+ units
1199
+ });
1200
+ if (!areEqual(prevSizes, nextSizes)) {
1201
+ setSizes(nextSizes);
1202
+ }
1203
+ });
1204
+ resizeObserver.observe(getPanelGroup(groupId));
1205
+ return () => {
1206
+ resizeObserver.disconnect();
1207
+ };
1208
+ }
1209
+ }, [groupId, units]);
1210
+ const getPanelSize = useCallback((id, unitsFromParams) => {
1211
+ const {
1212
+ panels,
1213
+ units: unitsFromProps
1214
+ } = committedValuesRef.current;
1215
+ const panelsArray = panelsMapToSortedArray(panels);
1216
+ const index = panelsArray.findIndex(panel => panel.current.id === id);
1217
+ const size = sizes[index];
1218
+ const units = unitsFromParams ?? unitsFromProps;
1219
+ if (units === "pixels") {
1220
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1221
+ return size / 100 * groupSizePixels;
1222
+ } else {
1223
+ return size;
1224
+ }
1225
+ }, [groupId, sizes]);
975
1226
  const getPanelStyle = useCallback((id, defaultSize) => {
976
1227
  const {
977
1228
  panels
@@ -1002,6 +1253,10 @@ function PanelGroupWithForwardedRef({
1002
1253
  };
1003
1254
  }, [activeHandleId, disablePointerEventsDuringResize, sizes]);
1004
1255
  const registerPanel = useCallback((id, panelRef) => {
1256
+ const {
1257
+ units
1258
+ } = committedValuesRef.current;
1259
+ validatePanelProps(units, panelRef);
1005
1260
  setPanels(prevPanels => {
1006
1261
  if (prevPanels.has(id)) {
1007
1262
  return prevPanels;
@@ -1038,7 +1293,10 @@ function PanelGroupWithForwardedRef({
1038
1293
  }
1039
1294
  const size = isHorizontal ? rect.width : rect.height;
1040
1295
  const delta = movement / size * 100;
1041
- const nextSizes = adjustByDelta(event, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1296
+
1297
+ // If a validateLayout method has been provided
1298
+ // it's important to use it before updating the mouse cursor
1299
+ const nextSizes = adjustByDelta(event, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, initialDragStateRef.current);
1042
1300
  const sizesChanged = !areEqual(prevSizes, nextSizes);
1043
1301
 
1044
1302
  // Don't update cursor for resizes triggered by keyboard interactions.
@@ -1065,6 +1323,8 @@ function PanelGroupWithForwardedRef({
1065
1323
  }
1066
1324
  if (sizesChanged) {
1067
1325
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1326
+
1327
+ // It's okay to bypass in this case because we already validated above
1068
1328
  setSizes(nextSizes);
1069
1329
 
1070
1330
  // If resize change handlers have been declared, this is the time to call them.
@@ -1118,7 +1378,7 @@ function PanelGroupWithForwardedRef({
1118
1378
  }
1119
1379
  const isLastPanel = index === panelsArray.length - 1;
1120
1380
  const delta = isLastPanel ? currentSize : collapsedSize - currentSize;
1121
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1381
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1122
1382
  if (prevSizes !== nextSizes) {
1123
1383
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1124
1384
  setSizes(nextSizes);
@@ -1161,7 +1421,7 @@ function PanelGroupWithForwardedRef({
1161
1421
  }
1162
1422
  const isLastPanel = index === panelsArray.length - 1;
1163
1423
  const delta = isLastPanel ? collapsedSize - sizeBeforeCollapse : sizeBeforeCollapse;
1164
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1424
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1165
1425
  if (prevSizes !== nextSizes) {
1166
1426
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1167
1427
  setSizes(nextSizes);
@@ -1171,21 +1431,34 @@ function PanelGroupWithForwardedRef({
1171
1431
  callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap);
1172
1432
  }
1173
1433
  }, []);
1174
- const resizePanel = useCallback((id, nextSize) => {
1434
+ const resizePanel = useCallback((id, nextSize, unitsFromParams) => {
1175
1435
  const {
1436
+ id: groupId,
1176
1437
  panels,
1177
- sizes: prevSizes
1438
+ sizes: prevSizes,
1439
+ units
1178
1440
  } = committedValuesRef.current;
1441
+ if ((unitsFromParams || units) === "pixels") {
1442
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1443
+ nextSize = nextSize / groupSizePixels * 100;
1444
+ }
1179
1445
  const panel = panels.get(id);
1180
1446
  if (panel == null) {
1181
1447
  return;
1182
1448
  }
1183
- const {
1449
+ let {
1184
1450
  collapsedSize,
1185
1451
  collapsible,
1186
1452
  maxSize,
1187
1453
  minSize
1188
1454
  } = panel.current;
1455
+ if (units === "pixels") {
1456
+ const groupSizePixels = getAvailableGroupSizePixels(groupId);
1457
+ minSize = minSize / groupSizePixels * 100;
1458
+ if (maxSize != null) {
1459
+ maxSize = maxSize / groupSizePixels * 100;
1460
+ }
1461
+ }
1189
1462
  const panelsArray = panelsMapToSortedArray(panels);
1190
1463
  const index = panelsArray.indexOf(panel);
1191
1464
  if (index < 0) {
@@ -1196,7 +1469,7 @@ function PanelGroupWithForwardedRef({
1196
1469
  return;
1197
1470
  }
1198
1471
  if (collapsible && nextSize === collapsedSize) ; else {
1199
- nextSize = Math.min(maxSize, Math.max(minSize, nextSize));
1472
+ nextSize = Math.min(maxSize != null ? maxSize : 100, Math.max(minSize, nextSize));
1200
1473
  }
1201
1474
  const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray);
1202
1475
  if (idBefore == null || idAfter == null) {
@@ -1204,7 +1477,7 @@ function PanelGroupWithForwardedRef({
1204
1477
  }
1205
1478
  const isLastPanel = index === panelsArray.length - 1;
1206
1479
  const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize;
1207
- const nextSizes = adjustByDelta(null, panels, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1480
+ const nextSizes = adjustByDelta(null, committedValuesRef.current, idBefore, idAfter, delta, prevSizes, panelSizeBeforeCollapse.current, null);
1208
1481
  if (prevSizes !== nextSizes) {
1209
1482
  const panelIdToLastNotifiedSizeMap = panelIdToLastNotifiedSizeMapRef.current;
1210
1483
  setSizes(nextSizes);
@@ -1219,6 +1492,7 @@ function PanelGroupWithForwardedRef({
1219
1492
  collapsePanel,
1220
1493
  direction,
1221
1494
  expandPanel,
1495
+ getPanelSize,
1222
1496
  getPanelStyle,
1223
1497
  groupId,
1224
1498
  registerPanel,
@@ -1240,8 +1514,9 @@ function PanelGroupWithForwardedRef({
1240
1514
  setActiveHandleId(null);
1241
1515
  initialDragStateRef.current = null;
1242
1516
  },
1517
+ units,
1243
1518
  unregisterPanel
1244
- }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, unregisterPanel]);
1519
+ }), [activeHandleId, collapsePanel, direction, expandPanel, getPanelSize, getPanelStyle, groupId, registerPanel, registerResizeHandle, resizePanel, units, unregisterPanel]);
1245
1520
  const style = {
1246
1521
  display: "flex",
1247
1522
  flexDirection: direction === "horizontal" ? "row" : "column",
@@ -1256,6 +1531,7 @@ function PanelGroupWithForwardedRef({
1256
1531
  "data-panel-group": "",
1257
1532
  "data-panel-group-direction": direction,
1258
1533
  "data-panel-group-id": groupId,
1534
+ "data-panel-group-units": units,
1259
1535
  style: {
1260
1536
  ...style,
1261
1537
  ...styleFromProps
@@ -1268,10 +1544,6 @@ const PanelGroup = forwardRef((props, ref) => createElement(PanelGroupWithForwar
1268
1544
  ...props,
1269
1545
  forwardedRef: ref
1270
1546
  }));
1271
-
1272
- // Workaround for Parcel scope hoisting (which renames objects/functions).
1273
- // Casting to :any is required to avoid corrupting the generated TypeScript types.
1274
- // See github.com/parcel-bundler/parcel/issues/8724
1275
1547
  PanelGroupWithForwardedRef.displayName = "PanelGroup";
1276
1548
  PanelGroup.displayName = "forwardRef(PanelGroup)";
1277
1549
 
@@ -1407,10 +1679,6 @@ function PanelResizeHandle({
1407
1679
  tabIndex: 0
1408
1680
  });
1409
1681
  }
1410
-
1411
- // Workaround for Parcel scope hoisting (which renames objects/functions).
1412
- // Casting to :any is required to avoid corrupting the generated TypeScript types.
1413
- // See github.com/parcel-bundler/parcel/issues/8724
1414
1682
  PanelResizeHandle.displayName = "PanelResizeHandle";
1415
1683
 
1416
- export { Panel, PanelGroup, PanelResizeHandle };
1684
+ export { Panel, PanelGroup, PanelResizeHandle, getAvailableGroupSizePixels };