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.
@@ -91,6 +91,7 @@ function PanelWithForwardedRef({
91
91
  expandPanel,
92
92
  getPanelSize,
93
93
  getPanelStyle,
94
+ groupId,
94
95
  isPanelCollapsed,
95
96
  registerPanel,
96
97
  resizePanel,
@@ -167,6 +168,7 @@ function PanelWithForwardedRef({
167
168
  // CSS selectors
168
169
  "data-panel": "",
169
170
  "data-panel-id": panelId,
171
+ "data-panel-group-id": groupId,
170
172
  // e2e test attributes
171
173
  "data-panel-collapsible": collapsible || undefined ,
172
174
  "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
@@ -179,8 +181,6 @@ const Panel = forwardRef((props, ref) => createElement(PanelWithForwardedRef, {
179
181
  PanelWithForwardedRef.displayName = "Panel";
180
182
  Panel.displayName = "forwardRef(Panel)";
181
183
 
182
- const PRECISION = 10;
183
-
184
184
  function convertPixelsToPercentage(pixels, groupSizePixels) {
185
185
  return pixels / groupSizePixels * 100;
186
186
  }
@@ -258,6 +258,8 @@ function computePercentagePanelConstraints(panelConstraintsArray, panelIndex, gr
258
258
  };
259
259
  }
260
260
 
261
+ const PRECISION = 10;
262
+
261
263
  function fuzzyCompareNumbers(actual, expected, fractionDigits = PRECISION) {
262
264
  actual = parseFloat(actual.toFixed(fractionDigits));
263
265
  expected = parseFloat(expected.toFixed(fractionDigits));
@@ -651,15 +653,10 @@ function useWindowSplitterPanelGroupBehavior({
651
653
  });
652
654
  useEffect(() => {
653
655
  const {
654
- direction,
655
656
  panelDataArray
656
657
  } = committedValuesRef.current;
657
658
  const groupElement = getPanelGroupElement(groupId);
658
659
  assert(groupElement != null, `No group found for id "${groupId}"`);
659
- const {
660
- height,
661
- width
662
- } = groupElement.getBoundingClientRect();
663
660
  const handles = getResizeHandleElementsForGroup(groupId);
664
661
  const cleanupFunctions = handles.map(handle => {
665
662
  const handleId = handle.getAttribute("data-panel-resize-handle-id");
@@ -679,21 +676,19 @@ function useWindowSplitterPanelGroupBehavior({
679
676
  if (index >= 0) {
680
677
  const panelData = panelDataArray[index];
681
678
  const size = layout[index];
682
- if (size != null) {
683
- var _getPercentageSizeFro;
679
+ if (size != null && panelData.constraints.collapsible) {
680
+ var _getPercentageSizeFro, _getPercentageSizeFro2;
684
681
  const groupSizePixels = getAvailableGroupSizePixels(groupId);
685
- const minSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
682
+ const collapsedSize = (_getPercentageSizeFro = getPercentageSizeFromMixedSizes({
683
+ sizePercentage: panelData.constraints.collapsedSizePercentage,
684
+ sizePixels: panelData.constraints.collapsedSizePixels
685
+ }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
686
+ const minSize = (_getPercentageSizeFro2 = getPercentageSizeFromMixedSizes({
686
687
  sizePercentage: panelData.constraints.minSizePercentage,
687
688
  sizePixels: panelData.constraints.minSizePixels
688
- }, groupSizePixels)) !== null && _getPercentageSizeFro !== void 0 ? _getPercentageSizeFro : 0;
689
- let delta = 0;
690
- if (size.toPrecision(PRECISION) <= minSize.toPrecision(PRECISION)) {
691
- delta = direction === "horizontal" ? width : height;
692
- } else {
693
- delta = -(direction === "horizontal" ? width : height);
694
- }
689
+ }, groupSizePixels)) !== null && _getPercentageSizeFro2 !== void 0 ? _getPercentageSizeFro2 : 0;
695
690
  const nextLayout = adjustLayoutByDelta({
696
- delta,
691
+ delta: fuzzyNumbersEqual(size, collapsedSize) ? minSize - collapsedSize : collapsedSize - size,
697
692
  groupSizePixels,
698
693
  layout,
699
694
  panelConstraints: panelDataArray.map(panelData => panelData.constraints),
@@ -814,6 +809,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
814
809
  }
815
810
  }
816
811
 
812
+ function calculateUnsafeDefaultLayout({
813
+ groupSizePixels,
814
+ panelDataArray
815
+ }) {
816
+ const layout = Array(panelDataArray.length);
817
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
818
+ let numPanelsWithSizes = 0;
819
+ let remainingSize = 100;
820
+
821
+ // Distribute default sizes first
822
+ for (let index = 0; index < panelDataArray.length; index++) {
823
+ const {
824
+ defaultSizePercentage
825
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
826
+ if (defaultSizePercentage != null) {
827
+ numPanelsWithSizes++;
828
+ layout[index] = defaultSizePercentage;
829
+ remainingSize -= defaultSizePercentage;
830
+ }
831
+ }
832
+
833
+ // Remaining size should be distributed evenly between panels without default sizes
834
+ for (let index = 0; index < panelDataArray.length; index++) {
835
+ const {
836
+ defaultSizePercentage
837
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
838
+ if (defaultSizePercentage != null) {
839
+ continue;
840
+ }
841
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
842
+ const size = remainingSize / numRemainingPanels;
843
+ numPanelsWithSizes++;
844
+ layout[index] = size;
845
+ remainingSize -= size;
846
+ }
847
+ return layout;
848
+ }
849
+
817
850
  function convertPercentageToPixels(percentage, groupSizePixels) {
818
851
  return percentage / 100 * groupSizePixels;
819
852
  }
@@ -964,6 +997,10 @@ function debounce(callback, durationMs = 10) {
964
997
  return callable;
965
998
  }
966
999
 
1000
+ function getPanelElementsForGroup(groupId) {
1001
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
1002
+ }
1003
+
967
1004
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
968
1005
  // or on a browser with cookies/storage disabled.
969
1006
  // In either case, this function avoids accessing localStorage until needed,
@@ -1019,6 +1056,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
1019
1056
  } catch (error) {}
1020
1057
  return null;
1021
1058
  }
1059
+ function loadPanelLayout(autoSaveId, panels, storage) {
1060
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1061
+ if (state) {
1062
+ var _state$key;
1063
+ const key = getSerializationKey(panels);
1064
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1065
+ }
1066
+ return null;
1067
+ }
1022
1068
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1023
1069
  const key = getSerializationKey(panels);
1024
1070
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -1030,6 +1076,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1030
1076
  }
1031
1077
  }
1032
1078
 
1079
+ function shouldMonitorPixelBasedConstraints(constraints) {
1080
+ return constraints.some(constraints => {
1081
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1082
+ });
1083
+ }
1084
+
1033
1085
  function validatePanelConstraints({
1034
1086
  groupSizePixels,
1035
1087
  panelConstraints,
@@ -1175,7 +1227,7 @@ const defaultStorage = {
1175
1227
  };
1176
1228
  const debounceMap = {};
1177
1229
  function PanelGroupWithForwardedRef({
1178
- autoSaveId,
1230
+ autoSaveId = null,
1179
1231
  children,
1180
1232
  className: classNameFromProps = "",
1181
1233
  dataAttributes,
@@ -1192,11 +1244,11 @@ function PanelGroupWithForwardedRef({
1192
1244
  const groupId = useUniqueId(idFromProps);
1193
1245
  const [dragState, setDragState] = useState(null);
1194
1246
  const [layout, setLayout] = useState([]);
1195
- const [panelDataArray, setPanelDataArray] = useState([]);
1196
1247
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1197
1248
  const panelSizeBeforeCollapseRef = useRef(new Map());
1198
1249
  const prevDeltaRef = useRef(0);
1199
1250
  const committedValuesRef = useRef({
1251
+ autoSaveId,
1200
1252
  direction,
1201
1253
  dragState,
1202
1254
  id: groupId,
@@ -1204,7 +1256,8 @@ function PanelGroupWithForwardedRef({
1204
1256
  keyboardResizeByPixels,
1205
1257
  layout,
1206
1258
  onLayout,
1207
- panelDataArray
1259
+ panelDataArray: [],
1260
+ storage
1208
1261
  });
1209
1262
  const devWarningsRef = useRef({
1210
1263
  didLogIdAndOrderWarning: false,
@@ -1242,6 +1295,7 @@ function PanelGroupWithForwardedRef({
1242
1295
  });
1243
1296
  if (!areEqual(prevLayout, safeLayout)) {
1244
1297
  setLayout(safeLayout);
1298
+ committedValuesRef.current.layout = safeLayout;
1245
1299
  if (onLayout) {
1246
1300
  onLayout(safeLayout.map(sizePercentage => ({
1247
1301
  sizePercentage,
@@ -1252,14 +1306,19 @@ function PanelGroupWithForwardedRef({
1252
1306
  }
1253
1307
  }
1254
1308
  }), []);
1309
+
1255
1310
  useWindowSplitterPanelGroupBehavior({
1256
1311
  committedValuesRef,
1257
1312
  groupId,
1258
1313
  layout,
1259
- panelDataArray,
1314
+ panelDataArray: committedValuesRef.current.panelDataArray,
1260
1315
  setLayout
1261
1316
  });
1262
1317
  useEffect(() => {
1318
+ const {
1319
+ panelDataArray
1320
+ } = committedValuesRef.current;
1321
+
1263
1322
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1264
1323
  if (autoSaveId) {
1265
1324
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1272,20 +1331,20 @@ function PanelGroupWithForwardedRef({
1272
1331
  }
1273
1332
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1274
1333
  }
1275
- }, [autoSaveId, layout, panelDataArray, storage]);
1334
+ }, [autoSaveId, layout, storage]);
1276
1335
 
1277
1336
  // DEV warnings
1278
1337
  useEffect(() => {
1279
1338
  {
1339
+ const {
1340
+ panelDataArray
1341
+ } = committedValuesRef.current;
1280
1342
  const {
1281
1343
  didLogIdAndOrderWarning,
1282
1344
  didLogPanelConstraintsWarning,
1283
1345
  prevPanelIds
1284
1346
  } = devWarningsRef.current;
1285
1347
  if (!didLogIdAndOrderWarning) {
1286
- const {
1287
- panelDataArray
1288
- } = committedValuesRef.current;
1289
1348
  const panelIds = panelDataArray.map(({
1290
1349
  id
1291
1350
  }) => id);
@@ -1351,6 +1410,7 @@ function PanelGroupWithForwardedRef({
1351
1410
  });
1352
1411
  if (!compareLayouts(prevLayout, nextLayout)) {
1353
1412
  setLayout(nextLayout);
1413
+ committedValuesRef.current.layout = nextLayout;
1354
1414
  if (onLayout) {
1355
1415
  onLayout(nextLayout.map(sizePercentage => ({
1356
1416
  sizePercentage,
@@ -1395,6 +1455,7 @@ function PanelGroupWithForwardedRef({
1395
1455
  });
1396
1456
  if (!compareLayouts(prevLayout, nextLayout)) {
1397
1457
  setLayout(nextLayout);
1458
+ committedValuesRef.current.layout = nextLayout;
1398
1459
  if (onLayout) {
1399
1460
  onLayout(nextLayout.map(sizePercentage => ({
1400
1461
  sizePercentage,
@@ -1425,6 +1486,9 @@ function PanelGroupWithForwardedRef({
1425
1486
 
1426
1487
  // This API should never read from committedValuesRef
1427
1488
  const getPanelStyle = useCallback(panelData => {
1489
+ const {
1490
+ panelDataArray
1491
+ } = committedValuesRef.current;
1428
1492
  const panelIndex = panelDataArray.indexOf(panelData);
1429
1493
  return computePanelFlexBoxStyle({
1430
1494
  dragState,
@@ -1432,7 +1496,7 @@ function PanelGroupWithForwardedRef({
1432
1496
  panelData: panelDataArray,
1433
1497
  panelIndex
1434
1498
  });
1435
- }, [dragState, layout, panelDataArray]);
1499
+ }, [dragState, layout]);
1436
1500
 
1437
1501
  // External APIs are safe to memoize via committed values ref
1438
1502
  const isPanelCollapsed = useCallback(panelData => {
@@ -1462,22 +1526,76 @@ function PanelGroupWithForwardedRef({
1462
1526
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1463
1527
  }, [groupId]);
1464
1528
  const registerPanel = useCallback(panelData => {
1465
- setPanelDataArray(prevPanelDataArray => {
1466
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1467
- return nextPanelDataArray.sort((panelA, panelB) => {
1468
- const orderA = panelA.order;
1469
- const orderB = panelB.order;
1470
- if (orderA == null && orderB == null) {
1471
- return 0;
1472
- } else if (orderA == null) {
1473
- return -1;
1474
- } else if (orderB == null) {
1475
- return 1;
1476
- } else {
1477
- return orderA - orderB;
1478
- }
1529
+ const {
1530
+ autoSaveId,
1531
+ id: groupId,
1532
+ layout: prevLayout,
1533
+ onLayout,
1534
+ panelDataArray,
1535
+ storage
1536
+ } = committedValuesRef.current;
1537
+ panelDataArray.push(panelData);
1538
+ panelDataArray.sort((panelA, panelB) => {
1539
+ const orderA = panelA.order;
1540
+ const orderB = panelB.order;
1541
+ if (orderA == null && orderB == null) {
1542
+ return 0;
1543
+ } else if (orderA == null) {
1544
+ return -1;
1545
+ } else if (orderB == null) {
1546
+ return 1;
1547
+ } else {
1548
+ return orderA - orderB;
1549
+ }
1550
+ });
1551
+
1552
+ // Wait until all panels have registered before we try to compute layout;
1553
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1554
+ const panelElements = getPanelElementsForGroup(groupId);
1555
+ if (panelElements.length !== panelDataArray.length) {
1556
+ return;
1557
+ }
1558
+
1559
+ // If this panel has been configured to persist sizing information,
1560
+ // default size should be restored from local storage if possible.
1561
+ let unsafeLayout = null;
1562
+ if (autoSaveId) {
1563
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1564
+ }
1565
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1566
+ if (groupSizePixels <= 0) {
1567
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1568
+ constraints
1569
+ }) => constraints))) {
1570
+ // Wait until the group has rendered a non-zero size before computing layout.
1571
+ return;
1572
+ }
1573
+ }
1574
+ if (unsafeLayout == null) {
1575
+ unsafeLayout = calculateUnsafeDefaultLayout({
1576
+ groupSizePixels,
1577
+ panelDataArray
1479
1578
  });
1579
+ }
1580
+
1581
+ // Validate even saved layouts in case something has changed since last render
1582
+ // e.g. for pixel groups, this could be the size of the window
1583
+ const nextLayout = validatePanelGroupLayout({
1584
+ groupSizePixels,
1585
+ layout: unsafeLayout,
1586
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1480
1587
  });
1588
+ if (!areEqual(prevLayout, nextLayout)) {
1589
+ setLayout(nextLayout);
1590
+ committedValuesRef.current.layout = nextLayout;
1591
+ if (onLayout) {
1592
+ onLayout(nextLayout.map(sizePercentage => ({
1593
+ sizePercentage,
1594
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1595
+ })));
1596
+ }
1597
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1598
+ }
1481
1599
  }, []);
1482
1600
  const registerResizeHandle = useCallback(dragHandleId => {
1483
1601
  return function resizeHandler(event) {
@@ -1547,6 +1665,7 @@ function PanelGroupWithForwardedRef({
1547
1665
  }
1548
1666
  if (layoutChanged) {
1549
1667
  setLayout(nextLayout);
1668
+ committedValuesRef.current.layout = nextLayout;
1550
1669
  if (onLayout) {
1551
1670
  onLayout(nextLayout.map(sizePercentage => ({
1552
1671
  sizePercentage,
@@ -1584,6 +1703,7 @@ function PanelGroupWithForwardedRef({
1584
1703
  });
1585
1704
  if (!compareLayouts(prevLayout, nextLayout)) {
1586
1705
  setLayout(nextLayout);
1706
+ committedValuesRef.current.layout = nextLayout;
1587
1707
  if (onLayout) {
1588
1708
  onLayout(nextLayout.map(sizePercentage => ({
1589
1709
  sizePercentage,
@@ -1611,16 +1731,84 @@ function PanelGroupWithForwardedRef({
1611
1731
  resetGlobalCursorStyle();
1612
1732
  setDragState(null);
1613
1733
  }, []);
1734
+ const unregisterPanelRef = useRef({
1735
+ pendingPanelIds: new Set(),
1736
+ timeout: null
1737
+ });
1614
1738
  const unregisterPanel = useCallback(panelData => {
1615
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1616
- setPanelDataArray(panelDataArray => {
1617
- const index = panelDataArray.indexOf(panelData);
1618
- if (index >= 0) {
1619
- panelDataArray = [...panelDataArray];
1620
- panelDataArray.splice(index, 1);
1739
+ const {
1740
+ id: groupId,
1741
+ layout: prevLayout,
1742
+ onLayout,
1743
+ panelDataArray
1744
+ } = committedValuesRef.current;
1745
+ const index = panelDataArray.indexOf(panelData);
1746
+ if (index >= 0) {
1747
+ panelDataArray.splice(index, 1);
1748
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1749
+ }
1750
+ if (unregisterPanelRef.current.timeout != null) {
1751
+ clearTimeout(unregisterPanelRef.current.timeout);
1752
+ }
1753
+
1754
+ // Batch panel unmounts so that we only calculate layout once;
1755
+ // This is more efficient and avoids misleading warnings in development mode.
1756
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1757
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1758
+ const {
1759
+ pendingPanelIds
1760
+ } = unregisterPanelRef.current;
1761
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1762
+
1763
+ // TRICKY
1764
+ // Strict effects mode
1765
+ let unmountDueToStrictMode = false;
1766
+ pendingPanelIds.forEach(panelId => {
1767
+ pendingPanelIds.delete(panelId);
1768
+ if (panelDataArray.find(({
1769
+ id
1770
+ }) => id === panelId) == null) {
1771
+ unmountDueToStrictMode = true;
1772
+
1773
+ // TRICKY
1774
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1775
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1776
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1777
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1778
+ }
1779
+ });
1780
+ if (!unmountDueToStrictMode) {
1781
+ return;
1621
1782
  }
1622
- return panelDataArray;
1623
- });
1783
+ if (panelDataArray.length === 0) {
1784
+ // The group is unmounting; skip layout calculation.
1785
+ return;
1786
+ }
1787
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1788
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1789
+ groupSizePixels,
1790
+ panelDataArray
1791
+ });
1792
+
1793
+ // Validate even saved layouts in case something has changed since last render
1794
+ // e.g. for pixel groups, this could be the size of the window
1795
+ const nextLayout = validatePanelGroupLayout({
1796
+ groupSizePixels,
1797
+ layout: unsafeLayout,
1798
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1799
+ });
1800
+ if (!areEqual(prevLayout, nextLayout)) {
1801
+ setLayout(nextLayout);
1802
+ committedValuesRef.current.layout = nextLayout;
1803
+ if (onLayout) {
1804
+ onLayout(nextLayout.map(sizePercentage => ({
1805
+ sizePercentage,
1806
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1807
+ })));
1808
+ }
1809
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1810
+ }
1811
+ }, 0);
1624
1812
  }, []);
1625
1813
  const context = useMemo(() => ({
1626
1814
  collapsePanel,