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.
@@ -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,
@@ -156,6 +157,7 @@ function PanelWithForwardedRef({
156
157
  // CSS selectors
157
158
  "data-panel": "",
158
159
  "data-panel-id": panelId,
160
+ "data-panel-group-id": groupId,
159
161
  // e2e test attributes
160
162
  "data-panel-collapsible": undefined,
161
163
  "data-panel-size": undefined
@@ -796,6 +798,44 @@ function calculateDeltaPercentage(event, groupId, dragHandleId, direction, initi
796
798
  }
797
799
  }
798
800
 
801
+ function calculateUnsafeDefaultLayout({
802
+ groupSizePixels,
803
+ panelDataArray
804
+ }) {
805
+ const layout = Array(panelDataArray.length);
806
+ const panelDataConstraints = panelDataArray.map(panelData => panelData.constraints);
807
+ let numPanelsWithSizes = 0;
808
+ let remainingSize = 100;
809
+
810
+ // Distribute default sizes first
811
+ for (let index = 0; index < panelDataArray.length; index++) {
812
+ const {
813
+ defaultSizePercentage
814
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
815
+ if (defaultSizePercentage != null) {
816
+ numPanelsWithSizes++;
817
+ layout[index] = defaultSizePercentage;
818
+ remainingSize -= defaultSizePercentage;
819
+ }
820
+ }
821
+
822
+ // Remaining size should be distributed evenly between panels without default sizes
823
+ for (let index = 0; index < panelDataArray.length; index++) {
824
+ const {
825
+ defaultSizePercentage
826
+ } = computePercentagePanelConstraints(panelDataConstraints, index, groupSizePixels);
827
+ if (defaultSizePercentage != null) {
828
+ continue;
829
+ }
830
+ const numRemainingPanels = panelDataArray.length - numPanelsWithSizes;
831
+ const size = remainingSize / numRemainingPanels;
832
+ numPanelsWithSizes++;
833
+ layout[index] = size;
834
+ remainingSize -= size;
835
+ }
836
+ return layout;
837
+ }
838
+
799
839
  function convertPercentageToPixels(percentage, groupSizePixels) {
800
840
  return percentage / 100 * groupSizePixels;
801
841
  }
@@ -946,6 +986,10 @@ function debounce(callback, durationMs = 10) {
946
986
  return callable;
947
987
  }
948
988
 
989
+ function getPanelElementsForGroup(groupId) {
990
+ return Array.from(document.querySelectorAll(`[data-panel][data-panel-group-id="${groupId}"]`));
991
+ }
992
+
949
993
  // PanelGroup might be rendering in a server-side environment where localStorage is not available
950
994
  // or on a browser with cookies/storage disabled.
951
995
  // In either case, this function avoids accessing localStorage until needed,
@@ -1001,6 +1045,15 @@ function loadSerializedPanelGroupState(autoSaveId, storage) {
1001
1045
  } catch (error) {}
1002
1046
  return null;
1003
1047
  }
1048
+ function loadPanelLayout(autoSaveId, panels, storage) {
1049
+ const state = loadSerializedPanelGroupState(autoSaveId, storage);
1050
+ if (state) {
1051
+ var _state$key;
1052
+ const key = getSerializationKey(panels);
1053
+ return (_state$key = state[key]) !== null && _state$key !== void 0 ? _state$key : null;
1054
+ }
1055
+ return null;
1056
+ }
1004
1057
  function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1005
1058
  const key = getSerializationKey(panels);
1006
1059
  const state = loadSerializedPanelGroupState(autoSaveId, storage) || {};
@@ -1012,6 +1065,12 @@ function savePanelGroupLayout(autoSaveId, panels, sizes, storage) {
1012
1065
  }
1013
1066
  }
1014
1067
 
1068
+ function shouldMonitorPixelBasedConstraints(constraints) {
1069
+ return constraints.some(constraints => {
1070
+ return constraints.collapsedSizePixels !== undefined || constraints.maxSizePixels !== undefined || constraints.minSizePixels !== undefined;
1071
+ });
1072
+ }
1073
+
1015
1074
  // All units must be in percentages; pixel values should be pre-converted
1016
1075
  function validatePanelGroupLayout({
1017
1076
  groupSizePixels,
@@ -1080,7 +1139,7 @@ const defaultStorage = {
1080
1139
  };
1081
1140
  const debounceMap = {};
1082
1141
  function PanelGroupWithForwardedRef({
1083
- autoSaveId,
1142
+ autoSaveId = null,
1084
1143
  children,
1085
1144
  className: classNameFromProps = "",
1086
1145
  dataAttributes,
@@ -1097,12 +1156,11 @@ function PanelGroupWithForwardedRef({
1097
1156
  const groupId = useUniqueId(idFromProps);
1098
1157
  const [dragState, setDragState] = useState(null);
1099
1158
  const [layout, setLayout] = useState([]);
1100
- const [panelDataArray, setPanelDataArray] = useState([]);
1101
1159
  const panelIdToLastNotifiedMixedSizesMapRef = useRef({});
1102
1160
  const panelSizeBeforeCollapseRef = useRef(new Map());
1103
1161
  const prevDeltaRef = useRef(0);
1104
- const [imperativeApiQueue, setImperativeApiQueue] = useState([]);
1105
1162
  const committedValuesRef = useRef({
1163
+ autoSaveId,
1106
1164
  direction,
1107
1165
  dragState,
1108
1166
  id: groupId,
@@ -1110,7 +1168,8 @@ function PanelGroupWithForwardedRef({
1110
1168
  keyboardResizeByPixels,
1111
1169
  layout,
1112
1170
  onLayout,
1113
- panelDataArray
1171
+ panelDataArray: [],
1172
+ storage
1114
1173
  });
1115
1174
  useRef({
1116
1175
  didLogIdAndOrderWarning: false,
@@ -1148,6 +1207,7 @@ function PanelGroupWithForwardedRef({
1148
1207
  });
1149
1208
  if (!areEqual(prevLayout, safeLayout)) {
1150
1209
  setLayout(safeLayout);
1210
+ committedValuesRef.current.layout = safeLayout;
1151
1211
  if (onLayout) {
1152
1212
  onLayout(safeLayout.map(sizePercentage => ({
1153
1213
  sizePercentage,
@@ -1158,14 +1218,19 @@ function PanelGroupWithForwardedRef({
1158
1218
  }
1159
1219
  }
1160
1220
  }), []);
1221
+
1161
1222
  useWindowSplitterPanelGroupBehavior({
1162
1223
  committedValuesRef,
1163
1224
  groupId,
1164
1225
  layout,
1165
- panelDataArray,
1226
+ panelDataArray: committedValuesRef.current.panelDataArray,
1166
1227
  setLayout
1167
1228
  });
1168
1229
  useEffect(() => {
1230
+ const {
1231
+ panelDataArray
1232
+ } = committedValuesRef.current;
1233
+
1169
1234
  // If this panel has been configured to persist sizing information, save sizes to local storage.
1170
1235
  if (autoSaveId) {
1171
1236
  if (layout.length === 0 || layout.length !== panelDataArray.length) {
@@ -1178,7 +1243,7 @@ function PanelGroupWithForwardedRef({
1178
1243
  }
1179
1244
  debounceMap[autoSaveId](autoSaveId, panelDataArray, layout, storage);
1180
1245
  }
1181
- }, [autoSaveId, layout, panelDataArray, storage]);
1246
+ }, [autoSaveId, layout, storage]);
1182
1247
 
1183
1248
  // DEV warnings
1184
1249
  useEffect(() => {
@@ -1191,17 +1256,6 @@ function PanelGroupWithForwardedRef({
1191
1256
  onLayout,
1192
1257
  panelDataArray
1193
1258
  } = committedValuesRef.current;
1194
-
1195
- // See issues/211
1196
- if (panelDataArray.find(({
1197
- id
1198
- }) => id === panelData.id) == null) {
1199
- setImperativeApiQueue(prev => [...prev, {
1200
- panelData,
1201
- type: "collapse"
1202
- }]);
1203
- return;
1204
- }
1205
1259
  if (panelData.constraints.collapsible) {
1206
1260
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1207
1261
  const {
@@ -1226,6 +1280,7 @@ function PanelGroupWithForwardedRef({
1226
1280
  });
1227
1281
  if (!compareLayouts(prevLayout, nextLayout)) {
1228
1282
  setLayout(nextLayout);
1283
+ committedValuesRef.current.layout = nextLayout;
1229
1284
  if (onLayout) {
1230
1285
  onLayout(nextLayout.map(sizePercentage => ({
1231
1286
  sizePercentage,
@@ -1245,17 +1300,6 @@ function PanelGroupWithForwardedRef({
1245
1300
  onLayout,
1246
1301
  panelDataArray
1247
1302
  } = committedValuesRef.current;
1248
-
1249
- // See issues/211
1250
- if (panelDataArray.find(({
1251
- id
1252
- }) => id === panelData.id) == null) {
1253
- setImperativeApiQueue(prev => [...prev, {
1254
- panelData,
1255
- type: "expand"
1256
- }]);
1257
- return;
1258
- }
1259
1303
  if (panelData.constraints.collapsible) {
1260
1304
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1261
1305
  const {
@@ -1281,6 +1325,7 @@ function PanelGroupWithForwardedRef({
1281
1325
  });
1282
1326
  if (!compareLayouts(prevLayout, nextLayout)) {
1283
1327
  setLayout(nextLayout);
1328
+ committedValuesRef.current.layout = nextLayout;
1284
1329
  if (onLayout) {
1285
1330
  onLayout(nextLayout.map(sizePercentage => ({
1286
1331
  sizePercentage,
@@ -1311,6 +1356,9 @@ function PanelGroupWithForwardedRef({
1311
1356
 
1312
1357
  // This API should never read from committedValuesRef
1313
1358
  const getPanelStyle = useCallback(panelData => {
1359
+ const {
1360
+ panelDataArray
1361
+ } = committedValuesRef.current;
1314
1362
  const panelIndex = panelDataArray.indexOf(panelData);
1315
1363
  return computePanelFlexBoxStyle({
1316
1364
  dragState,
@@ -1318,7 +1366,7 @@ function PanelGroupWithForwardedRef({
1318
1366
  panelData: panelDataArray,
1319
1367
  panelIndex
1320
1368
  });
1321
- }, [dragState, layout, panelDataArray]);
1369
+ }, [dragState, layout]);
1322
1370
 
1323
1371
  // External APIs are safe to memoize via committed values ref
1324
1372
  const isPanelCollapsed = useCallback(panelData => {
@@ -1348,22 +1396,76 @@ function PanelGroupWithForwardedRef({
1348
1396
  return !collapsible || panelSizePercentage > collapsedSizePercentage;
1349
1397
  }, [groupId]);
1350
1398
  const registerPanel = useCallback(panelData => {
1351
- setPanelDataArray(prevPanelDataArray => {
1352
- const nextPanelDataArray = [...prevPanelDataArray, panelData];
1353
- return nextPanelDataArray.sort((panelA, panelB) => {
1354
- const orderA = panelA.order;
1355
- const orderB = panelB.order;
1356
- if (orderA == null && orderB == null) {
1357
- return 0;
1358
- } else if (orderA == null) {
1359
- return -1;
1360
- } else if (orderB == null) {
1361
- return 1;
1362
- } else {
1363
- return orderA - orderB;
1364
- }
1399
+ const {
1400
+ autoSaveId,
1401
+ id: groupId,
1402
+ layout: prevLayout,
1403
+ onLayout,
1404
+ panelDataArray,
1405
+ storage
1406
+ } = committedValuesRef.current;
1407
+ panelDataArray.push(panelData);
1408
+ panelDataArray.sort((panelA, panelB) => {
1409
+ const orderA = panelA.order;
1410
+ const orderB = panelB.order;
1411
+ if (orderA == null && orderB == null) {
1412
+ return 0;
1413
+ } else if (orderA == null) {
1414
+ return -1;
1415
+ } else if (orderB == null) {
1416
+ return 1;
1417
+ } else {
1418
+ return orderA - orderB;
1419
+ }
1420
+ });
1421
+
1422
+ // Wait until all panels have registered before we try to compute layout;
1423
+ // doing it earlier is both wasteful and may trigger misleading warnings in development mode.
1424
+ const panelElements = getPanelElementsForGroup(groupId);
1425
+ if (panelElements.length !== panelDataArray.length) {
1426
+ return;
1427
+ }
1428
+
1429
+ // If this panel has been configured to persist sizing information,
1430
+ // default size should be restored from local storage if possible.
1431
+ let unsafeLayout = null;
1432
+ if (autoSaveId) {
1433
+ unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
1434
+ }
1435
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1436
+ if (groupSizePixels <= 0) {
1437
+ if (shouldMonitorPixelBasedConstraints(panelDataArray.map(({
1438
+ constraints
1439
+ }) => constraints))) {
1440
+ // Wait until the group has rendered a non-zero size before computing layout.
1441
+ return;
1442
+ }
1443
+ }
1444
+ if (unsafeLayout == null) {
1445
+ unsafeLayout = calculateUnsafeDefaultLayout({
1446
+ groupSizePixels,
1447
+ panelDataArray
1365
1448
  });
1449
+ }
1450
+
1451
+ // Validate even saved layouts in case something has changed since last render
1452
+ // e.g. for pixel groups, this could be the size of the window
1453
+ const nextLayout = validatePanelGroupLayout({
1454
+ groupSizePixels,
1455
+ layout: unsafeLayout,
1456
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1366
1457
  });
1458
+ if (!areEqual(prevLayout, nextLayout)) {
1459
+ setLayout(nextLayout);
1460
+ committedValuesRef.current.layout = nextLayout;
1461
+ if (onLayout) {
1462
+ onLayout(nextLayout.map(sizePercentage => ({
1463
+ sizePercentage,
1464
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1465
+ })));
1466
+ }
1467
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1468
+ }
1367
1469
  }, []);
1368
1470
  const registerResizeHandle = useCallback(dragHandleId => {
1369
1471
  return function resizeHandler(event) {
@@ -1433,6 +1535,7 @@ function PanelGroupWithForwardedRef({
1433
1535
  }
1434
1536
  if (layoutChanged) {
1435
1537
  setLayout(nextLayout);
1538
+ committedValuesRef.current.layout = nextLayout;
1436
1539
  if (onLayout) {
1437
1540
  onLayout(nextLayout.map(sizePercentage => ({
1438
1541
  sizePercentage,
@@ -1451,18 +1554,6 @@ function PanelGroupWithForwardedRef({
1451
1554
  onLayout,
1452
1555
  panelDataArray
1453
1556
  } = committedValuesRef.current;
1454
-
1455
- // See issues/211
1456
- if (panelDataArray.find(({
1457
- id
1458
- }) => id === panelData.id) == null) {
1459
- setImperativeApiQueue(prev => [...prev, {
1460
- panelData,
1461
- mixedSizes,
1462
- type: "resize"
1463
- }]);
1464
- return;
1465
- }
1466
1557
  const panelConstraintsArray = panelDataArray.map(panelData => panelData.constraints);
1467
1558
  const {
1468
1559
  groupSizePixels,
@@ -1482,6 +1573,7 @@ function PanelGroupWithForwardedRef({
1482
1573
  });
1483
1574
  if (!compareLayouts(prevLayout, nextLayout)) {
1484
1575
  setLayout(nextLayout);
1576
+ committedValuesRef.current.layout = nextLayout;
1485
1577
  if (onLayout) {
1486
1578
  onLayout(nextLayout.map(sizePercentage => ({
1487
1579
  sizePercentage,
@@ -1509,16 +1601,84 @@ function PanelGroupWithForwardedRef({
1509
1601
  resetGlobalCursorStyle();
1510
1602
  setDragState(null);
1511
1603
  }, []);
1604
+ const unregisterPanelRef = useRef({
1605
+ pendingPanelIds: new Set(),
1606
+ timeout: null
1607
+ });
1512
1608
  const unregisterPanel = useCallback(panelData => {
1513
- delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1514
- setPanelDataArray(panelDataArray => {
1515
- const index = panelDataArray.indexOf(panelData);
1516
- if (index >= 0) {
1517
- panelDataArray = [...panelDataArray];
1518
- panelDataArray.splice(index, 1);
1609
+ const {
1610
+ id: groupId,
1611
+ layout: prevLayout,
1612
+ onLayout,
1613
+ panelDataArray
1614
+ } = committedValuesRef.current;
1615
+ const index = panelDataArray.indexOf(panelData);
1616
+ if (index >= 0) {
1617
+ panelDataArray.splice(index, 1);
1618
+ unregisterPanelRef.current.pendingPanelIds.add(panelData.id);
1619
+ }
1620
+ if (unregisterPanelRef.current.timeout != null) {
1621
+ clearTimeout(unregisterPanelRef.current.timeout);
1622
+ }
1623
+
1624
+ // Batch panel unmounts so that we only calculate layout once;
1625
+ // This is more efficient and avoids misleading warnings in development mode.
1626
+ // We can't check the DOM to detect this because Panel elements have not yet been removed.
1627
+ unregisterPanelRef.current.timeout = setTimeout(() => {
1628
+ const {
1629
+ pendingPanelIds
1630
+ } = unregisterPanelRef.current;
1631
+ panelIdToLastNotifiedMixedSizesMapRef.current;
1632
+
1633
+ // TRICKY
1634
+ // Strict effects mode
1635
+ let unmountDueToStrictMode = false;
1636
+ pendingPanelIds.forEach(panelId => {
1637
+ pendingPanelIds.delete(panelId);
1638
+ if (panelDataArray.find(({
1639
+ id
1640
+ }) => id === panelId) == null) {
1641
+ unmountDueToStrictMode = true;
1642
+
1643
+ // TRICKY
1644
+ // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
1645
+ // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
1646
+ // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
1647
+ delete panelIdToLastNotifiedMixedSizesMapRef.current[panelData.id];
1648
+ }
1649
+ });
1650
+ if (!unmountDueToStrictMode) {
1651
+ return;
1519
1652
  }
1520
- return panelDataArray;
1521
- });
1653
+ if (panelDataArray.length === 0) {
1654
+ // The group is unmounting; skip layout calculation.
1655
+ return;
1656
+ }
1657
+ const groupSizePixels = calculateAvailablePanelSizeInPixels(groupId);
1658
+ let unsafeLayout = calculateUnsafeDefaultLayout({
1659
+ groupSizePixels,
1660
+ panelDataArray
1661
+ });
1662
+
1663
+ // Validate even saved layouts in case something has changed since last render
1664
+ // e.g. for pixel groups, this could be the size of the window
1665
+ const nextLayout = validatePanelGroupLayout({
1666
+ groupSizePixels,
1667
+ layout: unsafeLayout,
1668
+ panelConstraints: panelDataArray.map(panelData => panelData.constraints)
1669
+ });
1670
+ if (!areEqual(prevLayout, nextLayout)) {
1671
+ setLayout(nextLayout);
1672
+ committedValuesRef.current.layout = nextLayout;
1673
+ if (onLayout) {
1674
+ onLayout(nextLayout.map(sizePercentage => ({
1675
+ sizePercentage,
1676
+ sizePixels: convertPercentageToPixels(sizePercentage, groupSizePixels)
1677
+ })));
1678
+ }
1679
+ callPanelCallbacks(groupId, panelDataArray, nextLayout, panelIdToLastNotifiedMixedSizesMapRef.current);
1680
+ }
1681
+ }, 0);
1522
1682
  }, []);
1523
1683
  const context = useMemo(() => ({
1524
1684
  collapsePanel,