react-resizable-panels 0.0.58 → 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,
@@ -168,6 +169,7 @@ function PanelWithForwardedRef({
168
169
  // CSS selectors
169
170
  "data-panel": "",
170
171
  "data-panel-id": panelId,
172
+ "data-panel-group-id": groupId,
171
173
  // e2e test attributes
172
174
  "data-panel-collapsible": collapsible || undefined ,
173
175
  "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
@@ -180,8 +182,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
180
182
  PanelWithForwardedRef.displayName = "Panel";
181
183
  Panel.displayName = "forwardRef(Panel)";
182
184
 
183
- const PRECISION = 10;
184
-
185
185
  function convertPixelsToPercentage(pixels, groupSizePixels) {
186
186
  return pixels / groupSizePixels * 100;
187
187
  }
@@ -259,6 +259,8 @@ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, gr
259
259
  };
260
260
  }
261
261
 
262
+ const PRECISION = 10;
263
+
262
264
  function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
263
265
  actual = parseFloat(actual.toFixed(fractionDigits));
264
266
  expected = parseFloat(expected.toFixed(fractionDigits));
@@ -742,15 +744,10 @@ function useWindowSplitterPanelGroupBehavior({
742
744
  }, [groupId, layout, panelDataArray]);
743
745
  useEffect(() => {
744
746
  const {
745
- direction,
746
747
  panelDataArray
747
748
  } = committedValuesRef.current;
748
749
  const groupElement = getPanelGroupElement(groupId);
749
750
  assert(groupElement != null, `No group found for id "${groupId}"`);
750
- const {
751
- height,
752
- width
753
- } = groupElement.getBoundingClientRect();
754
751
  const handles = getResizeHandleElementsForGroup(groupId);
755
752
  const cleanupFunctions = handles.map(handle => {
756
753
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
@@ -770,21 +767,19 @@ function useWindowSplitterPanelGroupBehavior({
770
767
  if (index >= 0) {
771
768
  const panelData = panelDataArray[index];
772
769
  const size = layout[index];
773
- if (size != null) {
774
- var _getPercentageSizeFro;
770
+ if (size != null && panelData.constraints.collapsible) {
771
+ var _getPercentageSizeFro, _getPercentageSizeFro2;
775
772
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
776
- const minSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
773
+ const collapsedSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
774
+ sizePercentage: panelData.constraints.collapsedSizePercentage,
775
+ sizePixels: panelData.constraints.collapsedSizePixels
776
+ }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
777
+ const minSize = (_getPercentageSizeFro2 = getPercentageSizeFromMixedSizes({
777
778
  sizePercentage: panelData.constraints.minSizePercentage,
778
779
  sizePixels: panelData.constraints.minSizePixels
779
- }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
780
- let delta = 0;
781
- if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
782
- delta = direction === "horizontal" ? width : height;
783
- } else {
784
- delta = -(direction === "horizontal" ? width : height);
785
- }
780
+ }, groupSizePixels)) !== null && _getPercentageSizeFro2 !== void 0 ? _getPercentageSizeFro2 : 0;
786
781
  const nextLayout = adjustLayoutByDelta({
787
- delta,
782
+ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size,
788
783
  groupSizePixels,
789
784
  layout,
790
785
  panelConstraints: panelDataArray.map(panelData => panelData.constraints),
@@ -1093,6 +1088,10 @@ function debounce(callback, durationMs = 10) {
1093
1088
  return callable;
1094
1089
  }
1095
1090
 
1091
+ function getPanelElementsForGroup(groupId) {
1092
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1093
+ }
1094
+
1096
1095
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1097
1096
  // or on a browser with cookies/storage disabled.
1098
1097
  // In either case, this function avoids accessing localStorage until needed,
@@ -1319,7 +1318,7 @@ const defaultStorage = {
1319
1318
  };
1320
1319
  const debounceMap = {};
1321
1320
  function PanelGroupWithForwardedRef({
1322
- autoSaveId,
1321
+ autoSaveId = null,
1323
1322
  children,
1324
1323
  className: classNameFromProps = "",
1325
1324
  dataAttributes,
@@ -1336,11 +1335,11 @@ function PanelGroupWithForwardedRef({
1336
1335
  const groupId = useUniqueId(idFromProps);
1337
1336
  const [dragState, setDragState] = useState(null);
1338
1337
  const [layout, setLayout] = useState([]);
1339
- const [panelDataArray, setPanelDataArray] = useState([]);
1340
1338
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1341
1339
  const panelSizeBeforeCollapseRef = useRef(new Map());
1342
1340
  const prevDeltaRef = useRef(0);
1343
1341
  const committedValuesRef = useRef({
1342
+ autoSaveId,
1344
1343
  direction,
1345
1344
  dragState,
1346
1345
  id: groupId,
@@ -1348,7 +1347,8 @@ function PanelGroupWithForwardedRef({
1348
1347
  keyboardResizeByPixels,
1349
1348
  layout,
1350
1349
  onLayout,
1351
- panelDataArray
1350
+ panelDataArray: [],
1351
+ storage
1352
1352
  });
1353
1353
  const devWarningsRef = useRef({
1354
1354
  didLogIdAndOrderWarning: false,
@@ -1386,6 +1386,7 @@ function PanelGroupWithForwardedRef({
1386
1386
  });
1387
1387
  if (!areEqual(prevLayout, safeLayout)) {
1388
1388
  setLayout(safeLayout);
1389
+ committedValuesRef.current.layout = safeLayout;
1389
1390
  if (onLayout) {
1390
1391
  onLayout(safeLayout.map(sizePercentage => ({
1391
1392
  sizePercentage,
@@ -1397,21 +1398,29 @@ function PanelGroupWithForwardedRef({
1397
1398
  }
1398
1399
  }), []);
1399
1400
  useIsomorphicLayoutEffect(() => {
1401
+ committedValuesRef.current.autoSaveId = autoSaveId;
1400
1402
  committedValuesRef.current.direction = direction;
1401
1403
  committedValuesRef.current.dragState = dragState;
1402
1404
  committedValuesRef.current.id = groupId;
1403
- committedValuesRef.current.layout = layout;
1404
1405
  committedValuesRef.current.onLayout = onLayout;
1405
- committedValuesRef.current.panelDataArray = panelDataArray;
1406
+ committedValuesRef.current.storage = storage;
1407
+
1408
+ // panelDataArray and layout are updated in-sync with scheduled state updates.
1409
+ // TODO [217] Move these values into a separate ref
1406
1410
  });
1411
+
1407
1412
  useWindowSplitterPanelGroupBehavior({
1408
1413
  committedValuesRef,
1409
1414
  groupId,
1410
1415
  layout,
1411
- panelDataArray,
1416
+ panelDataArray: committedValuesRef.current.panelDataArray,
1412
1417
  setLayout
1413
1418
  });
1414
1419
  useEffect(() => {
1420
+ const {
1421
+ panelDataArray
1422
+ } = committedValuesRef.current;
1423
+
1415
1424
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1416
1425
  if (autoSaveId) {
1417
1426
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1424,59 +1433,11 @@ function PanelGroupWithForwardedRef({
1424
1433
  }
1425
1434
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1426
1435
  }
1427
- }, [autoSaveId, layout, panelDataArray, storage]);
1428
-
1429
- // Once all panels have registered themselves,
1430
- // Compute the initial sizes based on default weights.
1431
- // This assumes that panels register during initial mount (no conditional rendering)!
1436
+ }, [autoSaveId, layout, storage]);
1432
1437
  useIsomorphicLayoutEffect(() => {
1433
1438
  const {
1434
- id: groupId,
1435
- layout,
1436
- onLayout
1439
+ panelDataArray
1437
1440
  } = committedValuesRef.current;
1438
- if (layout.length === panelDataArray.length) {
1439
- // Only compute (or restore) default layout once per panel configuration.
1440
- return;
1441
- }
1442
-
1443
- // If this panel has been configured to persist sizing information,
1444
- // default size should be restored from local storage if possible.
1445
- let unsafeLayout = null;
1446
- if (autoSaveId) {
1447
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1448
- }
1449
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1450
- if (groupSizePixels <= 0) {
1451
- // Wait until the group has rendered a non-zero size before computing layout.
1452
- return;
1453
- }
1454
- if (unsafeLayout == null) {
1455
- unsafeLayout = calculateUnsafeDefaultLayout({
1456
- groupSizePixels,
1457
- panelDataArray
1458
- });
1459
- }
1460
-
1461
- // Validate even saved layouts in case something has changed since last render
1462
- // e.g. for pixel groups, this could be the size of the window
1463
- const validatedLayout = validatePanelGroupLayout({
1464
- groupSizePixels,
1465
- layout: unsafeLayout,
1466
- panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1467
- });
1468
- if (!areEqual(layout, validatedLayout)) {
1469
- setLayout(validatedLayout);
1470
- }
1471
- if (onLayout) {
1472
- onLayout(validatedLayout.map(sizePercentage => ({
1473
- sizePercentage,
1474
- sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1475
- })));
1476
- }
1477
- callPanelCallbacks(groupId, panelDataArray, validatedLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1478
- }, [autoSaveId, layout, panelDataArray, storage]);
1479
- useIsomorphicLayoutEffect(() => {
1480
1441
  const constraints = panelDataArray.map(({
1481
1442
  constraints
1482
1443
  }) => constraints);
@@ -1500,6 +1461,7 @@ function PanelGroupWithForwardedRef({
1500
1461
  });
1501
1462
  if (!areEqual(prevLayout, nextLayout)) {
1502
1463
  setLayout(nextLayout);
1464
+ committedValuesRef.current.layout = nextLayout;
1503
1465
  if (onLayout) {
1504
1466
  onLayout(nextLayout.map(sizePercentage => ({
1505
1467
  sizePercentage,
@@ -1514,20 +1476,20 @@ function PanelGroupWithForwardedRef({
1514
1476
  resizeObserver.disconnect();
1515
1477
  };
1516
1478
  }
1517
- }, [groupId, panelDataArray]);
1479
+ }, [groupId]);
1518
1480
 
1519
1481
  // DEV warnings
1520
1482
  useEffect(() => {
1521
1483
  {
1484
+ const {
1485
+ panelDataArray
1486
+ } = committedValuesRef.current;
1522
1487
  const {
1523
1488
  didLogIdAndOrderWarning,
1524
1489
  didLogPanelConstraintsWarning,
1525
1490
  prevPanelIds
1526
1491
  } = devWarningsRef.current;
1527
1492
  if (!didLogIdAndOrderWarning) {
1528
- const {
1529
- panelDataArray
1530
- } = committedValuesRef.current;
1531
1493
  const panelIds = panelDataArray.map(({
1532
1494
  id
1533
1495
  }) => id);
@@ -1593,6 +1555,7 @@ function PanelGroupWithForwardedRef({
1593
1555
  });
1594
1556
  if (!compareLayouts(prevLayout, nextLayout)) {
1595
1557
  setLayout(nextLayout);
1558
+ committedValuesRef.current.layout = nextLayout;
1596
1559
  if (onLayout) {
1597
1560
  onLayout(nextLayout.map(sizePercentage => ({
1598
1561
  sizePercentage,
@@ -1637,6 +1600,7 @@ function PanelGroupWithForwardedRef({
1637
1600
  });
1638
1601
  if (!compareLayouts(prevLayout, nextLayout)) {
1639
1602
  setLayout(nextLayout);
1603
+ committedValuesRef.current.layout = nextLayout;
1640
1604
  if (onLayout) {
1641
1605
  onLayout(nextLayout.map(sizePercentage => ({
1642
1606
  sizePercentage,
@@ -1667,6 +1631,9 @@ function PanelGroupWithForwardedRef({
1667
1631
 
1668
1632
  // This API should never read from committedValuesRef
1669
1633
  const getPanelStyle = useCallback(panelData => {
1634
+ const {
1635
+ panelDataArray
1636
+ } = committedValuesRef.current;
1670
1637
  const panelIndex = panelDataArray.indexOf(panelData);
1671
1638
  return computePanelFlexBoxStyle({
1672
1639
  dragState,
@@ -1674,7 +1641,7 @@ function PanelGroupWithForwardedRef({
1674
1641
  panelData: panelDataArray,
1675
1642
  panelIndex
1676
1643
  });
1677
- }, [dragState, layout, panelDataArray]);
1644
+ }, [dragState, layout]);
1678
1645
 
1679
1646
  // External APIs are safe to memoize via committed values ref
1680
1647
  const isPanelCollapsed = useCallback(panelData => {
@@ -1704,22 +1671,76 @@ function PanelGroupWithForwardedRef({
1704
1671
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1705
1672
  }, [groupId]);
1706
1673
  const registerPanel = useCallback(panelData => {
1707
- setPanelDataArray(prevPanelDataArray => {
1708
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1709
- return nextPanelDataArray.sort((panelA, panelB) => {
1710
- const orderA = panelA.order;
1711
- const orderB = panelB.order;
1712
- if (orderA == null && orderB == null) {
1713
- return 0;
1714
- } else if (orderA == null) {
1715
- return -1;
1716
- } else if (orderB == null) {
1717
- return 1;
1718
- } else {
1719
- return orderA - orderB;
1720
- }
1674
+ const {
1675
+ autoSaveId,
1676
+ id: groupId,
1677
+ layout: prevLayout,
1678
+ onLayout,
1679
+ panelDataArray,
1680
+ storage
1681
+ } = committedValuesRef.current;
1682
+ panelDataArray.push(panelData);
1683
+ panelDataArray.sort((panelA, panelB) => {
1684
+ const orderA = panelA.order;
1685
+ const orderB = panelB.order;
1686
+ if (orderA == null && orderB == null) {
1687
+ return 0;
1688
+ } else if (orderA == null) {
1689
+ return -1;
1690
+ } else if (orderB == null) {
1691
+ return 1;
1692
+ } else {
1693
+ return orderA - orderB;
1694
+ }
1695
+ });
1696
+
1697
+ // Wait until all panels have registered before we try to compute layout;
1698
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1699
+ const panelElements = getPanelElementsForGroup(groupId);
1700
+ if (panelElements.length !== panelDataArray.length) {
1701
+ return;
1702
+ }
1703
+
1704
+ // If this panel has been configured to persist sizing information,
1705
+ // default size should be restored from local storage if possible.
1706
+ let unsafeLayout = null;
1707
+ if (autoSaveId) {
1708
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1709
+ }
1710
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1711
+ if (groupSizePixels <= 0) {
1712
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1713
+ constraints
1714
+ }) => constraints))) {
1715
+ // Wait until the group has rendered a non-zero size before computing layout.
1716
+ return;
1717
+ }
1718
+ }
1719
+ if (unsafeLayout == null) {
1720
+ unsafeLayout = calculateUnsafeDefaultLayout({
1721
+ groupSizePixels,
1722
+ panelDataArray
1721
1723
  });
1724
+ }
1725
+
1726
+ // Validate even saved layouts in case something has changed since last render
1727
+ // e.g. for pixel groups, this could be the size of the window
1728
+ const nextLayout = validatePanelGroupLayout({
1729
+ groupSizePixels,
1730
+ layout: unsafeLayout,
1731
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1722
1732
  });
1733
+ if (!areEqual(prevLayout, nextLayout)) {
1734
+ setLayout(nextLayout);
1735
+ committedValuesRef.current.layout = nextLayout;
1736
+ if (onLayout) {
1737
+ onLayout(nextLayout.map(sizePercentage => ({
1738
+ sizePercentage,
1739
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1740
+ })));
1741
+ }
1742
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1743
+ }
1723
1744
  }, []);
1724
1745
  const registerResizeHandle = useCallback(dragHandleId => {
1725
1746
  return function resizeHandler(event) {
@@ -1789,6 +1810,7 @@ function PanelGroupWithForwardedRef({
1789
1810
  }
1790
1811
  if (layoutChanged) {
1791
1812
  setLayout(nextLayout);
1813
+ committedValuesRef.current.layout = nextLayout;
1792
1814
  if (onLayout) {
1793
1815
  onLayout(nextLayout.map(sizePercentage => ({
1794
1816
  sizePercentage,
@@ -1826,6 +1848,7 @@ function PanelGroupWithForwardedRef({
1826
1848
  });
1827
1849
  if (!compareLayouts(prevLayout, nextLayout)) {
1828
1850
  setLayout(nextLayout);
1851
+ committedValuesRef.current.layout = nextLayout;
1829
1852
  if (onLayout) {
1830
1853
  onLayout(nextLayout.map(sizePercentage => ({
1831
1854
  sizePercentage,
@@ -1853,16 +1876,84 @@ function PanelGroupWithForwardedRef({
1853
1876
  resetGlobalCursorStyle();
1854
1877
  setDragState(null);
1855
1878
  }, []);
1879
+ const unregisterPanelRef = useRef({
1880
+ pendingPanelIds: new Set(),
1881
+ timeout: null
1882
+ });
1856
1883
  const unregisterPanel = useCallback(panelData => {
1857
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1858
- setPanelDataArray(panelDataArray => {
1859
- const index = panelDataArray.indexOf(panelData);
1860
- if (index >= 0) {
1861
- panelDataArray = [...panelDataArray];
1862
- panelDataArray.splice(index, 1);
1884
+ const {
1885
+ id: groupId,
1886
+ layout: prevLayout,
1887
+ onLayout,
1888
+ panelDataArray
1889
+ } = committedValuesRef.current;
1890
+ const index = panelDataArray.indexOf(panelData);
1891
+ if (index >= 0) {
1892
+ panelDataArray.splice(index, 1);
1893
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1894
+ }
1895
+ if (unregisterPanelRef.current.timeout != null) {
1896
+ clearTimeout(unregisterPanelRef.current.timeout);
1897
+ }
1898
+
1899
+ // Batch panel unmounts so that we only calculate layout once;
1900
+ // This is more efficient and avoids misleading warnings in development mode.
1901
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1902
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1903
+ const {
1904
+ pendingPanelIds
1905
+ } = unregisterPanelRef.current;
1906
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1907
+
1908
+ // TRICKY
1909
+ // Strict effects mode
1910
+ let unmountDueToStrictMode = false;
1911
+ pendingPanelIds.forEach(panelId => {
1912
+ pendingPanelIds.delete(panelId);
1913
+ if (panelDataArray.find(({
1914
+ id
1915
+ }) => id === panelId) == null) {
1916
+ unmountDueToStrictMode = true;
1917
+
1918
+ // TRICKY
1919
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1920
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1921
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1922
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1923
+ }
1924
+ });
1925
+ if (!unmountDueToStrictMode) {
1926
+ return;
1863
1927
  }
1864
- return panelDataArray;
1865
- });
1928
+ if (panelDataArray.length === 0) {
1929
+ // The group is unmounting; skip layout calculation.
1930
+ return;
1931
+ }
1932
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1933
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1934
+ groupSizePixels,
1935
+ panelDataArray
1936
+ });
1937
+
1938
+ // Validate even saved layouts in case something has changed since last render
1939
+ // e.g. for pixel groups, this could be the size of the window
1940
+ const nextLayout = validatePanelGroupLayout({
1941
+ groupSizePixels,
1942
+ layout: unsafeLayout,
1943
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1944
+ });
1945
+ if (!areEqual(prevLayout, nextLayout)) {
1946
+ setLayout(nextLayout);
1947
+ committedValuesRef.current.layout = nextLayout;
1948
+ if (onLayout) {
1949
+ onLayout(nextLayout.map(sizePercentage => ({
1950
+ sizePercentage,
1951
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1952
+ })));
1953
+ }
1954
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1955
+ }
1956
+ }, 0);
1866
1957
  }, []);
1867
1958
  const context = useMemo(() => ({
1868
1959
  collapsePanel,