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.
@@ -71,6 +71,7 @@ function PanelWithForwardedRef({
71
71
  expandPanel,
72
72
  getPanelSize,
73
73
  getPanelStyle,
74
+ groupId,
74
75
  isPanelCollapsed,
75
76
  registerPanel,
76
77
  resizePanel,
@@ -164,6 +165,7 @@ function PanelWithForwardedRef({
164
165
  // CSS selectors
165
166
  "data-panel": "",
166
167
  "data-panel-id": panelId,
168
+ "data-panel-group-id": groupId,
167
169
  // e2e test attributes
168
170
  "data-panel-collapsible": undefined,
169
171
  "data-panel-size": undefined
@@ -1072,6 +1074,10 @@ function debounce(callback, durationMs = 10) {
1072
1074
  return callable;
1073
1075
  }
1074
1076
 
1077
+ function getPanelElementsForGroup(groupId) {
1078
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1079
+ }
1080
+
1075
1081
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1076
1082
  // or on a browser with cookies/storage disabled.
1077
1083
  // In either case, this function avoids accessing localStorage until needed,
@@ -1221,7 +1227,7 @@ const defaultStorage = {
1221
1227
  };
1222
1228
  const debounceMap = {};
1223
1229
  function PanelGroupWithForwardedRef({
1224
- autoSaveId,
1230
+ autoSaveId = null,
1225
1231
  children,
1226
1232
  className: classNameFromProps = "",
1227
1233
  dataAttributes,
@@ -1238,12 +1244,11 @@ function PanelGroupWithForwardedRef({
1238
1244
  const groupId = useUniqueId(idFromProps);
1239
1245
  const [dragState, setDragState] = useState(null);
1240
1246
  const [layout, setLayout] = useState([]);
1241
- const [panelDataArray, setPanelDataArray] = useState([]);
1242
1247
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1243
1248
  const panelSizeBeforeCollapseRef = useRef(new Map());
1244
1249
  const prevDeltaRef = useRef(0);
1245
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1246
1250
  const committedValuesRef = useRef({
1251
+ autoSaveId,
1247
1252
  direction,
1248
1253
  dragState,
1249
1254
  id: groupId,
@@ -1251,7 +1256,8 @@ function PanelGroupWithForwardedRef({
1251
1256
  keyboardResizeByPixels,
1252
1257
  layout,
1253
1258
  onLayout,
1254
- panelDataArray
1259
+ panelDataArray: [],
1260
+ storage
1255
1261
  });
1256
1262
  useRef({
1257
1263
  didLogIdAndOrderWarning: false,
@@ -1289,6 +1295,7 @@ function PanelGroupWithForwardedRef({
1289
1295
  });
1290
1296
  if (!areEqual(prevLayout, safeLayout)) {
1291
1297
  setLayout(safeLayout);
1298
+ committedValuesRef.current.layout = safeLayout;
1292
1299
  if (onLayout) {
1293
1300
  onLayout(safeLayout.map(sizePercentage => ({
1294
1301
  sizePercentage,
@@ -1300,21 +1307,29 @@ function PanelGroupWithForwardedRef({
1300
1307
  }
1301
1308
  }), []);
1302
1309
  useIsomorphicLayoutEffect(() => {
1310
+ committedValuesRef.current.autoSaveId = autoSaveId;
1303
1311
  committedValuesRef.current.direction = direction;
1304
1312
  committedValuesRef.current.dragState = dragState;
1305
1313
  committedValuesRef.current.id = groupId;
1306
- committedValuesRef.current.layout = layout;
1307
1314
  committedValuesRef.current.onLayout = onLayout;
1308
- committedValuesRef.current.panelDataArray = panelDataArray;
1315
+ committedValuesRef.current.storage = storage;
1316
+
1317
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
1318
+ // TODO [217] Move these values into a separate ref
1309
1319
  });
1320
+
1310
1321
  useWindowSplitterPanelGroupBehavior({
1311
1322
  committedValuesRef,
1312
1323
  groupId,
1313
1324
  layout,
1314
- panelDataArray,
1325
+ panelDataArray: committedValuesRef.current.panelDataArray,
1315
1326
  setLayout
1316
1327
  });
1317
1328
  useEffect(() => {
1329
+ const {
1330
+ panelDataArray
1331
+ } = committedValuesRef.current;
1332
+
1318
1333
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1319
1334
  if (autoSaveId) {
1320
1335
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1327,63 +1342,11 @@ function PanelGroupWithForwardedRef({
1327
1342
  }
1328
1343
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1329
1344
  }
1330
- }, [autoSaveId, layout, panelDataArray, storage]);
1331
-
1332
- // Once all panels have registered themselves,
1333
- // Compute the initial sizes based on default weights.
1334
- // This assumes that panels register during initial mount (no conditional rendering)!
1345
+ }, [autoSaveId, layout, storage]);
1335
1346
  useIsomorphicLayoutEffect(() => {
1336
1347
  const {
1337
- id: groupId,
1338
- layout,
1339
- onLayout
1348
+ panelDataArray
1340
1349
  } = committedValuesRef.current;
1341
- if (layout.length === panelDataArray.length) {
1342
- // Only compute (or restore) default layout once per panel configuration.
1343
- return;
1344
- }
1345
-
1346
- // If this panel has been configured to persist sizing information,
1347
- // default size should be restored from local storage if possible.
1348
- let unsafeLayout = null;
1349
- if (autoSaveId) {
1350
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1351
- }
1352
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1353
- if (groupSizePixels <= 0) {
1354
- if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1355
- constraints
1356
- }) => constraints))) {
1357
- // Wait until the group has rendered a non-zero size before computing layout.
1358
- return;
1359
- }
1360
- }
1361
- if (unsafeLayout == null) {
1362
- unsafeLayout = calculateUnsafeDefaultLayout({
1363
- groupSizePixels,
1364
- panelDataArray
1365
- });
1366
- }
1367
-
1368
- // Validate even saved layouts in case something has changed since last render
1369
- // e.g. for pixel groups, this could be the size of the window
1370
- const validatedLayout = validatePanelGroupLayout({
1371
- groupSizePixels,
1372
- layout: unsafeLayout,
1373
- panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1374
- });
1375
- if (!areEqual(layout, validatedLayout)) {
1376
- setLayout(validatedLayout);
1377
- }
1378
- if (onLayout) {
1379
- onLayout(validatedLayout.map(sizePercentage => ({
1380
- sizePercentage,
1381
- sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1382
- })));
1383
- }
1384
- callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1385
- }, [autoSaveId, layout, panelDataArray, storage]);
1386
- useIsomorphicLayoutEffect(() => {
1387
1350
  const constraints = panelDataArray.map(({
1388
1351
  constraints
1389
1352
  }) => constraints);
@@ -1407,6 +1370,7 @@ function PanelGroupWithForwardedRef({
1407
1370
  });
1408
1371
  if (!areEqual(prevLayout, nextLayout)) {
1409
1372
  setLayout(nextLayout);
1373
+ committedValuesRef.current.layout = nextLayout;
1410
1374
  if (onLayout) {
1411
1375
  onLayout(nextLayout.map(sizePercentage => ({
1412
1376
  sizePercentage,
@@ -1421,7 +1385,7 @@ function PanelGroupWithForwardedRef({
1421
1385
  resizeObserver.disconnect();
1422
1386
  };
1423
1387
  }
1424
- }, [groupId, panelDataArray]);
1388
+ }, [groupId]);
1425
1389
 
1426
1390
  // DEV warnings
1427
1391
  useEffect(() => {
@@ -1434,17 +1398,6 @@ function PanelGroupWithForwardedRef({
1434
1398
  onLayout,
1435
1399
  panelDataArray
1436
1400
  } = committedValuesRef.current;
1437
-
1438
- // See issues/211
1439
- if (panelDataArray.find(({
1440
- id
1441
- }) => id === panelData.id) == null) {
1442
- setImperativeApiQueue(prev => [...prev, {
1443
- panelData,
1444
- type: "collapse"
1445
- }]);
1446
- return;
1447
- }
1448
1401
  if (panelData.constraints.collapsible) {
1449
1402
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1450
1403
  const {
@@ -1469,6 +1422,7 @@ function PanelGroupWithForwardedRef({
1469
1422
  });
1470
1423
  if (!compareLayouts(prevLayout, nextLayout)) {
1471
1424
  setLayout(nextLayout);
1425
+ committedValuesRef.current.layout = nextLayout;
1472
1426
  if (onLayout) {
1473
1427
  onLayout(nextLayout.map(sizePercentage => ({
1474
1428
  sizePercentage,
@@ -1488,17 +1442,6 @@ function PanelGroupWithForwardedRef({
1488
1442
  onLayout,
1489
1443
  panelDataArray
1490
1444
  } = committedValuesRef.current;
1491
-
1492
- // See issues/211
1493
- if (panelDataArray.find(({
1494
- id
1495
- }) => id === panelData.id) == null) {
1496
- setImperativeApiQueue(prev => [...prev, {
1497
- panelData,
1498
- type: "expand"
1499
- }]);
1500
- return;
1501
- }
1502
1445
  if (panelData.constraints.collapsible) {
1503
1446
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1504
1447
  const {
@@ -1524,6 +1467,7 @@ function PanelGroupWithForwardedRef({
1524
1467
  });
1525
1468
  if (!compareLayouts(prevLayout, nextLayout)) {
1526
1469
  setLayout(nextLayout);
1470
+ committedValuesRef.current.layout = nextLayout;
1527
1471
  if (onLayout) {
1528
1472
  onLayout(nextLayout.map(sizePercentage => ({
1529
1473
  sizePercentage,
@@ -1554,6 +1498,9 @@ function PanelGroupWithForwardedRef({
1554
1498
 
1555
1499
  // This API should never read from committedValuesRef
1556
1500
  const getPanelStyle = useCallback(panelData => {
1501
+ const {
1502
+ panelDataArray
1503
+ } = committedValuesRef.current;
1557
1504
  const panelIndex = panelDataArray.indexOf(panelData);
1558
1505
  return computePanelFlexBoxStyle({
1559
1506
  dragState,
@@ -1561,7 +1508,7 @@ function PanelGroupWithForwardedRef({
1561
1508
  panelData: panelDataArray,
1562
1509
  panelIndex
1563
1510
  });
1564
- }, [dragState, layout, panelDataArray]);
1511
+ }, [dragState, layout]);
1565
1512
 
1566
1513
  // External APIs are safe to memoize via committed values ref
1567
1514
  const isPanelCollapsed = useCallback(panelData => {
@@ -1591,22 +1538,76 @@ function PanelGroupWithForwardedRef({
1591
1538
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1592
1539
  }, [groupId]);
1593
1540
  const registerPanel = useCallback(panelData => {
1594
- setPanelDataArray(prevPanelDataArray => {
1595
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1596
- return nextPanelDataArray.sort((panelA, panelB) => {
1597
- const orderA = panelA.order;
1598
- const orderB = panelB.order;
1599
- if (orderA == null && orderB == null) {
1600
- return 0;
1601
- } else if (orderA == null) {
1602
- return -1;
1603
- } else if (orderB == null) {
1604
- return 1;
1605
- } else {
1606
- return orderA - orderB;
1607
- }
1541
+ const {
1542
+ autoSaveId,
1543
+ id: groupId,
1544
+ layout: prevLayout,
1545
+ onLayout,
1546
+ panelDataArray,
1547
+ storage
1548
+ } = committedValuesRef.current;
1549
+ panelDataArray.push(panelData);
1550
+ panelDataArray.sort((panelA, panelB) => {
1551
+ const orderA = panelA.order;
1552
+ const orderB = panelB.order;
1553
+ if (orderA == null && orderB == null) {
1554
+ return 0;
1555
+ } else if (orderA == null) {
1556
+ return -1;
1557
+ } else if (orderB == null) {
1558
+ return 1;
1559
+ } else {
1560
+ return orderA - orderB;
1561
+ }
1562
+ });
1563
+
1564
+ // Wait until all panels have registered before we try to compute layout;
1565
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1566
+ const panelElements = getPanelElementsForGroup(groupId);
1567
+ if (panelElements.length !== panelDataArray.length) {
1568
+ return;
1569
+ }
1570
+
1571
+ // If this panel has been configured to persist sizing information,
1572
+ // default size should be restored from local storage if possible.
1573
+ let unsafeLayout = null;
1574
+ if (autoSaveId) {
1575
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1576
+ }
1577
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1578
+ if (groupSizePixels <= 0) {
1579
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1580
+ constraints
1581
+ }) => constraints))) {
1582
+ // Wait until the group has rendered a non-zero size before computing layout.
1583
+ return;
1584
+ }
1585
+ }
1586
+ if (unsafeLayout == null) {
1587
+ unsafeLayout = calculateUnsafeDefaultLayout({
1588
+ groupSizePixels,
1589
+ panelDataArray
1608
1590
  });
1591
+ }
1592
+
1593
+ // Validate even saved layouts in case something has changed since last render
1594
+ // e.g. for pixel groups, this could be the size of the window
1595
+ const nextLayout = validatePanelGroupLayout({
1596
+ groupSizePixels,
1597
+ layout: unsafeLayout,
1598
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1609
1599
  });
1600
+ if (!areEqual(prevLayout, nextLayout)) {
1601
+ setLayout(nextLayout);
1602
+ committedValuesRef.current.layout = nextLayout;
1603
+ if (onLayout) {
1604
+ onLayout(nextLayout.map(sizePercentage => ({
1605
+ sizePercentage,
1606
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1607
+ })));
1608
+ }
1609
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1610
+ }
1610
1611
  }, []);
1611
1612
  const registerResizeHandle = useCallback(dragHandleId => {
1612
1613
  return function resizeHandler(event) {
@@ -1676,6 +1677,7 @@ function PanelGroupWithForwardedRef({
1676
1677
  }
1677
1678
  if (layoutChanged) {
1678
1679
  setLayout(nextLayout);
1680
+ committedValuesRef.current.layout = nextLayout;
1679
1681
  if (onLayout) {
1680
1682
  onLayout(nextLayout.map(sizePercentage => ({
1681
1683
  sizePercentage,
@@ -1694,18 +1696,6 @@ function PanelGroupWithForwardedRef({
1694
1696
  onLayout,
1695
1697
  panelDataArray
1696
1698
  } = committedValuesRef.current;
1697
-
1698
- // See issues/211
1699
- if (panelDataArray.find(({
1700
- id
1701
- }) => id === panelData.id) == null) {
1702
- setImperativeApiQueue(prev => [...prev, {
1703
- panelData,
1704
- mixedSizes,
1705
- type: "resize"
1706
- }]);
1707
- return;
1708
- }
1709
1699
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1710
1700
  const {
1711
1701
  groupSizePixels,
@@ -1725,6 +1715,7 @@ function PanelGroupWithForwardedRef({
1725
1715
  });
1726
1716
  if (!compareLayouts(prevLayout, nextLayout)) {
1727
1717
  setLayout(nextLayout);
1718
+ committedValuesRef.current.layout = nextLayout;
1728
1719
  if (onLayout) {
1729
1720
  onLayout(nextLayout.map(sizePercentage => ({
1730
1721
  sizePercentage,
@@ -1752,42 +1743,85 @@ function PanelGroupWithForwardedRef({
1752
1743
  resetGlobalCursorStyle();
1753
1744
  setDragState(null);
1754
1745
  }, []);
1746
+ const unregisterPanelRef = useRef({
1747
+ pendingPanelIds: new Set(),
1748
+ timeout: null
1749
+ });
1755
1750
  const unregisterPanel = useCallback(panelData => {
1756
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1757
- setPanelDataArray(panelDataArray => {
1758
- const index = panelDataArray.indexOf(panelData);
1759
- if (index >= 0) {
1760
- panelDataArray = [...panelDataArray];
1761
- panelDataArray.splice(index, 1);
1751
+ const {
1752
+ id: groupId,
1753
+ layout: prevLayout,
1754
+ onLayout,
1755
+ panelDataArray
1756
+ } = committedValuesRef.current;
1757
+ const index = panelDataArray.indexOf(panelData);
1758
+ if (index >= 0) {
1759
+ panelDataArray.splice(index, 1);
1760
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1761
+ }
1762
+ if (unregisterPanelRef.current.timeout != null) {
1763
+ clearTimeout(unregisterPanelRef.current.timeout);
1764
+ }
1765
+
1766
+ // Batch panel unmounts so that we only calculate layout once;
1767
+ // This is more efficient and avoids misleading warnings in development mode.
1768
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1769
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1770
+ const {
1771
+ pendingPanelIds
1772
+ } = unregisterPanelRef.current;
1773
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1774
+
1775
+ // TRICKY
1776
+ // Strict effects mode
1777
+ let unmountDueToStrictMode = false;
1778
+ pendingPanelIds.forEach(panelId => {
1779
+ pendingPanelIds.delete(panelId);
1780
+ if (panelDataArray.find(({
1781
+ id
1782
+ }) => id === panelId) == null) {
1783
+ unmountDueToStrictMode = true;
1784
+
1785
+ // TRICKY
1786
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1787
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1788
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1789
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1790
+ }
1791
+ });
1792
+ if (!unmountDueToStrictMode) {
1793
+ return;
1762
1794
  }
1763
- return panelDataArray;
1764
- });
1765
- }, []);
1795
+ if (panelDataArray.length === 0) {
1796
+ // The group is unmounting; skip layout calculation.
1797
+ return;
1798
+ }
1799
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1800
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1801
+ groupSizePixels,
1802
+ panelDataArray
1803
+ });
1766
1804
 
1767
- // Handle imperative API calls that were made before panels were registered
1768
- useIsomorphicLayoutEffect(() => {
1769
- const queue = imperativeApiQueue;
1770
- while (queue.length > 0) {
1771
- const current = queue.shift();
1772
- switch (current.type) {
1773
- case "collapse":
1774
- {
1775
- collapsePanel(current.panelData);
1776
- break;
1777
- }
1778
- case "expand":
1779
- {
1780
- expandPanel(current.panelData);
1781
- break;
1782
- }
1783
- case "resize":
1784
- {
1785
- resizePanel(current.panelData, current.mixedSizes);
1786
- break;
1787
- }
1805
+ // Validate even saved layouts in case something has changed since last render
1806
+ // e.g. for pixel groups, this could be the size of the window
1807
+ const nextLayout = validatePanelGroupLayout({
1808
+ groupSizePixels,
1809
+ layout: unsafeLayout,
1810
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1811
+ });
1812
+ if (!areEqual(prevLayout, nextLayout)) {
1813
+ setLayout(nextLayout);
1814
+ committedValuesRef.current.layout = nextLayout;
1815
+ if (onLayout) {
1816
+ onLayout(nextLayout.map(sizePercentage => ({
1817
+ sizePercentage,
1818
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1819
+ })));
1820
+ }
1821
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1788
1822
  }
1789
- }
1790
- }, [collapsePanel, expandPanel, imperativeApiQueue, layout, panelDataArray, resizePanel]);
1823
+ }, 0);
1824
+ }, []);
1791
1825
  const context = useMemo(() => ({
1792
1826
  collapsePanel,
1793
1827
  direction,