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