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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.0.59
10
+
11
+ - Support imperative panel API usage on-mount.
12
+ - Made PanelGroup bailout condition smarter (don't bailout for empty groups unless pixel constraints are used).
13
+ - Improved window splitter compatibility by better handling "Enter" key.
14
+
3
15
  ## 0.0.58
4
16
 
5
17
  - Change group layout to more thoroughly distribute resize delta to support more flexible group size configurations.
@@ -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
@@ -198,8 +200,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
198
200
  PanelWithForwardedRef.displayName = "Panel";
199
201
  Panel.displayName = "forwardRef(Panel)";
200
202
 
201
- const PRECISION = 10;
202
-
203
203
  function convertPixelsToPercentage(pixels, groupSizePixels) {
204
204
  return pixels / groupSizePixels * 100;
205
205
  }
@@ -277,6 +277,8 @@ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, gr
277
277
  };
278
278
  }
279
279
 
280
+ const PRECISION = 10;
281
+
280
282
  function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
281
283
  actual = parseFloat(actual.toFixed(fractionDigits));
282
284
  expected = parseFloat(expected.toFixed(fractionDigits));
@@ -750,15 +752,10 @@ function useWindowSplitterPanelGroupBehavior({
750
752
  }, [groupId, layout, panelDataArray]);
751
753
  useEffect(() => {
752
754
  const {
753
- direction,
754
755
  panelDataArray
755
756
  } = committedValuesRef.current;
756
757
  const groupElement = getPanelGroupElement(groupId);
757
758
  assert(groupElement != null, `No group found for id "${groupId}"`);
758
- const {
759
- height,
760
- width
761
- } = groupElement.getBoundingClientRect();
762
759
  const handles = getResizeHandleElementsForGroup(groupId);
763
760
  const cleanupFunctions = handles.map(handle => {
764
761
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
@@ -778,21 +775,19 @@ function useWindowSplitterPanelGroupBehavior({
778
775
  if (index >= 0) {
779
776
  const panelData = panelDataArray[index];
780
777
  const size = layout[index];
781
- if (size != null) {
782
- var _getPercentageSizeFro;
778
+ if (size != null && panelData.constraints.collapsible) {
779
+ var _getPercentageSizeFro, _getPercentageSizeFro2;
783
780
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
784
- const minSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
781
+ const collapsedSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
782
+ sizePercentage: panelData.constraints.collapsedSizePercentage,
783
+ sizePixels: panelData.constraints.collapsedSizePixels
784
+ }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
785
+ const minSize = (_getPercentageSizeFro2 = getPercentageSizeFromMixedSizes({
785
786
  sizePercentage: panelData.constraints.minSizePercentage,
786
787
  sizePixels: panelData.constraints.minSizePixels
787
- }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
788
- let delta = 0;
789
- if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
790
- delta = direction === "horizontal" ? width : height;
791
- } else {
792
- delta = -(direction === "horizontal" ? width : height);
793
- }
788
+ }, groupSizePixels)) !== null && _getPercentageSizeFro2 !== void 0 ? _getPercentageSizeFro2 : 0;
794
789
  const nextLayout = adjustLayoutByDelta({
795
- delta,
790
+ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size,
796
791
  groupSizePixels,
797
792
  layout,
798
793
  panelConstraints: panelDataArray.map(panelData => panelData.constraints),
@@ -1101,6 +1096,10 @@ function debounce(callback, durationMs = 10) {
1101
1096
  return callable;
1102
1097
  }
1103
1098
 
1099
+ function getPanelElementsForGroup(groupId) {
1100
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1101
+ }
1102
+
1104
1103
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
1105
1104
  // or on a browser with cookies/storage disabled.
1106
1105
  // In either case, this function avoids accessing localStorage until needed,
@@ -1250,7 +1249,7 @@ const defaultStorage = {
1250
1249
  };
1251
1250
  const debounceMap = {};
1252
1251
  function PanelGroupWithForwardedRef({
1253
- autoSaveId,
1252
+ autoSaveId = null,
1254
1253
  children,
1255
1254
  className: classNameFromProps = "",
1256
1255
  dataAttributes,
@@ -1267,11 +1266,11 @@ function PanelGroupWithForwardedRef({
1267
1266
  const groupId = useUniqueId(idFromProps);
1268
1267
  const [dragState, setDragState] = useState(null);
1269
1268
  const [layout, setLayout] = useState([]);
1270
- const [panelDataArray, setPanelDataArray] = useState([]);
1271
1269
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1272
1270
  const panelSizeBeforeCollapseRef = useRef(new Map());
1273
1271
  const prevDeltaRef = useRef(0);
1274
1272
  const committedValuesRef = useRef({
1273
+ autoSaveId,
1275
1274
  direction,
1276
1275
  dragState,
1277
1276
  id: groupId,
@@ -1279,7 +1278,8 @@ function PanelGroupWithForwardedRef({
1279
1278
  keyboardResizeByPixels,
1280
1279
  layout,
1281
1280
  onLayout,
1282
- panelDataArray
1281
+ panelDataArray: [],
1282
+ storage
1283
1283
  });
1284
1284
  useRef({
1285
1285
  didLogIdAndOrderWarning: false,
@@ -1317,6 +1317,7 @@ function PanelGroupWithForwardedRef({
1317
1317
  });
1318
1318
  if (!areEqual(prevLayout, safeLayout)) {
1319
1319
  setLayout(safeLayout);
1320
+ committedValuesRef.current.layout = safeLayout;
1320
1321
  if (onLayout) {
1321
1322
  onLayout(safeLayout.map(sizePercentage => ({
1322
1323
  sizePercentage,
@@ -1328,21 +1329,29 @@ function PanelGroupWithForwardedRef({
1328
1329
  }
1329
1330
  }), []);
1330
1331
  useIsomorphicLayoutEffect(() => {
1332
+ committedValuesRef.current.autoSaveId = autoSaveId;
1331
1333
  committedValuesRef.current.direction = direction;
1332
1334
  committedValuesRef.current.dragState = dragState;
1333
1335
  committedValuesRef.current.id = groupId;
1334
- committedValuesRef.current.layout = layout;
1335
1336
  committedValuesRef.current.onLayout = onLayout;
1336
- 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
1337
1341
  });
1342
+
1338
1343
  useWindowSplitterPanelGroupBehavior({
1339
1344
  committedValuesRef,
1340
1345
  groupId,
1341
1346
  layout,
1342
- panelDataArray,
1347
+ panelDataArray: committedValuesRef.current.panelDataArray,
1343
1348
  setLayout
1344
1349
  });
1345
1350
  useEffect(() => {
1351
+ const {
1352
+ panelDataArray
1353
+ } = committedValuesRef.current;
1354
+
1346
1355
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1347
1356
  if (autoSaveId) {
1348
1357
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1355,59 +1364,11 @@ function PanelGroupWithForwardedRef({
1355
1364
  }
1356
1365
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1357
1366
  }
1358
- }, [autoSaveId, layout, panelDataArray, storage]);
1359
-
1360
- // Once all panels have registered themselves,
1361
- // Compute the initial sizes based on default weights.
1362
- // This assumes that panels register during initial mount (no conditional rendering)!
1367
+ }, [autoSaveId, layout, storage]);
1363
1368
  useIsomorphicLayoutEffect(() => {
1364
1369
  const {
1365
- id: groupId,
1366
- layout,
1367
- onLayout
1370
+ panelDataArray
1368
1371
  } = committedValuesRef.current;
1369
- if (layout.length === panelDataArray.length) {
1370
- // Only compute (or restore) default layout once per panel configuration.
1371
- return;
1372
- }
1373
-
1374
- // If this panel has been configured to persist sizing information,
1375
- // default size should be restored from local storage if possible.
1376
- let unsafeLayout = null;
1377
- if (autoSaveId) {
1378
- unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1379
- }
1380
- const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1381
- if (groupSizePixels <= 0) {
1382
- // Wait until the group has rendered a non-zero size before computing layout.
1383
- return;
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
1372
  const constraints = panelDataArray.map(({
1412
1373
  constraints
1413
1374
  }) => constraints);
@@ -1431,6 +1392,7 @@ function PanelGroupWithForwardedRef({
1431
1392
  });
1432
1393
  if (!areEqual(prevLayout, nextLayout)) {
1433
1394
  setLayout(nextLayout);
1395
+ committedValuesRef.current.layout = nextLayout;
1434
1396
  if (onLayout) {
1435
1397
  onLayout(nextLayout.map(sizePercentage => ({
1436
1398
  sizePercentage,
@@ -1445,7 +1407,7 @@ function PanelGroupWithForwardedRef({
1445
1407
  resizeObserver.disconnect();
1446
1408
  };
1447
1409
  }
1448
- }, [groupId, panelDataArray]);
1410
+ }, [groupId]);
1449
1411
 
1450
1412
  // DEV warnings
1451
1413
  useEffect(() => {
@@ -1482,6 +1444,7 @@ function PanelGroupWithForwardedRef({
1482
1444
  });
1483
1445
  if (!compareLayouts(prevLayout, nextLayout)) {
1484
1446
  setLayout(nextLayout);
1447
+ committedValuesRef.current.layout = nextLayout;
1485
1448
  if (onLayout) {
1486
1449
  onLayout(nextLayout.map(sizePercentage => ({
1487
1450
  sizePercentage,
@@ -1526,6 +1489,7 @@ function PanelGroupWithForwardedRef({
1526
1489
  });
1527
1490
  if (!compareLayouts(prevLayout, nextLayout)) {
1528
1491
  setLayout(nextLayout);
1492
+ committedValuesRef.current.layout = nextLayout;
1529
1493
  if (onLayout) {
1530
1494
  onLayout(nextLayout.map(sizePercentage => ({
1531
1495
  sizePercentage,
@@ -1556,6 +1520,9 @@ function PanelGroupWithForwardedRef({
1556
1520
 
1557
1521
  // This API should never read from committedValuesRef
1558
1522
  const getPanelStyle = useCallback(panelData => {
1523
+ const {
1524
+ panelDataArray
1525
+ } = committedValuesRef.current;
1559
1526
  const panelIndex = panelDataArray.indexOf(panelData);
1560
1527
  return computePanelFlexBoxStyle({
1561
1528
  dragState,
@@ -1563,7 +1530,7 @@ function PanelGroupWithForwardedRef({
1563
1530
  panelData: panelDataArray,
1564
1531
  panelIndex
1565
1532
  });
1566
- }, [dragState, layout, panelDataArray]);
1533
+ }, [dragState, layout]);
1567
1534
 
1568
1535
  // External APIs are safe to memoize via committed values ref
1569
1536
  const isPanelCollapsed = useCallback(panelData => {
@@ -1593,22 +1560,76 @@ function PanelGroupWithForwardedRef({
1593
1560
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1594
1561
  }, [groupId]);
1595
1562
  const registerPanel = useCallback(panelData => {
1596
- setPanelDataArray(prevPanelDataArray => {
1597
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1598
- return nextPanelDataArray.sort((panelA, panelB) => {
1599
- const orderA = panelA.order;
1600
- const orderB = panelB.order;
1601
- if (orderA == null && orderB == null) {
1602
- return 0;
1603
- } else if (orderA == null) {
1604
- return -1;
1605
- } else if (orderB == null) {
1606
- return 1;
1607
- } else {
1608
- return orderA - orderB;
1609
- }
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
1610
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)
1611
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
+ }
1612
1633
  }, []);
1613
1634
  const registerResizeHandle = useCallback(dragHandleId => {
1614
1635
  return function resizeHandler(event) {
@@ -1678,6 +1699,7 @@ function PanelGroupWithForwardedRef({
1678
1699
  }
1679
1700
  if (layoutChanged) {
1680
1701
  setLayout(nextLayout);
1702
+ committedValuesRef.current.layout = nextLayout;
1681
1703
  if (onLayout) {
1682
1704
  onLayout(nextLayout.map(sizePercentage => ({
1683
1705
  sizePercentage,
@@ -1715,6 +1737,7 @@ function PanelGroupWithForwardedRef({
1715
1737
  });
1716
1738
  if (!compareLayouts(prevLayout, nextLayout)) {
1717
1739
  setLayout(nextLayout);
1740
+ committedValuesRef.current.layout = nextLayout;
1718
1741
  if (onLayout) {
1719
1742
  onLayout(nextLayout.map(sizePercentage => ({
1720
1743
  sizePercentage,
@@ -1742,16 +1765,84 @@ function PanelGroupWithForwardedRef({
1742
1765
  resetGlobalCursorStyle();
1743
1766
  setDragState(null);
1744
1767
  }, []);
1768
+ const unregisterPanelRef = useRef({
1769
+ pendingPanelIds: new Set(),
1770
+ timeout: null
1771
+ });
1745
1772
  const unregisterPanel = useCallback(panelData => {
1746
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1747
- setPanelDataArray(panelDataArray => {
1748
- const index = panelDataArray.indexOf(panelData);
1749
- if (index >= 0) {
1750
- panelDataArray = [...panelDataArray];
1751
- 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;
1752
1816
  }
1753
- return panelDataArray;
1754
- });
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
+ });
1826
+
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);
1844
+ }
1845
+ }, 0);
1755
1846
  }, []);
1756
1847
  const context = useMemo(() => ({
1757
1848
  collapsePanel,