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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.60
4
+
5
+ - Better support imperative API usage from mount effects.
6
+ - Better support strict effects mode.
7
+ - Better checks not to call `onResize` or `onLayout` more than once.
8
+
3
9
  ## 0.0.59
4
10
 
5
11
  - 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
@@ -1094,6 +1096,10 @@ function debounce(callback, durationMs = 10) {
1094
1096
  return callable;
1095
1097
  }
1096
1098
 
1099
+ function getPanelElementsForGroup(groupId) {
1100
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1101
+ }
1102
+
1097
1103
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1098
1104
  // or on a browser with cookies/storage disabled.
1099
1105
  // In either case, this function avoids accessing localStorage until needed,
@@ -1243,7 +1249,7 @@ const defaultStorage = {
1243
1249
  };
1244
1250
  const debounceMap = {};
1245
1251
  function PanelGroupWithForwardedRef({
1246
- autoSaveId,
1252
+ autoSaveId = null,
1247
1253
  children,
1248
1254
  className: classNameFromProps = "",
1249
1255
  dataAttributes,
@@ -1260,12 +1266,11 @@ function PanelGroupWithForwardedRef({
1260
1266
  const groupId = useUniqueId(idFromProps);
1261
1267
  const [dragState, setDragState] = useState(null);
1262
1268
  const [layout, setLayout] = useState([]);
1263
- const [panelDataArray, setPanelDataArray] = useState([]);
1264
1269
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1265
1270
  const panelSizeBeforeCollapseRef = useRef(new Map());
1266
1271
  const prevDeltaRef = useRef(0);
1267
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1268
1272
  const committedValuesRef = useRef({
1273
+ autoSaveId,
1269
1274
  direction,
1270
1275
  dragState,
1271
1276
  id: groupId,
@@ -1273,7 +1278,8 @@ function PanelGroupWithForwardedRef({
1273
1278
  keyboardResizeByPixels,
1274
1279
  layout,
1275
1280
  onLayout,
1276
- panelDataArray
1281
+ panelDataArray: [],
1282
+ storage
1277
1283
  });
1278
1284
  useRef({
1279
1285
  didLogIdAndOrderWarning: false,
@@ -1311,6 +1317,7 @@ function PanelGroupWithForwardedRef({
1311
1317
  });
1312
1318
  if (!areEqual(prevLayout, safeLayout)) {
1313
1319
  setLayout(safeLayout);
1320
+ committedValuesRef.current.layout = safeLayout;
1314
1321
  if (onLayout) {
1315
1322
  onLayout(safeLayout.map(sizePercentage => ({
1316
1323
  sizePercentage,
@@ -1322,21 +1329,29 @@ function PanelGroupWithForwardedRef({
1322
1329
  }
1323
1330
  }), []);
1324
1331
  useIsomorphicLayoutEffect(() => {
1332
+ committedValuesRef.current.autoSaveId = autoSaveId;
1325
1333
  committedValuesRef.current.direction = direction;
1326
1334
  committedValuesRef.current.dragState = dragState;
1327
1335
  committedValuesRef.current.id = groupId;
1328
- committedValuesRef.current.layout = layout;
1329
1336
  committedValuesRef.current.onLayout = onLayout;
1330
- committedValuesRef.current.panelDataArray = panelDataArray;
1337
+ committedValuesRef.current.storage = storage;
1338
+
1339
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
1340
+ // TODO [217] Move these values into a separate ref
1331
1341
  });
1342
+
1332
1343
  useWindowSplitterPanelGroupBehavior({
1333
1344
  committedValuesRef,
1334
1345
  groupId,
1335
1346
  layout,
1336
- panelDataArray,
1347
+ panelDataArray: committedValuesRef.current.panelDataArray,
1337
1348
  setLayout
1338
1349
  });
1339
1350
  useEffect(() => {
1351
+ const {
1352
+ panelDataArray
1353
+ } = committedValuesRef.current;
1354
+
1340
1355
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1341
1356
  if (autoSaveId) {
1342
1357
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1349,63 +1364,11 @@ function PanelGroupWithForwardedRef({
1349
1364
  }
1350
1365
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1351
1366
  }
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)!
1367
+ }, [autoSaveId, layout, storage]);
1357
1368
  useIsomorphicLayoutEffect(() => {
1358
1369
  const {
1359
- id: groupId,
1360
- layout,
1361
- onLayout
1370
+ panelDataArray
1362
1371
  } = 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(() => {
1409
1372
  const constraints = panelDataArray.map(({
1410
1373
  constraints
1411
1374
  }) => constraints);
@@ -1429,6 +1392,7 @@ function PanelGroupWithForwardedRef({
1429
1392
  });
1430
1393
  if (!areEqual(prevLayout, nextLayout)) {
1431
1394
  setLayout(nextLayout);
1395
+ committedValuesRef.current.layout = nextLayout;
1432
1396
  if (onLayout) {
1433
1397
  onLayout(nextLayout.map(sizePercentage => ({
1434
1398
  sizePercentage,
@@ -1443,7 +1407,7 @@ function PanelGroupWithForwardedRef({
1443
1407
  resizeObserver.disconnect();
1444
1408
  };
1445
1409
  }
1446
- }, [groupId, panelDataArray]);
1410
+ }, [groupId]);
1447
1411
 
1448
1412
  // DEV warnings
1449
1413
  useEffect(() => {
@@ -1456,17 +1420,6 @@ function PanelGroupWithForwardedRef({
1456
1420
  onLayout,
1457
1421
  panelDataArray
1458
1422
  } = 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
- }
1470
1423
  if (panelData.constraints.collapsible) {
1471
1424
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1472
1425
  const {
@@ -1491,6 +1444,7 @@ function PanelGroupWithForwardedRef({
1491
1444
  });
1492
1445
  if (!compareLayouts(prevLayout, nextLayout)) {
1493
1446
  setLayout(nextLayout);
1447
+ committedValuesRef.current.layout = nextLayout;
1494
1448
  if (onLayout) {
1495
1449
  onLayout(nextLayout.map(sizePercentage => ({
1496
1450
  sizePercentage,
@@ -1510,17 +1464,6 @@ function PanelGroupWithForwardedRef({
1510
1464
  onLayout,
1511
1465
  panelDataArray
1512
1466
  } = 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
- }
1524
1467
  if (panelData.constraints.collapsible) {
1525
1468
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1526
1469
  const {
@@ -1546,6 +1489,7 @@ function PanelGroupWithForwardedRef({
1546
1489
  });
1547
1490
  if (!compareLayouts(prevLayout, nextLayout)) {
1548
1491
  setLayout(nextLayout);
1492
+ committedValuesRef.current.layout = nextLayout;
1549
1493
  if (onLayout) {
1550
1494
  onLayout(nextLayout.map(sizePercentage => ({
1551
1495
  sizePercentage,
@@ -1576,6 +1520,9 @@ function PanelGroupWithForwardedRef({
1576
1520
 
1577
1521
  // This API should never read from committedValuesRef
1578
1522
  const getPanelStyle = useCallback(panelData => {
1523
+ const {
1524
+ panelDataArray
1525
+ } = committedValuesRef.current;
1579
1526
  const panelIndex = panelDataArray.indexOf(panelData);
1580
1527
  return computePanelFlexBoxStyle({
1581
1528
  dragState,
@@ -1583,7 +1530,7 @@ function PanelGroupWithForwardedRef({
1583
1530
  panelData: panelDataArray,
1584
1531
  panelIndex
1585
1532
  });
1586
- }, [dragState, layout, panelDataArray]);
1533
+ }, [dragState, layout]);
1587
1534
 
1588
1535
  // External APIs are safe to memoize via committed values ref
1589
1536
  const isPanelCollapsed = useCallback(panelData => {
@@ -1613,22 +1560,76 @@ function PanelGroupWithForwardedRef({
1613
1560
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1614
1561
  }, [groupId]);
1615
1562
  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
- }
1563
+ const {
1564
+ autoSaveId,
1565
+ id: groupId,
1566
+ layout: prevLayout,
1567
+ onLayout,
1568
+ panelDataArray,
1569
+ storage
1570
+ } = committedValuesRef.current;
1571
+ panelDataArray.push(panelData);
1572
+ panelDataArray.sort((panelA, panelB) => {
1573
+ const orderA = panelA.order;
1574
+ const orderB = panelB.order;
1575
+ if (orderA == null && orderB == null) {
1576
+ return 0;
1577
+ } else if (orderA == null) {
1578
+ return -1;
1579
+ } else if (orderB == null) {
1580
+ return 1;
1581
+ } else {
1582
+ return orderA - orderB;
1583
+ }
1584
+ });
1585
+
1586
+ // Wait until all panels have registered before we try to compute layout;
1587
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1588
+ const panelElements = getPanelElementsForGroup(groupId);
1589
+ if (panelElements.length !== panelDataArray.length) {
1590
+ return;
1591
+ }
1592
+
1593
+ // If this panel has been configured to persist sizing information,
1594
+ // default size should be restored from local storage if possible.
1595
+ let unsafeLayout = null;
1596
+ if (autoSaveId) {
1597
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1598
+ }
1599
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1600
+ if (groupSizePixels <= 0) {
1601
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1602
+ constraints
1603
+ }) => constraints))) {
1604
+ // Wait until the group has rendered a non-zero size before computing layout.
1605
+ return;
1606
+ }
1607
+ }
1608
+ if (unsafeLayout == null) {
1609
+ unsafeLayout = calculateUnsafeDefaultLayout({
1610
+ groupSizePixels,
1611
+ panelDataArray
1630
1612
  });
1613
+ }
1614
+
1615
+ // Validate even saved layouts in case something has changed since last render
1616
+ // e.g. for pixel groups, this could be the size of the window
1617
+ const nextLayout = validatePanelGroupLayout({
1618
+ groupSizePixels,
1619
+ layout: unsafeLayout,
1620
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1631
1621
  });
1622
+ if (!areEqual(prevLayout, nextLayout)) {
1623
+ setLayout(nextLayout);
1624
+ committedValuesRef.current.layout = nextLayout;
1625
+ if (onLayout) {
1626
+ onLayout(nextLayout.map(sizePercentage => ({
1627
+ sizePercentage,
1628
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1629
+ })));
1630
+ }
1631
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1632
+ }
1632
1633
  }, []);
1633
1634
  const registerResizeHandle = useCallback(dragHandleId => {
1634
1635
  return function resizeHandler(event) {
@@ -1698,6 +1699,7 @@ function PanelGroupWithForwardedRef({
1698
1699
  }
1699
1700
  if (layoutChanged) {
1700
1701
  setLayout(nextLayout);
1702
+ committedValuesRef.current.layout = nextLayout;
1701
1703
  if (onLayout) {
1702
1704
  onLayout(nextLayout.map(sizePercentage => ({
1703
1705
  sizePercentage,
@@ -1716,18 +1718,6 @@ function PanelGroupWithForwardedRef({
1716
1718
  onLayout,
1717
1719
  panelDataArray
1718
1720
  } = 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
- }
1731
1721
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1732
1722
  const {
1733
1723
  groupSizePixels,
@@ -1747,6 +1737,7 @@ function PanelGroupWithForwardedRef({
1747
1737
  });
1748
1738
  if (!compareLayouts(prevLayout, nextLayout)) {
1749
1739
  setLayout(nextLayout);
1740
+ committedValuesRef.current.layout = nextLayout;
1750
1741
  if (onLayout) {
1751
1742
  onLayout(nextLayout.map(sizePercentage => ({
1752
1743
  sizePercentage,
@@ -1774,42 +1765,85 @@ function PanelGroupWithForwardedRef({
1774
1765
  resetGlobalCursorStyle();
1775
1766
  setDragState(null);
1776
1767
  }, []);
1768
+ const unregisterPanelRef = useRef({
1769
+ pendingPanelIds: new Set(),
1770
+ timeout: null
1771
+ });
1777
1772
  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);
1773
+ const {
1774
+ id: groupId,
1775
+ layout: prevLayout,
1776
+ onLayout,
1777
+ panelDataArray
1778
+ } = committedValuesRef.current;
1779
+ const index = panelDataArray.indexOf(panelData);
1780
+ if (index >= 0) {
1781
+ panelDataArray.splice(index, 1);
1782
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1783
+ }
1784
+ if (unregisterPanelRef.current.timeout != null) {
1785
+ clearTimeout(unregisterPanelRef.current.timeout);
1786
+ }
1787
+
1788
+ // Batch panel unmounts so that we only calculate layout once;
1789
+ // This is more efficient and avoids misleading warnings in development mode.
1790
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1791
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1792
+ const {
1793
+ pendingPanelIds
1794
+ } = unregisterPanelRef.current;
1795
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1796
+
1797
+ // TRICKY
1798
+ // Strict effects mode
1799
+ let unmountDueToStrictMode = false;
1800
+ pendingPanelIds.forEach(panelId => {
1801
+ pendingPanelIds.delete(panelId);
1802
+ if (panelDataArray.find(({
1803
+ id
1804
+ }) => id === panelId) == null) {
1805
+ unmountDueToStrictMode = true;
1806
+
1807
+ // TRICKY
1808
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1809
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1810
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1811
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1812
+ }
1813
+ });
1814
+ if (!unmountDueToStrictMode) {
1815
+ return;
1784
1816
  }
1785
- return panelDataArray;
1786
- });
1787
- }, []);
1817
+ if (panelDataArray.length === 0) {
1818
+ // The group is unmounting; skip layout calculation.
1819
+ return;
1820
+ }
1821
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1822
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1823
+ groupSizePixels,
1824
+ panelDataArray
1825
+ });
1788
1826
 
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
- }
1827
+ // Validate even saved layouts in case something has changed since last render
1828
+ // e.g. for pixel groups, this could be the size of the window
1829
+ const nextLayout = validatePanelGroupLayout({
1830
+ groupSizePixels,
1831
+ layout: unsafeLayout,
1832
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1833
+ });
1834
+ if (!areEqual(prevLayout, nextLayout)) {
1835
+ setLayout(nextLayout);
1836
+ committedValuesRef.current.layout = nextLayout;
1837
+ if (onLayout) {
1838
+ onLayout(nextLayout.map(sizePercentage => ({
1839
+ sizePercentage,
1840
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1841
+ })));
1842
+ }
1843
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1810
1844
  }
1811
- }
1812
- }, [collapsePanel, expandPanel, imperativeApiQueue, layout, panelDataArray, resizePanel]);
1845
+ }, 0);
1846
+ }, []);
1813
1847
  const context = useMemo(() => ({
1814
1848
  collapsePanel,
1815
1849
  direction,