react-resizable-panels 0.0.59 → 0.0.61

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.61
4
+
5
+ - Better unstable Offscreen/Activity API.
6
+
7
+ ## 0.0.60
8
+
9
+ - Better support imperative API usage from mount effects.
10
+ - Better support strict effects mode.
11
+ - Better checks not to call `onResize` or `onLayout` more than once.
12
+
3
13
  ## 0.0.59
4
14
 
5
15
  - Support imperative panel API usage on-mount.
@@ -11,7 +11,7 @@ export type PanelGroupStorage = {
11
11
  };
12
12
  export type PanelGroupOnLayout = (layout: MixedSizes[]) => void;
13
13
  export type PanelGroupProps = PropsWithChildren<{
14
- autoSaveId?: string;
14
+ autoSaveId?: string | null;
15
15
  className?: string;
16
16
  dataAttributes?: DataAttributes;
17
17
  direction: Direction;
@@ -24,7 +24,7 @@ export type PanelGroupProps = PropsWithChildren<{
24
24
  tagName?: ElementType;
25
25
  }>;
26
26
  export declare const PanelGroup: import("react").ForwardRefExoticComponent<{
27
- autoSaveId?: string | undefined;
27
+ autoSaveId?: string | null | undefined;
28
28
  className?: string | undefined;
29
29
  dataAttributes?: DataAttributes | undefined;
30
30
  direction: Direction;
@@ -93,6 +93,7 @@ function PanelWithForwardedRef({
93
93
  expandPanel,
94
94
  getPanelSize,
95
95
  getPanelStyle,
96
+ groupId,
96
97
  isPanelCollapsed,
97
98
  registerPanel,
98
99
  resizePanel,
@@ -186,6 +187,7 @@ function PanelWithForwardedRef({
186
187
  // CSS selectors
187
188
  "data-panel": "",
188
189
  "data-panel-id": panelId,
190
+ "data-panel-group-id": groupId,
189
191
  // e2e test attributes
190
192
  "data-panel-collapsible": undefined,
191
193
  "data-panel-size": undefined
@@ -709,6 +711,7 @@ function getResizeHandlePanelIds(groupId, handleId, panelsArray) {
709
711
 
710
712
  function useWindowSplitterPanelGroupBehavior({
711
713
  committedValuesRef,
714
+ eagerValuesRef,
712
715
  groupId,
713
716
  layout,
714
717
  panelDataArray,
@@ -751,7 +754,7 @@ function useWindowSplitterPanelGroupBehavior({
751
754
  useEffect(() => {
752
755
  const {
753
756
  panelDataArray
754
- } = committedValuesRef.current;
757
+ } = eagerValuesRef.current;
755
758
  const groupElement = getPanelGroupElement(groupId);
756
759
  assert(groupElement != null, `No group found for id "${groupId}"`);
757
760
  const handles = getResizeHandleElementsForGroup(groupId);
@@ -809,7 +812,7 @@ function useWindowSplitterPanelGroupBehavior({
809
812
  return () => {
810
813
  cleanupFunctions.forEach(cleanupFunction => cleanupFunction());
811
814
  };
812
- }, [committedValuesRef, groupId, layout, panelDataArray, setLayout]);
815
+ }, [committedValuesRef, eagerValuesRef, groupId, layout, panelDataArray, setLayout]);
813
816
  }
814
817
 
815
818
  function areEqual(arrayA, arrayB) {
@@ -1094,6 +1097,10 @@ function debounce(callback, durationMs = 10) {
1094
1097
  return callable;
1095
1098
  }
1096
1099
 
1100
+ function getPanelElementsForGroup(groupId) {
1101
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1102
+ }
1103
+
1097
1104
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1098
1105
  // or on a browser with cookies/storage disabled.
1099
1106
  // In either case, this function avoids accessing localStorage until needed,
@@ -1243,7 +1250,7 @@ const defaultStorage = {
1243
1250
  };
1244
1251
  const debounceMap = {};
1245
1252
  function PanelGroupWithForwardedRef({
1246
- autoSaveId,
1253
+ autoSaveId = null,
1247
1254
  children,
1248
1255
  className: classNameFromProps = "",
1249
1256
  dataAttributes,
@@ -1260,20 +1267,22 @@ function PanelGroupWithForwardedRef({
1260
1267
  const groupId = useUniqueId(idFromProps);
1261
1268
  const [dragState, setDragState] = useState(null);
1262
1269
  const [layout, setLayout] = useState([]);
1263
- const [panelDataArray, setPanelDataArray] = useState([]);
1264
1270
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1265
1271
  const panelSizeBeforeCollapseRef = useRef(new Map());
1266
1272
  const prevDeltaRef = useRef(0);
1267
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1268
1273
  const committedValuesRef = useRef({
1274
+ autoSaveId,
1269
1275
  direction,
1270
1276
  dragState,
1271
1277
  id: groupId,
1272
1278
  keyboardResizeByPercentage,
1273
1279
  keyboardResizeByPixels,
1274
- layout,
1275
1280
  onLayout,
1276
- panelDataArray
1281
+ storage
1282
+ });
1283
+ const eagerValuesRef = useRef({
1284
+ layout,
1285
+ panelDataArray: []
1277
1286
  });
1278
1287
  useRef({
1279
1288
  didLogIdAndOrderWarning: false,
@@ -1284,9 +1293,11 @@ function PanelGroupWithForwardedRef({
1284
1293
  getId: () => committedValuesRef.current.id,
1285
1294
  getLayout: () => {
1286
1295
  const {
1287
- id: groupId,
1288
- layout
1296
+ id: groupId
1289
1297
  } = committedValuesRef.current;
1298
+ const {
1299
+ layout
1300
+ } = eagerValuesRef.current;
1290
1301
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1291
1302
  return layout.map(sizePercentage => {
1292
1303
  return {
@@ -1298,10 +1309,12 @@ function PanelGroupWithForwardedRef({
1298
1309
  setLayout: mixedSizes => {
1299
1310
  const {
1300
1311
  id: groupId,
1312
+ onLayout
1313
+ } = committedValuesRef.current;
1314
+ const {
1301
1315
  layout: prevLayout,
1302
- onLayout,
1303
1316
  panelDataArray
1304
- } = committedValuesRef.current;
1317
+ } = eagerValuesRef.current;
1305
1318
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1306
1319
  const unsafeLayout = mixedSizes.map(mixedSize => getPercentageSizeFromMixedSizes(mixedSize, groupSizePixels));
1307
1320
  const safeLayout = validatePanelGroupLayout({
@@ -1311,6 +1324,7 @@ function PanelGroupWithForwardedRef({
1311
1324
  });
1312
1325
  if (!areEqual(prevLayout, safeLayout)) {
1313
1326
  setLayout(safeLayout);
1327
+ eagerValuesRef.current.layout = safeLayout;
1314
1328
  if (onLayout) {
1315
1329
  onLayout(safeLayout.map(sizePercentage => ({
1316
1330
  sizePercentage,
@@ -1322,21 +1336,30 @@ function PanelGroupWithForwardedRef({
1322
1336
  }
1323
1337
  }), []);
1324
1338
  useIsomorphicLayoutEffect(() => {
1339
+ committedValuesRef.current.autoSaveId = autoSaveId;
1325
1340
  committedValuesRef.current.direction = direction;
1326
1341
  committedValuesRef.current.dragState = dragState;
1327
1342
  committedValuesRef.current.id = groupId;
1328
- committedValuesRef.current.layout = layout;
1329
1343
  committedValuesRef.current.onLayout = onLayout;
1330
- committedValuesRef.current.panelDataArray = panelDataArray;
1344
+ committedValuesRef.current.storage = storage;
1345
+
1346
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
1347
+ // TODO [217] Move these values into a separate ref
1331
1348
  });
1349
+
1332
1350
  useWindowSplitterPanelGroupBehavior({
1333
1351
  committedValuesRef,
1352
+ eagerValuesRef,
1334
1353
  groupId,
1335
1354
  layout,
1336
- panelDataArray,
1355
+ panelDataArray: eagerValuesRef.current.panelDataArray,
1337
1356
  setLayout
1338
1357
  });
1339
1358
  useEffect(() => {
1359
+ const {
1360
+ panelDataArray
1361
+ } = eagerValuesRef.current;
1362
+
1340
1363
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1341
1364
  if (autoSaveId) {
1342
1365
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1349,63 +1372,12 @@ function PanelGroupWithForwardedRef({
1349
1372
  }
1350
1373
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1351
1374
  }
1352
- }, [autoSaveId, layout, panelDataArray, storage]);
1353
-
1354
- // Once all panels have registered themselves,
1355
- // Compute the initial sizes based on default weights.
1356
- // This assumes that panels register during initial mount (no conditional rendering)!
1375
+ }, [autoSaveId, layout, storage]);
1357
1376
  useIsomorphicLayoutEffect(() => {
1358
1377
  const {
1359
- id: groupId,
1360
- layout,
1361
- onLayout
1362
- } = committedValuesRef.current;
1363
- if (layout.length === panelDataArray.length) {
1364
- // Only compute (or restore) default layout once per panel configuration.
1365
- return;
1366
- }
1367
-
1368
- // If this panel has been configured to persist sizing information,
1369
- // default size should be restored from local storage if possible.
1370
- let unsafeLayout = null;
1371
- if (autoSaveId) {
1372
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1373
- }
1374
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1375
- if (groupSizePixels <= 0) {
1376
- if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1377
- constraints
1378
- }) => constraints))) {
1379
- // Wait until the group has rendered a non-zero size before computing layout.
1380
- return;
1381
- }
1382
- }
1383
- if (unsafeLayout == null) {
1384
- unsafeLayout = calculateUnsafeDefaultLayout({
1385
- groupSizePixels,
1386
- panelDataArray
1387
- });
1388
- }
1389
-
1390
- // Validate even saved layouts in case something has changed since last render
1391
- // e.g. for pixel groups, this could be the size of the window
1392
- const validatedLayout = validatePanelGroupLayout({
1393
- groupSizePixels,
1394
- layout: unsafeLayout,
1395
- panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1396
- });
1397
- if (!areEqual(layout, validatedLayout)) {
1398
- setLayout(validatedLayout);
1399
- }
1400
- if (onLayout) {
1401
- onLayout(validatedLayout.map(sizePercentage => ({
1402
- sizePercentage,
1403
- sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1404
- })));
1405
- }
1406
- callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1407
- }, [autoSaveId, layout, panelDataArray, storage]);
1408
- useIsomorphicLayoutEffect(() => {
1378
+ layout: prevLayout,
1379
+ panelDataArray
1380
+ } = eagerValuesRef.current;
1409
1381
  const constraints = panelDataArray.map(({
1410
1382
  constraints
1411
1383
  }) => constraints);
@@ -1419,7 +1391,6 @@ function PanelGroupWithForwardedRef({
1419
1391
  const resizeObserver = new ResizeObserver(() => {
1420
1392
  const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1421
1393
  const {
1422
- layout: prevLayout,
1423
1394
  onLayout
1424
1395
  } = committedValuesRef.current;
1425
1396
  const nextLayout = validatePanelGroupLayout({
@@ -1429,6 +1400,7 @@ function PanelGroupWithForwardedRef({
1429
1400
  });
1430
1401
  if (!areEqual(prevLayout, nextLayout)) {
1431
1402
  setLayout(nextLayout);
1403
+ eagerValuesRef.current.layout = nextLayout;
1432
1404
  if (onLayout) {
1433
1405
  onLayout(nextLayout.map(sizePercentage => ({
1434
1406
  sizePercentage,
@@ -1443,7 +1415,7 @@ function PanelGroupWithForwardedRef({
1443
1415
  resizeObserver.disconnect();
1444
1416
  };
1445
1417
  }
1446
- }, [groupId, panelDataArray]);
1418
+ }, [groupId]);
1447
1419
 
1448
1420
  // DEV warnings
1449
1421
  useEffect(() => {
@@ -1451,22 +1423,13 @@ function PanelGroupWithForwardedRef({
1451
1423
 
1452
1424
  // External APIs are safe to memoize via committed values ref
1453
1425
  const collapsePanel = useCallback(panelData => {
1426
+ const {
1427
+ onLayout
1428
+ } = committedValuesRef.current;
1454
1429
  const {
1455
1430
  layout: prevLayout,
1456
- onLayout,
1457
1431
  panelDataArray
1458
- } = committedValuesRef.current;
1459
-
1460
- // See issues/211
1461
- if (panelDataArray.find(({
1462
- id
1463
- }) => id === panelData.id) == null) {
1464
- setImperativeApiQueue(prev => [...prev, {
1465
- panelData,
1466
- type: "collapse"
1467
- }]);
1468
- return;
1469
- }
1432
+ } = eagerValuesRef.current;
1470
1433
  if (panelData.constraints.collapsible) {
1471
1434
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1472
1435
  const {
@@ -1491,6 +1454,7 @@ function PanelGroupWithForwardedRef({
1491
1454
  });
1492
1455
  if (!compareLayouts(prevLayout, nextLayout)) {
1493
1456
  setLayout(nextLayout);
1457
+ eagerValuesRef.current.layout = nextLayout;
1494
1458
  if (onLayout) {
1495
1459
  onLayout(nextLayout.map(sizePercentage => ({
1496
1460
  sizePercentage,
@@ -1505,22 +1469,13 @@ function PanelGroupWithForwardedRef({
1505
1469
 
1506
1470
  // External APIs are safe to memoize via committed values ref
1507
1471
  const expandPanel = useCallback(panelData => {
1472
+ const {
1473
+ onLayout
1474
+ } = committedValuesRef.current;
1508
1475
  const {
1509
1476
  layout: prevLayout,
1510
- onLayout,
1511
1477
  panelDataArray
1512
- } = committedValuesRef.current;
1513
-
1514
- // See issues/211
1515
- if (panelDataArray.find(({
1516
- id
1517
- }) => id === panelData.id) == null) {
1518
- setImperativeApiQueue(prev => [...prev, {
1519
- panelData,
1520
- type: "expand"
1521
- }]);
1522
- return;
1523
- }
1478
+ } = eagerValuesRef.current;
1524
1479
  if (panelData.constraints.collapsible) {
1525
1480
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1526
1481
  const {
@@ -1546,6 +1501,7 @@ function PanelGroupWithForwardedRef({
1546
1501
  });
1547
1502
  if (!compareLayouts(prevLayout, nextLayout)) {
1548
1503
  setLayout(nextLayout);
1504
+ eagerValuesRef.current.layout = nextLayout;
1549
1505
  if (onLayout) {
1550
1506
  onLayout(nextLayout.map(sizePercentage => ({
1551
1507
  sizePercentage,
@@ -1563,7 +1519,7 @@ function PanelGroupWithForwardedRef({
1563
1519
  const {
1564
1520
  layout,
1565
1521
  panelDataArray
1566
- } = committedValuesRef.current;
1522
+ } = eagerValuesRef.current;
1567
1523
  const {
1568
1524
  panelSizePercentage,
1569
1525
  panelSizePixels
@@ -1576,6 +1532,9 @@ function PanelGroupWithForwardedRef({
1576
1532
 
1577
1533
  // This API should never read from committedValuesRef
1578
1534
  const getPanelStyle = useCallback(panelData => {
1535
+ const {
1536
+ panelDataArray
1537
+ } = eagerValuesRef.current;
1579
1538
  const panelIndex = panelDataArray.indexOf(panelData);
1580
1539
  return computePanelFlexBoxStyle({
1581
1540
  dragState,
@@ -1583,14 +1542,14 @@ function PanelGroupWithForwardedRef({
1583
1542
  panelData: panelDataArray,
1584
1543
  panelIndex
1585
1544
  });
1586
- }, [dragState, layout, panelDataArray]);
1545
+ }, [dragState, layout]);
1587
1546
 
1588
1547
  // External APIs are safe to memoize via committed values ref
1589
1548
  const isPanelCollapsed = useCallback(panelData => {
1590
1549
  const {
1591
1550
  layout,
1592
1551
  panelDataArray
1593
- } = committedValuesRef.current;
1552
+ } = eagerValuesRef.current;
1594
1553
  const {
1595
1554
  collapsedSizePercentage,
1596
1555
  collapsible,
@@ -1604,7 +1563,7 @@ function PanelGroupWithForwardedRef({
1604
1563
  const {
1605
1564
  layout,
1606
1565
  panelDataArray
1607
- } = committedValuesRef.current;
1566
+ } = eagerValuesRef.current;
1608
1567
  const {
1609
1568
  collapsedSizePercentage,
1610
1569
  collapsible,
@@ -1613,22 +1572,82 @@ function PanelGroupWithForwardedRef({
1613
1572
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1614
1573
  }, [groupId]);
1615
1574
  const registerPanel = useCallback(panelData => {
1616
- setPanelDataArray(prevPanelDataArray => {
1617
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1618
- return nextPanelDataArray.sort((panelA, panelB) => {
1619
- const orderA = panelA.order;
1620
- const orderB = panelB.order;
1621
- if (orderA == null && orderB == null) {
1622
- return 0;
1623
- } else if (orderA == null) {
1624
- return -1;
1625
- } else if (orderB == null) {
1626
- return 1;
1627
- } else {
1628
- return orderA - orderB;
1629
- }
1575
+ const {
1576
+ autoSaveId,
1577
+ id: groupId,
1578
+ onLayout,
1579
+ storage
1580
+ } = committedValuesRef.current;
1581
+ const {
1582
+ layout: prevLayout,
1583
+ panelDataArray
1584
+ } = eagerValuesRef.current;
1585
+ panelDataArray.push(panelData);
1586
+ panelDataArray.sort((panelA, panelB) => {
1587
+ const orderA = panelA.order;
1588
+ const orderB = panelB.order;
1589
+ if (orderA == null && orderB == null) {
1590
+ return 0;
1591
+ } else if (orderA == null) {
1592
+ return -1;
1593
+ } else if (orderB == null) {
1594
+ return 1;
1595
+ } else {
1596
+ return orderA - orderB;
1597
+ }
1598
+ });
1599
+
1600
+ // Wait until all panels have registered before we try to compute layout;
1601
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1602
+ const panelElements = getPanelElementsForGroup(groupId);
1603
+ if (panelElements.length !== panelDataArray.length) {
1604
+ return;
1605
+ }
1606
+
1607
+ // If this panel has been configured to persist sizing information,
1608
+ // default size should be restored from local storage if possible.
1609
+ let unsafeLayout = null;
1610
+ if (autoSaveId) {
1611
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1612
+ }
1613
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1614
+ if (groupSizePixels <= 0) {
1615
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1616
+ constraints
1617
+ }) => constraints))) {
1618
+ // Wait until the group has rendered a non-zero size before computing layout.
1619
+ return;
1620
+ }
1621
+ }
1622
+ if (unsafeLayout == null) {
1623
+ unsafeLayout = calculateUnsafeDefaultLayout({
1624
+ groupSizePixels,
1625
+ panelDataArray
1630
1626
  });
1627
+ }
1628
+
1629
+ // Validate even saved layouts in case something has changed since last render
1630
+ // e.g. for pixel groups, this could be the size of the window
1631
+ const nextLayout = validatePanelGroupLayout({
1632
+ groupSizePixels,
1633
+ layout: unsafeLayout,
1634
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1631
1635
  });
1636
+
1637
+ // Offscreen mode makes this a bit weird;
1638
+ // Panels unregister when hidden and re-register when shown again,
1639
+ // but the overall layout doesn't change between these two cases.
1640
+ setLayout(nextLayout);
1641
+ eagerValuesRef.current.layout = nextLayout;
1642
+ if (!areEqual(prevLayout, nextLayout)) {
1643
+ if (onLayout) {
1644
+ onLayout(nextLayout.map(sizePercentage => ({
1645
+ sizePercentage,
1646
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1647
+ })));
1648
+ }
1649
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1650
+ }
1632
1651
  }, []);
1633
1652
  const registerResizeHandle = useCallback(dragHandleId => {
1634
1653
  return function resizeHandler(event) {
@@ -1639,10 +1658,12 @@ function PanelGroupWithForwardedRef({
1639
1658
  id: groupId,
1640
1659
  keyboardResizeByPercentage,
1641
1660
  keyboardResizeByPixels,
1642
- onLayout,
1643
- panelDataArray,
1644
- layout: prevLayout
1661
+ onLayout
1645
1662
  } = committedValuesRef.current;
1663
+ const {
1664
+ layout: prevLayout,
1665
+ panelDataArray
1666
+ } = eagerValuesRef.current;
1646
1667
  const {
1647
1668
  initialLayout
1648
1669
  } = dragState !== null && dragState !== void 0 ? dragState : {};
@@ -1698,6 +1719,7 @@ function PanelGroupWithForwardedRef({
1698
1719
  }
1699
1720
  if (layoutChanged) {
1700
1721
  setLayout(nextLayout);
1722
+ eagerValuesRef.current.layout = nextLayout;
1701
1723
  if (onLayout) {
1702
1724
  onLayout(nextLayout.map(sizePercentage => ({
1703
1725
  sizePercentage,
@@ -1711,23 +1733,13 @@ function PanelGroupWithForwardedRef({
1711
1733
 
1712
1734
  // External APIs are safe to memoize via committed values ref
1713
1735
  const resizePanel = useCallback((panelData, mixedSizes) => {
1736
+ const {
1737
+ onLayout
1738
+ } = committedValuesRef.current;
1714
1739
  const {
1715
1740
  layout: prevLayout,
1716
- onLayout,
1717
1741
  panelDataArray
1718
- } = committedValuesRef.current;
1719
-
1720
- // See issues/211
1721
- if (panelDataArray.find(({
1722
- id
1723
- }) => id === panelData.id) == null) {
1724
- setImperativeApiQueue(prev => [...prev, {
1725
- panelData,
1726
- mixedSizes,
1727
- type: "resize"
1728
- }]);
1729
- return;
1730
- }
1742
+ } = eagerValuesRef.current;
1731
1743
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1732
1744
  const {
1733
1745
  groupSizePixels,
@@ -1747,6 +1759,7 @@ function PanelGroupWithForwardedRef({
1747
1759
  });
1748
1760
  if (!compareLayouts(prevLayout, nextLayout)) {
1749
1761
  setLayout(nextLayout);
1762
+ eagerValuesRef.current.layout = nextLayout;
1750
1763
  if (onLayout) {
1751
1764
  onLayout(nextLayout.map(sizePercentage => ({
1752
1765
  sizePercentage,
@@ -1758,9 +1771,11 @@ function PanelGroupWithForwardedRef({
1758
1771
  }, [groupId]);
1759
1772
  const startDragging = useCallback((dragHandleId, event) => {
1760
1773
  const {
1761
- direction,
1762
- layout
1774
+ direction
1763
1775
  } = committedValuesRef.current;
1776
+ const {
1777
+ layout
1778
+ } = eagerValuesRef.current;
1764
1779
  const handleElement = getResizeHandleElement(dragHandleId);
1765
1780
  const initialCursorPosition = getResizeEventCursorPosition(direction, event);
1766
1781
  setDragState({
@@ -1774,42 +1789,87 @@ function PanelGroupWithForwardedRef({
1774
1789
  resetGlobalCursorStyle();
1775
1790
  setDragState(null);
1776
1791
  }, []);
1792
+ const unregisterPanelRef = useRef({
1793
+ pendingPanelIds: new Set(),
1794
+ timeout: null
1795
+ });
1777
1796
  const unregisterPanel = useCallback(panelData => {
1778
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1779
- setPanelDataArray(panelDataArray => {
1780
- const index = panelDataArray.indexOf(panelData);
1781
- if (index >= 0) {
1782
- panelDataArray = [...panelDataArray];
1783
- panelDataArray.splice(index, 1);
1797
+ const {
1798
+ id: groupId,
1799
+ onLayout
1800
+ } = committedValuesRef.current;
1801
+ const {
1802
+ layout: prevLayout,
1803
+ panelDataArray
1804
+ } = eagerValuesRef.current;
1805
+ const index = panelDataArray.indexOf(panelData);
1806
+ if (index >= 0) {
1807
+ panelDataArray.splice(index, 1);
1808
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1809
+ }
1810
+ if (unregisterPanelRef.current.timeout != null) {
1811
+ clearTimeout(unregisterPanelRef.current.timeout);
1812
+ }
1813
+
1814
+ // Batch panel unmounts so that we only calculate layout once;
1815
+ // This is more efficient and avoids misleading warnings in development mode.
1816
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1817
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1818
+ const {
1819
+ pendingPanelIds
1820
+ } = unregisterPanelRef.current;
1821
+ const map = panelIdToLastNotifiedMixedSizesMapRef.current;
1822
+
1823
+ // TRICKY
1824
+ // Strict effects mode
1825
+ let unmountDueToStrictMode = false;
1826
+ pendingPanelIds.forEach(panelId => {
1827
+ pendingPanelIds.delete(panelId);
1828
+ if (panelDataArray.find(({
1829
+ id
1830
+ }) => id === panelId) == null) {
1831
+ unmountDueToStrictMode = true;
1832
+
1833
+ // TRICKY
1834
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1835
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1836
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1837
+ delete map[panelData.id];
1838
+ }
1839
+ });
1840
+ if (!unmountDueToStrictMode) {
1841
+ return;
1784
1842
  }
1785
- return panelDataArray;
1786
- });
1787
- }, []);
1843
+ if (panelDataArray.length === 0) {
1844
+ // The group is unmounting; skip layout calculation.
1845
+ return;
1846
+ }
1847
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1848
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1849
+ groupSizePixels,
1850
+ panelDataArray
1851
+ });
1788
1852
 
1789
- // Handle imperative API calls that were made before panels were registered
1790
- useIsomorphicLayoutEffect(() => {
1791
- const queue = imperativeApiQueue;
1792
- while (queue.length > 0) {
1793
- const current = queue.shift();
1794
- switch (current.type) {
1795
- case "collapse":
1796
- {
1797
- collapsePanel(current.panelData);
1798
- break;
1799
- }
1800
- case "expand":
1801
- {
1802
- expandPanel(current.panelData);
1803
- break;
1804
- }
1805
- case "resize":
1806
- {
1807
- resizePanel(current.panelData, current.mixedSizes);
1808
- break;
1809
- }
1853
+ // Validate even saved layouts in case something has changed since last render
1854
+ // e.g. for pixel groups, this could be the size of the window
1855
+ const nextLayout = validatePanelGroupLayout({
1856
+ groupSizePixels,
1857
+ layout: unsafeLayout,
1858
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1859
+ });
1860
+ if (!areEqual(prevLayout, nextLayout)) {
1861
+ setLayout(nextLayout);
1862
+ eagerValuesRef.current.layout = nextLayout;
1863
+ if (onLayout) {
1864
+ onLayout(nextLayout.map(sizePercentage => ({
1865
+ sizePercentage,
1866
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1867
+ })));
1868
+ }
1869
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1810
1870
  }
1811
- }
1812
- }, [collapsePanel, expandPanel, imperativeApiQueue, layout, panelDataArray, resizePanel]);
1871
+ }, 0);
1872
+ }, []);
1813
1873
  const context = useMemo(() => ({
1814
1874
  collapsePanel,
1815
1875
  direction,