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.
@@ -67,6 +67,7 @@ function PanelWithForwardedRef({
67
67
  expandPanel,
68
68
  getPanelSize,
69
69
  getPanelStyle,
70
+ groupId,
70
71
  isPanelCollapsed,
71
72
  registerPanel,
72
73
  resizePanel,
@@ -143,6 +144,7 @@ function PanelWithForwardedRef({
143
144
  // CSS selectors
144
145
  "data-panel": "",
145
146
  "data-panel-id": panelId,
147
+ "data-panel-group-id": groupId,
146
148
  // e2e test attributes
147
149
  "data-panel-collapsible": collapsible || undefined ,
148
150
  "data-panel-size": parseFloat("" + style.flexGrow).toFixed(1)
@@ -783,6 +785,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
783
785
  }
784
786
  }
785
787
 
788
+ function calculateUnsafeDefaultLayout({
789
+ groupSizePixels,
790
+ panelDataArray
791
+ }) {
792
+ const layout = Array(panelDataArray.length);
793
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
794
+ let numPanelsWithSizes = 0;
795
+ let remainingSize = 100;
796
+
797
+ // Distribute default sizes first
798
+ for (let index = 0; index < panelDataArray.length; index++) {
799
+ const {
800
+ defaultSizePercentage
801
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
802
+ if (defaultSizePercentage != null) {
803
+ numPanelsWithSizes++;
804
+ layout[index] = defaultSizePercentage;
805
+ remainingSize -= defaultSizePercentage;
806
+ }
807
+ }
808
+
809
+ // Remaining size should be distributed evenly between panels without default sizes
810
+ for (let index = 0; index < panelDataArray.length; index++) {
811
+ const {
812
+ defaultSizePercentage
813
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
814
+ if (defaultSizePercentage != null) {
815
+ continue;
816
+ }
817
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
818
+ const size = remainingSize / numRemainingPanels;
819
+ numPanelsWithSizes++;
820
+ layout[index] = size;
821
+ remainingSize -= size;
822
+ }
823
+ return layout;
824
+ }
825
+
786
826
  function convertPercentageToPixels(percentage, groupSizePixels) {
787
827
  return percentage / 100 * groupSizePixels;
788
828
  }
@@ -933,6 +973,10 @@ function debounce(callback, durationMs = 10) {
933
973
  return callable;
934
974
  }
935
975
 
976
+ function getPanelElementsForGroup(groupId) {
977
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
978
+ }
979
+
936
980
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
937
981
  // or on a browser with cookies/storage disabled.
938
982
  // In either case, this function avoids accessing localStorage until needed,
@@ -988,6 +1032,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
988
1032
  } catch (error) {}
989
1033
  return null;
990
1034
  }
1035
+ function loadPanelLayout(autoSaveId, panels, storage) {
1036
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1037
+ if (state) {
1038
+ var _state$key;
1039
+ const key = getSerializationKey(panels);
1040
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1041
+ }
1042
+ return null;
1043
+ }
991
1044
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
992
1045
  const key = getSerializationKey(panels);
993
1046
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -999,6 +1052,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
999
1052
  }
1000
1053
  }
1001
1054
 
1055
+ function shouldMonitorPixelBasedConstraints(constraints) {
1056
+ return constraints.some(constraints => {
1057
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1058
+ });
1059
+ }
1060
+
1002
1061
  function validatePanelConstraints({
1003
1062
  groupSizePixels,
1004
1063
  panelConstraints,
@@ -1144,7 +1203,7 @@ const defaultStorage = {
1144
1203
  };
1145
1204
  const debounceMap = {};
1146
1205
  function PanelGroupWithForwardedRef({
1147
- autoSaveId,
1206
+ autoSaveId = null,
1148
1207
  children,
1149
1208
  className: classNameFromProps = "",
1150
1209
  dataAttributes,
@@ -1161,12 +1220,11 @@ function PanelGroupWithForwardedRef({
1161
1220
  const groupId = useUniqueId(idFromProps);
1162
1221
  const [dragState, setDragState] = useState(null);
1163
1222
  const [layout, setLayout] = useState([]);
1164
- const [panelDataArray, setPanelDataArray] = useState([]);
1165
1223
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1166
1224
  const panelSizeBeforeCollapseRef = useRef(new Map());
1167
1225
  const prevDeltaRef = useRef(0);
1168
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1169
1226
  const committedValuesRef = useRef({
1227
+ autoSaveId,
1170
1228
  direction,
1171
1229
  dragState,
1172
1230
  id: groupId,
@@ -1174,7 +1232,8 @@ function PanelGroupWithForwardedRef({
1174
1232
  keyboardResizeByPixels,
1175
1233
  layout,
1176
1234
  onLayout,
1177
- panelDataArray
1235
+ panelDataArray: [],
1236
+ storage
1178
1237
  });
1179
1238
  const devWarningsRef = useRef({
1180
1239
  didLogIdAndOrderWarning: false,
@@ -1212,6 +1271,7 @@ function PanelGroupWithForwardedRef({
1212
1271
  });
1213
1272
  if (!areEqual(prevLayout, safeLayout)) {
1214
1273
  setLayout(safeLayout);
1274
+ committedValuesRef.current.layout = safeLayout;
1215
1275
  if (onLayout) {
1216
1276
  onLayout(safeLayout.map(sizePercentage => ({
1217
1277
  sizePercentage,
@@ -1222,14 +1282,19 @@ function PanelGroupWithForwardedRef({
1222
1282
  }
1223
1283
  }
1224
1284
  }), []);
1285
+
1225
1286
  useWindowSplitterPanelGroupBehavior({
1226
1287
  committedValuesRef,
1227
1288
  groupId,
1228
1289
  layout,
1229
- panelDataArray,
1290
+ panelDataArray: committedValuesRef.current.panelDataArray,
1230
1291
  setLayout
1231
1292
  });
1232
1293
  useEffect(() => {
1294
+ const {
1295
+ panelDataArray
1296
+ } = committedValuesRef.current;
1297
+
1233
1298
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1234
1299
  if (autoSaveId) {
1235
1300
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1242,20 +1307,20 @@ function PanelGroupWithForwardedRef({
1242
1307
  }
1243
1308
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1244
1309
  }
1245
- }, [autoSaveId, layout, panelDataArray, storage]);
1310
+ }, [autoSaveId, layout, storage]);
1246
1311
 
1247
1312
  // DEV warnings
1248
1313
  useEffect(() => {
1249
1314
  {
1315
+ const {
1316
+ panelDataArray
1317
+ } = committedValuesRef.current;
1250
1318
  const {
1251
1319
  didLogIdAndOrderWarning,
1252
1320
  didLogPanelConstraintsWarning,
1253
1321
  prevPanelIds
1254
1322
  } = devWarningsRef.current;
1255
1323
  if (!didLogIdAndOrderWarning) {
1256
- const {
1257
- panelDataArray
1258
- } = committedValuesRef.current;
1259
1324
  const panelIds = panelDataArray.map(({
1260
1325
  id
1261
1326
  }) => id);
@@ -1297,17 +1362,6 @@ function PanelGroupWithForwardedRef({
1297
1362
  onLayout,
1298
1363
  panelDataArray
1299
1364
  } = committedValuesRef.current;
1300
-
1301
- // See issues/211
1302
- if (panelDataArray.find(({
1303
- id
1304
- }) => id === panelData.id) == null) {
1305
- setImperativeApiQueue(prev => [...prev, {
1306
- panelData,
1307
- type: "collapse"
1308
- }]);
1309
- return;
1310
- }
1311
1365
  if (panelData.constraints.collapsible) {
1312
1366
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1313
1367
  const {
@@ -1332,6 +1386,7 @@ function PanelGroupWithForwardedRef({
1332
1386
  });
1333
1387
  if (!compareLayouts(prevLayout, nextLayout)) {
1334
1388
  setLayout(nextLayout);
1389
+ committedValuesRef.current.layout = nextLayout;
1335
1390
  if (onLayout) {
1336
1391
  onLayout(nextLayout.map(sizePercentage => ({
1337
1392
  sizePercentage,
@@ -1351,17 +1406,6 @@ function PanelGroupWithForwardedRef({
1351
1406
  onLayout,
1352
1407
  panelDataArray
1353
1408
  } = committedValuesRef.current;
1354
-
1355
- // See issues/211
1356
- if (panelDataArray.find(({
1357
- id
1358
- }) => id === panelData.id) == null) {
1359
- setImperativeApiQueue(prev => [...prev, {
1360
- panelData,
1361
- type: "expand"
1362
- }]);
1363
- return;
1364
- }
1365
1409
  if (panelData.constraints.collapsible) {
1366
1410
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1367
1411
  const {
@@ -1387,6 +1431,7 @@ function PanelGroupWithForwardedRef({
1387
1431
  });
1388
1432
  if (!compareLayouts(prevLayout, nextLayout)) {
1389
1433
  setLayout(nextLayout);
1434
+ committedValuesRef.current.layout = nextLayout;
1390
1435
  if (onLayout) {
1391
1436
  onLayout(nextLayout.map(sizePercentage => ({
1392
1437
  sizePercentage,
@@ -1417,6 +1462,9 @@ function PanelGroupWithForwardedRef({
1417
1462
 
1418
1463
  // This API should never read from committedValuesRef
1419
1464
  const getPanelStyle = useCallback(panelData => {
1465
+ const {
1466
+ panelDataArray
1467
+ } = committedValuesRef.current;
1420
1468
  const panelIndex = panelDataArray.indexOf(panelData);
1421
1469
  return computePanelFlexBoxStyle({
1422
1470
  dragState,
@@ -1424,7 +1472,7 @@ function PanelGroupWithForwardedRef({
1424
1472
  panelData: panelDataArray,
1425
1473
  panelIndex
1426
1474
  });
1427
- }, [dragState, layout, panelDataArray]);
1475
+ }, [dragState, layout]);
1428
1476
 
1429
1477
  // External APIs are safe to memoize via committed values ref
1430
1478
  const isPanelCollapsed = useCallback(panelData => {
@@ -1454,22 +1502,76 @@ function PanelGroupWithForwardedRef({
1454
1502
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1455
1503
  }, [groupId]);
1456
1504
  const registerPanel = useCallback(panelData => {
1457
- setPanelDataArray(prevPanelDataArray => {
1458
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1459
- return nextPanelDataArray.sort((panelA, panelB) => {
1460
- const orderA = panelA.order;
1461
- const orderB = panelB.order;
1462
- if (orderA == null && orderB == null) {
1463
- return 0;
1464
- } else if (orderA == null) {
1465
- return -1;
1466
- } else if (orderB == null) {
1467
- return 1;
1468
- } else {
1469
- return orderA - orderB;
1470
- }
1505
+ const {
1506
+ autoSaveId,
1507
+ id: groupId,
1508
+ layout: prevLayout,
1509
+ onLayout,
1510
+ panelDataArray,
1511
+ storage
1512
+ } = committedValuesRef.current;
1513
+ panelDataArray.push(panelData);
1514
+ panelDataArray.sort((panelA, panelB) => {
1515
+ const orderA = panelA.order;
1516
+ const orderB = panelB.order;
1517
+ if (orderA == null && orderB == null) {
1518
+ return 0;
1519
+ } else if (orderA == null) {
1520
+ return -1;
1521
+ } else if (orderB == null) {
1522
+ return 1;
1523
+ } else {
1524
+ return orderA - orderB;
1525
+ }
1526
+ });
1527
+
1528
+ // Wait until all panels have registered before we try to compute layout;
1529
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1530
+ const panelElements = getPanelElementsForGroup(groupId);
1531
+ if (panelElements.length !== panelDataArray.length) {
1532
+ return;
1533
+ }
1534
+
1535
+ // If this panel has been configured to persist sizing information,
1536
+ // default size should be restored from local storage if possible.
1537
+ let unsafeLayout = null;
1538
+ if (autoSaveId) {
1539
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1540
+ }
1541
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1542
+ if (groupSizePixels <= 0) {
1543
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1544
+ constraints
1545
+ }) => constraints))) {
1546
+ // Wait until the group has rendered a non-zero size before computing layout.
1547
+ return;
1548
+ }
1549
+ }
1550
+ if (unsafeLayout == null) {
1551
+ unsafeLayout = calculateUnsafeDefaultLayout({
1552
+ groupSizePixels,
1553
+ panelDataArray
1471
1554
  });
1555
+ }
1556
+
1557
+ // Validate even saved layouts in case something has changed since last render
1558
+ // e.g. for pixel groups, this could be the size of the window
1559
+ const nextLayout = validatePanelGroupLayout({
1560
+ groupSizePixels,
1561
+ layout: unsafeLayout,
1562
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1472
1563
  });
1564
+ if (!areEqual(prevLayout, nextLayout)) {
1565
+ setLayout(nextLayout);
1566
+ committedValuesRef.current.layout = nextLayout;
1567
+ if (onLayout) {
1568
+ onLayout(nextLayout.map(sizePercentage => ({
1569
+ sizePercentage,
1570
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1571
+ })));
1572
+ }
1573
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1574
+ }
1473
1575
  }, []);
1474
1576
  const registerResizeHandle = useCallback(dragHandleId => {
1475
1577
  return function resizeHandler(event) {
@@ -1539,6 +1641,7 @@ function PanelGroupWithForwardedRef({
1539
1641
  }
1540
1642
  if (layoutChanged) {
1541
1643
  setLayout(nextLayout);
1644
+ committedValuesRef.current.layout = nextLayout;
1542
1645
  if (onLayout) {
1543
1646
  onLayout(nextLayout.map(sizePercentage => ({
1544
1647
  sizePercentage,
@@ -1557,18 +1660,6 @@ function PanelGroupWithForwardedRef({
1557
1660
  onLayout,
1558
1661
  panelDataArray
1559
1662
  } = committedValuesRef.current;
1560
-
1561
- // See issues/211
1562
- if (panelDataArray.find(({
1563
- id
1564
- }) => id === panelData.id) == null) {
1565
- setImperativeApiQueue(prev => [...prev, {
1566
- panelData,
1567
- mixedSizes,
1568
- type: "resize"
1569
- }]);
1570
- return;
1571
- }
1572
1663
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1573
1664
  const {
1574
1665
  groupSizePixels,
@@ -1588,6 +1679,7 @@ function PanelGroupWithForwardedRef({
1588
1679
  });
1589
1680
  if (!compareLayouts(prevLayout, nextLayout)) {
1590
1681
  setLayout(nextLayout);
1682
+ committedValuesRef.current.layout = nextLayout;
1591
1683
  if (onLayout) {
1592
1684
  onLayout(nextLayout.map(sizePercentage => ({
1593
1685
  sizePercentage,
@@ -1615,16 +1707,84 @@ function PanelGroupWithForwardedRef({
1615
1707
  resetGlobalCursorStyle();
1616
1708
  setDragState(null);
1617
1709
  }, []);
1710
+ const unregisterPanelRef = useRef({
1711
+ pendingPanelIds: new Set(),
1712
+ timeout: null
1713
+ });
1618
1714
  const unregisterPanel = useCallback(panelData => {
1619
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1620
- setPanelDataArray(panelDataArray => {
1621
- const index = panelDataArray.indexOf(panelData);
1622
- if (index >= 0) {
1623
- panelDataArray = [...panelDataArray];
1624
- panelDataArray.splice(index, 1);
1715
+ const {
1716
+ id: groupId,
1717
+ layout: prevLayout,
1718
+ onLayout,
1719
+ panelDataArray
1720
+ } = committedValuesRef.current;
1721
+ const index = panelDataArray.indexOf(panelData);
1722
+ if (index >= 0) {
1723
+ panelDataArray.splice(index, 1);
1724
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1725
+ }
1726
+ if (unregisterPanelRef.current.timeout != null) {
1727
+ clearTimeout(unregisterPanelRef.current.timeout);
1728
+ }
1729
+
1730
+ // Batch panel unmounts so that we only calculate layout once;
1731
+ // This is more efficient and avoids misleading warnings in development mode.
1732
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1733
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1734
+ const {
1735
+ pendingPanelIds
1736
+ } = unregisterPanelRef.current;
1737
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1738
+
1739
+ // TRICKY
1740
+ // Strict effects mode
1741
+ let unmountDueToStrictMode = false;
1742
+ pendingPanelIds.forEach(panelId => {
1743
+ pendingPanelIds.delete(panelId);
1744
+ if (panelDataArray.find(({
1745
+ id
1746
+ }) => id === panelId) == null) {
1747
+ unmountDueToStrictMode = true;
1748
+
1749
+ // TRICKY
1750
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1751
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1752
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1753
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1754
+ }
1755
+ });
1756
+ if (!unmountDueToStrictMode) {
1757
+ return;
1625
1758
  }
1626
- return panelDataArray;
1627
- });
1759
+ if (panelDataArray.length === 0) {
1760
+ // The group is unmounting; skip layout calculation.
1761
+ return;
1762
+ }
1763
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1764
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1765
+ groupSizePixels,
1766
+ panelDataArray
1767
+ });
1768
+
1769
+ // Validate even saved layouts in case something has changed since last render
1770
+ // e.g. for pixel groups, this could be the size of the window
1771
+ const nextLayout = validatePanelGroupLayout({
1772
+ groupSizePixels,
1773
+ layout: unsafeLayout,
1774
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1775
+ });
1776
+ if (!areEqual(prevLayout, nextLayout)) {
1777
+ setLayout(nextLayout);
1778
+ committedValuesRef.current.layout = nextLayout;
1779
+ if (onLayout) {
1780
+ onLayout(nextLayout.map(sizePercentage => ({
1781
+ sizePercentage,
1782
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1783
+ })));
1784
+ }
1785
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1786
+ }
1787
+ }, 0);
1628
1788
  }, []);
1629
1789
  const context = useMemo(() => ({
1630
1790
  collapsePanel,