react-resizable-panels 0.0.59 → 0.0.60

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.
@@ -95,6 +95,7 @@ function PanelWithForwardedRef({
95
95
  expandPanel,
96
96
  getPanelSize,
97
97
  getPanelStyle,
98
+ groupId,
98
99
  isPanelCollapsed,
99
100
  registerPanel,
100
101
  resizePanel,
@@ -188,6 +189,7 @@ function PanelWithForwardedRef({
188
189
  // CSS selectors
189
190
  "data-panel": "",
190
191
  "data-panel-id": panelId,
192
+ "data-panel-group-id": groupId,
191
193
  // e2e test attributes
192
194
  "data-panel-collapsible": undefined,
193
195
  "data-panel-size": undefined
@@ -1096,6 +1098,10 @@ function debounce(callback, durationMs = 10) {
1096
1098
  return callable;
1097
1099
  }
1098
1100
 
1101
+ function getPanelElementsForGroup(groupId) {
1102
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1103
+ }
1104
+
1099
1105
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1100
1106
  // or on a browser with cookies/storage disabled.
1101
1107
  // In either case, this function avoids accessing localStorage until needed,
@@ -1245,7 +1251,7 @@ const defaultStorage = {
1245
1251
  };
1246
1252
  const debounceMap = {};
1247
1253
  function PanelGroupWithForwardedRef({
1248
- autoSaveId,
1254
+ autoSaveId = null,
1249
1255
  children,
1250
1256
  className: classNameFromProps = "",
1251
1257
  dataAttributes,
@@ -1262,12 +1268,11 @@ function PanelGroupWithForwardedRef({
1262
1268
  const groupId = useUniqueId(idFromProps);
1263
1269
  const [dragState, setDragState] = useState(null);
1264
1270
  const [layout, setLayout] = useState([]);
1265
- const [panelDataArray, setPanelDataArray] = useState([]);
1266
1271
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1267
1272
  const panelSizeBeforeCollapseRef = useRef(new Map());
1268
1273
  const prevDeltaRef = useRef(0);
1269
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1270
1274
  const committedValuesRef = useRef({
1275
+ autoSaveId,
1271
1276
  direction,
1272
1277
  dragState,
1273
1278
  id: groupId,
@@ -1275,7 +1280,8 @@ function PanelGroupWithForwardedRef({
1275
1280
  keyboardResizeByPixels,
1276
1281
  layout,
1277
1282
  onLayout,
1278
- panelDataArray
1283
+ panelDataArray: [],
1284
+ storage
1279
1285
  });
1280
1286
  useRef({
1281
1287
  didLogIdAndOrderWarning: false,
@@ -1313,6 +1319,7 @@ function PanelGroupWithForwardedRef({
1313
1319
  });
1314
1320
  if (!areEqual(prevLayout, safeLayout)) {
1315
1321
  setLayout(safeLayout);
1322
+ committedValuesRef.current.layout = safeLayout;
1316
1323
  if (onLayout) {
1317
1324
  onLayout(safeLayout.map(sizePercentage => ({
1318
1325
  sizePercentage,
@@ -1324,21 +1331,29 @@ function PanelGroupWithForwardedRef({
1324
1331
  }
1325
1332
  }), []);
1326
1333
  useIsomorphicLayoutEffect(() => {
1334
+ committedValuesRef.current.autoSaveId = autoSaveId;
1327
1335
  committedValuesRef.current.direction = direction;
1328
1336
  committedValuesRef.current.dragState = dragState;
1329
1337
  committedValuesRef.current.id = groupId;
1330
- committedValuesRef.current.layout = layout;
1331
1338
  committedValuesRef.current.onLayout = onLayout;
1332
- committedValuesRef.current.panelDataArray = panelDataArray;
1339
+ committedValuesRef.current.storage = storage;
1340
+
1341
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
1342
+ // TODO [217] Move these values into a separate ref
1333
1343
  });
1344
+
1334
1345
  useWindowSplitterPanelGroupBehavior({
1335
1346
  committedValuesRef,
1336
1347
  groupId,
1337
1348
  layout,
1338
- panelDataArray,
1349
+ panelDataArray: committedValuesRef.current.panelDataArray,
1339
1350
  setLayout
1340
1351
  });
1341
1352
  useEffect(() => {
1353
+ const {
1354
+ panelDataArray
1355
+ } = committedValuesRef.current;
1356
+
1342
1357
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1343
1358
  if (autoSaveId) {
1344
1359
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1351,63 +1366,11 @@ function PanelGroupWithForwardedRef({
1351
1366
  }
1352
1367
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1353
1368
  }
1354
- }, [autoSaveId, layout, panelDataArray, storage]);
1355
-
1356
- // Once all panels have registered themselves,
1357
- // Compute the initial sizes based on default weights.
1358
- // This assumes that panels register during initial mount (no conditional rendering)!
1369
+ }, [autoSaveId, layout, storage]);
1359
1370
  useIsomorphicLayoutEffect(() => {
1360
1371
  const {
1361
- id: groupId,
1362
- layout,
1363
- onLayout
1372
+ panelDataArray
1364
1373
  } = committedValuesRef.current;
1365
- if (layout.length === panelDataArray.length) {
1366
- // Only compute (or restore) default layout once per panel configuration.
1367
- return;
1368
- }
1369
-
1370
- // If this panel has been configured to persist sizing information,
1371
- // default size should be restored from local storage if possible.
1372
- let unsafeLayout = null;
1373
- if (autoSaveId) {
1374
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1375
- }
1376
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1377
- if (groupSizePixels <= 0) {
1378
- if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1379
- constraints
1380
- }) => constraints))) {
1381
- // Wait until the group has rendered a non-zero size before computing layout.
1382
- return;
1383
- }
1384
- }
1385
- if (unsafeLayout == null) {
1386
- unsafeLayout = calculateUnsafeDefaultLayout({
1387
- groupSizePixels,
1388
- panelDataArray
1389
- });
1390
- }
1391
-
1392
- // Validate even saved layouts in case something has changed since last render
1393
- // e.g. for pixel groups, this could be the size of the window
1394
- const validatedLayout = validatePanelGroupLayout({
1395
- groupSizePixels,
1396
- layout: unsafeLayout,
1397
- panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1398
- });
1399
- if (!areEqual(layout, validatedLayout)) {
1400
- setLayout(validatedLayout);
1401
- }
1402
- if (onLayout) {
1403
- onLayout(validatedLayout.map(sizePercentage => ({
1404
- sizePercentage,
1405
- sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1406
- })));
1407
- }
1408
- callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1409
- }, [autoSaveId, layout, panelDataArray, storage]);
1410
- useIsomorphicLayoutEffect(() => {
1411
1374
  const constraints = panelDataArray.map(({
1412
1375
  constraints
1413
1376
  }) => constraints);
@@ -1431,6 +1394,7 @@ function PanelGroupWithForwardedRef({
1431
1394
  });
1432
1395
  if (!areEqual(prevLayout, nextLayout)) {
1433
1396
  setLayout(nextLayout);
1397
+ committedValuesRef.current.layout = nextLayout;
1434
1398
  if (onLayout) {
1435
1399
  onLayout(nextLayout.map(sizePercentage => ({
1436
1400
  sizePercentage,
@@ -1445,7 +1409,7 @@ function PanelGroupWithForwardedRef({
1445
1409
  resizeObserver.disconnect();
1446
1410
  };
1447
1411
  }
1448
- }, [groupId, panelDataArray]);
1412
+ }, [groupId]);
1449
1413
 
1450
1414
  // DEV warnings
1451
1415
  useEffect(() => {
@@ -1458,17 +1422,6 @@ function PanelGroupWithForwardedRef({
1458
1422
  onLayout,
1459
1423
  panelDataArray
1460
1424
  } = committedValuesRef.current;
1461
-
1462
- // See issues/211
1463
- if (panelDataArray.find(({
1464
- id
1465
- }) => id === panelData.id) == null) {
1466
- setImperativeApiQueue(prev => [...prev, {
1467
- panelData,
1468
- type: "collapse"
1469
- }]);
1470
- return;
1471
- }
1472
1425
  if (panelData.constraints.collapsible) {
1473
1426
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1474
1427
  const {
@@ -1493,6 +1446,7 @@ function PanelGroupWithForwardedRef({
1493
1446
  });
1494
1447
  if (!compareLayouts(prevLayout, nextLayout)) {
1495
1448
  setLayout(nextLayout);
1449
+ committedValuesRef.current.layout = nextLayout;
1496
1450
  if (onLayout) {
1497
1451
  onLayout(nextLayout.map(sizePercentage => ({
1498
1452
  sizePercentage,
@@ -1512,17 +1466,6 @@ function PanelGroupWithForwardedRef({
1512
1466
  onLayout,
1513
1467
  panelDataArray
1514
1468
  } = committedValuesRef.current;
1515
-
1516
- // See issues/211
1517
- if (panelDataArray.find(({
1518
- id
1519
- }) => id === panelData.id) == null) {
1520
- setImperativeApiQueue(prev => [...prev, {
1521
- panelData,
1522
- type: "expand"
1523
- }]);
1524
- return;
1525
- }
1526
1469
  if (panelData.constraints.collapsible) {
1527
1470
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1528
1471
  const {
@@ -1548,6 +1491,7 @@ function PanelGroupWithForwardedRef({
1548
1491
  });
1549
1492
  if (!compareLayouts(prevLayout, nextLayout)) {
1550
1493
  setLayout(nextLayout);
1494
+ committedValuesRef.current.layout = nextLayout;
1551
1495
  if (onLayout) {
1552
1496
  onLayout(nextLayout.map(sizePercentage => ({
1553
1497
  sizePercentage,
@@ -1578,6 +1522,9 @@ function PanelGroupWithForwardedRef({
1578
1522
 
1579
1523
  // This API should never read from committedValuesRef
1580
1524
  const getPanelStyle = useCallback(panelData => {
1525
+ const {
1526
+ panelDataArray
1527
+ } = committedValuesRef.current;
1581
1528
  const panelIndex = panelDataArray.indexOf(panelData);
1582
1529
  return computePanelFlexBoxStyle({
1583
1530
  dragState,
@@ -1585,7 +1532,7 @@ function PanelGroupWithForwardedRef({
1585
1532
  panelData: panelDataArray,
1586
1533
  panelIndex
1587
1534
  });
1588
- }, [dragState, layout, panelDataArray]);
1535
+ }, [dragState, layout]);
1589
1536
 
1590
1537
  // External APIs are safe to memoize via committed values ref
1591
1538
  const isPanelCollapsed = useCallback(panelData => {
@@ -1615,22 +1562,76 @@ function PanelGroupWithForwardedRef({
1615
1562
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1616
1563
  }, [groupId]);
1617
1564
  const registerPanel = useCallback(panelData => {
1618
- setPanelDataArray(prevPanelDataArray => {
1619
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1620
- return nextPanelDataArray.sort((panelA, panelB) => {
1621
- const orderA = panelA.order;
1622
- const orderB = panelB.order;
1623
- if (orderA == null && orderB == null) {
1624
- return 0;
1625
- } else if (orderA == null) {
1626
- return -1;
1627
- } else if (orderB == null) {
1628
- return 1;
1629
- } else {
1630
- return orderA - orderB;
1631
- }
1565
+ const {
1566
+ autoSaveId,
1567
+ id: groupId,
1568
+ layout: prevLayout,
1569
+ onLayout,
1570
+ panelDataArray,
1571
+ storage
1572
+ } = committedValuesRef.current;
1573
+ panelDataArray.push(panelData);
1574
+ panelDataArray.sort((panelA, panelB) => {
1575
+ const orderA = panelA.order;
1576
+ const orderB = panelB.order;
1577
+ if (orderA == null && orderB == null) {
1578
+ return 0;
1579
+ } else if (orderA == null) {
1580
+ return -1;
1581
+ } else if (orderB == null) {
1582
+ return 1;
1583
+ } else {
1584
+ return orderA - orderB;
1585
+ }
1586
+ });
1587
+
1588
+ // Wait until all panels have registered before we try to compute layout;
1589
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1590
+ const panelElements = getPanelElementsForGroup(groupId);
1591
+ if (panelElements.length !== panelDataArray.length) {
1592
+ return;
1593
+ }
1594
+
1595
+ // If this panel has been configured to persist sizing information,
1596
+ // default size should be restored from local storage if possible.
1597
+ let unsafeLayout = null;
1598
+ if (autoSaveId) {
1599
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1600
+ }
1601
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1602
+ if (groupSizePixels <= 0) {
1603
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1604
+ constraints
1605
+ }) => constraints))) {
1606
+ // Wait until the group has rendered a non-zero size before computing layout.
1607
+ return;
1608
+ }
1609
+ }
1610
+ if (unsafeLayout == null) {
1611
+ unsafeLayout = calculateUnsafeDefaultLayout({
1612
+ groupSizePixels,
1613
+ panelDataArray
1632
1614
  });
1615
+ }
1616
+
1617
+ // Validate even saved layouts in case something has changed since last render
1618
+ // e.g. for pixel groups, this could be the size of the window
1619
+ const nextLayout = validatePanelGroupLayout({
1620
+ groupSizePixels,
1621
+ layout: unsafeLayout,
1622
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1633
1623
  });
1624
+ if (!areEqual(prevLayout, nextLayout)) {
1625
+ setLayout(nextLayout);
1626
+ committedValuesRef.current.layout = nextLayout;
1627
+ if (onLayout) {
1628
+ onLayout(nextLayout.map(sizePercentage => ({
1629
+ sizePercentage,
1630
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1631
+ })));
1632
+ }
1633
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1634
+ }
1634
1635
  }, []);
1635
1636
  const registerResizeHandle = useCallback(dragHandleId => {
1636
1637
  return function resizeHandler(event) {
@@ -1700,6 +1701,7 @@ function PanelGroupWithForwardedRef({
1700
1701
  }
1701
1702
  if (layoutChanged) {
1702
1703
  setLayout(nextLayout);
1704
+ committedValuesRef.current.layout = nextLayout;
1703
1705
  if (onLayout) {
1704
1706
  onLayout(nextLayout.map(sizePercentage => ({
1705
1707
  sizePercentage,
@@ -1718,18 +1720,6 @@ function PanelGroupWithForwardedRef({
1718
1720
  onLayout,
1719
1721
  panelDataArray
1720
1722
  } = committedValuesRef.current;
1721
-
1722
- // See issues/211
1723
- if (panelDataArray.find(({
1724
- id
1725
- }) => id === panelData.id) == null) {
1726
- setImperativeApiQueue(prev => [...prev, {
1727
- panelData,
1728
- mixedSizes,
1729
- type: "resize"
1730
- }]);
1731
- return;
1732
- }
1733
1723
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1734
1724
  const {
1735
1725
  groupSizePixels,
@@ -1749,6 +1739,7 @@ function PanelGroupWithForwardedRef({
1749
1739
  });
1750
1740
  if (!compareLayouts(prevLayout, nextLayout)) {
1751
1741
  setLayout(nextLayout);
1742
+ committedValuesRef.current.layout = nextLayout;
1752
1743
  if (onLayout) {
1753
1744
  onLayout(nextLayout.map(sizePercentage => ({
1754
1745
  sizePercentage,
@@ -1776,42 +1767,85 @@ function PanelGroupWithForwardedRef({
1776
1767
  resetGlobalCursorStyle();
1777
1768
  setDragState(null);
1778
1769
  }, []);
1770
+ const unregisterPanelRef = useRef({
1771
+ pendingPanelIds: new Set(),
1772
+ timeout: null
1773
+ });
1779
1774
  const unregisterPanel = useCallback(panelData => {
1780
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1781
- setPanelDataArray(panelDataArray => {
1782
- const index = panelDataArray.indexOf(panelData);
1783
- if (index >= 0) {
1784
- panelDataArray = [...panelDataArray];
1785
- panelDataArray.splice(index, 1);
1775
+ const {
1776
+ id: groupId,
1777
+ layout: prevLayout,
1778
+ onLayout,
1779
+ panelDataArray
1780
+ } = committedValuesRef.current;
1781
+ const index = panelDataArray.indexOf(panelData);
1782
+ if (index >= 0) {
1783
+ panelDataArray.splice(index, 1);
1784
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1785
+ }
1786
+ if (unregisterPanelRef.current.timeout != null) {
1787
+ clearTimeout(unregisterPanelRef.current.timeout);
1788
+ }
1789
+
1790
+ // Batch panel unmounts so that we only calculate layout once;
1791
+ // This is more efficient and avoids misleading warnings in development mode.
1792
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1793
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1794
+ const {
1795
+ pendingPanelIds
1796
+ } = unregisterPanelRef.current;
1797
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1798
+
1799
+ // TRICKY
1800
+ // Strict effects mode
1801
+ let unmountDueToStrictMode = false;
1802
+ pendingPanelIds.forEach(panelId => {
1803
+ pendingPanelIds.delete(panelId);
1804
+ if (panelDataArray.find(({
1805
+ id
1806
+ }) => id === panelId) == null) {
1807
+ unmountDueToStrictMode = true;
1808
+
1809
+ // TRICKY
1810
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1811
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1812
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1813
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1814
+ }
1815
+ });
1816
+ if (!unmountDueToStrictMode) {
1817
+ return;
1786
1818
  }
1787
- return panelDataArray;
1788
- });
1789
- }, []);
1819
+ if (panelDataArray.length === 0) {
1820
+ // The group is unmounting; skip layout calculation.
1821
+ return;
1822
+ }
1823
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1824
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1825
+ groupSizePixels,
1826
+ panelDataArray
1827
+ });
1790
1828
 
1791
- // Handle imperative API calls that were made before panels were registered
1792
- useIsomorphicLayoutEffect(() => {
1793
- const queue = imperativeApiQueue;
1794
- while (queue.length > 0) {
1795
- const current = queue.shift();
1796
- switch (current.type) {
1797
- case "collapse":
1798
- {
1799
- collapsePanel(current.panelData);
1800
- break;
1801
- }
1802
- case "expand":
1803
- {
1804
- expandPanel(current.panelData);
1805
- break;
1806
- }
1807
- case "resize":
1808
- {
1809
- resizePanel(current.panelData, current.mixedSizes);
1810
- break;
1811
- }
1829
+ // Validate even saved layouts in case something has changed since last render
1830
+ // e.g. for pixel groups, this could be the size of the window
1831
+ const nextLayout = validatePanelGroupLayout({
1832
+ groupSizePixels,
1833
+ layout: unsafeLayout,
1834
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1835
+ });
1836
+ if (!areEqual(prevLayout, nextLayout)) {
1837
+ setLayout(nextLayout);
1838
+ committedValuesRef.current.layout = nextLayout;
1839
+ if (onLayout) {
1840
+ onLayout(nextLayout.map(sizePercentage => ({
1841
+ sizePercentage,
1842
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1843
+ })));
1844
+ }
1845
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1812
1846
  }
1813
- }
1814
- }, [collapsePanel, expandPanel, imperativeApiQueue, layout, panelDataArray, resizePanel]);
1847
+ }, 0);
1848
+ }, []);
1815
1849
  const context = useMemo(() => ({
1816
1850
  collapsePanel,
1817
1851
  direction,