@aptre/flex-layout 0.3.5 → 0.4.1

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.
Files changed (69) hide show
  1. package/README.md +13 -0
  2. package/dist/Layout.e2e.test.d.ts +1 -0
  3. package/dist/Layout.test.d.ts +1 -0
  4. package/dist/OptimizedLayout.e2e.test.d.ts +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.mjs +220 -52
  7. package/dist/test/setup.d.ts +8 -0
  8. package/dist/test/unit-setup.d.ts +1 -0
  9. package/dist/view/Layout.d.ts +2 -0
  10. package/dist/view/OptimizedLayout.d.ts +29 -0
  11. package/package.json +119 -102
  12. package/tsconfig.json +18 -18
  13. package/typedoc/assets/hierarchy.js +1 -1
  14. package/typedoc/assets/main.js +5 -5
  15. package/typedoc/assets/navigation.js +1 -1
  16. package/typedoc/assets/search.js +1 -1
  17. package/typedoc/assets/style.css +251 -228
  18. package/typedoc/classes/Action.html +4 -4
  19. package/typedoc/classes/Actions.html +74 -74
  20. package/typedoc/classes/BorderNode.html +25 -25
  21. package/typedoc/classes/BorderSet.html +2 -2
  22. package/typedoc/classes/DockLocation.html +10 -10
  23. package/typedoc/classes/DropInfo.html +7 -7
  24. package/typedoc/classes/Layout.html +96 -96
  25. package/typedoc/classes/LayoutWindow.html +12 -12
  26. package/typedoc/classes/Model.html +42 -42
  27. package/typedoc/classes/Node.html +12 -12
  28. package/typedoc/classes/Orientation.html +6 -6
  29. package/typedoc/classes/Rect.html +26 -26
  30. package/typedoc/classes/RowNode.html +16 -16
  31. package/typedoc/classes/TabNode.html +39 -39
  32. package/typedoc/classes/TabSetNode.html +39 -39
  33. package/typedoc/enums/CLASSES.html +94 -94
  34. package/typedoc/enums/I18nLabel.html +11 -11
  35. package/typedoc/enums/ICloseType.html +4 -4
  36. package/typedoc/functions/OptimizedLayout.html +18 -0
  37. package/typedoc/functions/findJsonNodeById.html +4 -4
  38. package/typedoc/functions/walkJsonModel.html +3 -3
  39. package/typedoc/hierarchy.html +1 -1
  40. package/typedoc/index.html +1 -1
  41. package/typedoc/interfaces/IBorderAttributes.html +25 -25
  42. package/typedoc/interfaces/IDraggable.html +1 -1
  43. package/typedoc/interfaces/IDropTarget.html +1 -1
  44. package/typedoc/interfaces/IGlobalAttributes.html +99 -99
  45. package/typedoc/interfaces/IIcons.html +9 -9
  46. package/typedoc/interfaces/IJsonBorderNode.html +27 -27
  47. package/typedoc/interfaces/IJsonModel.html +5 -5
  48. package/typedoc/interfaces/IJsonPopout.html +3 -3
  49. package/typedoc/interfaces/IJsonRect.html +5 -5
  50. package/typedoc/interfaces/IJsonRowNode.html +8 -8
  51. package/typedoc/interfaces/IJsonTabNode.html +53 -53
  52. package/typedoc/interfaces/IJsonTabSetNode.html +52 -52
  53. package/typedoc/interfaces/ILayoutProps.html +41 -39
  54. package/typedoc/interfaces/IOptimizedLayoutProps.html +42 -0
  55. package/typedoc/interfaces/IRowAttributes.html +7 -7
  56. package/typedoc/interfaces/ITabAttributes.html +53 -53
  57. package/typedoc/interfaces/ITabRenderValues.html +7 -7
  58. package/typedoc/interfaces/ITabSetAttributes.html +47 -47
  59. package/typedoc/interfaces/ITabSetRenderValues.html +7 -7
  60. package/typedoc/interfaces/VisitorResult.html +5 -5
  61. package/typedoc/types/DragRectRenderCallback.html +1 -1
  62. package/typedoc/types/IBorderLocation.html +1 -1
  63. package/typedoc/types/ITabLocation.html +1 -1
  64. package/typedoc/types/JsonNode.html +2 -2
  65. package/typedoc/types/ModelVisitor.html +5 -5
  66. package/typedoc/types/NodeMouseEvent.html +1 -1
  67. package/typedoc/types/ShowOverflowMenuCallback.html +1 -1
  68. package/typedoc/types/TabSetPlaceHolderCallback.html +1 -1
  69. package/typedoc/variables/FlexLayoutVersion.html +1 -1
package/README.md CHANGED
@@ -38,6 +38,19 @@ Features:
38
38
  * component state is preserved when tabs are moved
39
39
  * typescript type declarations
40
40
 
41
+ ## Demo
42
+
43
+ To demo and test this library, clone this repo, then:
44
+
45
+ ```
46
+ npm i -g yarn
47
+ yarn
48
+ yarn test:browser
49
+ ```
50
+
51
+ Your browser will open to show + all the tests with vitest Browser Mode.
52
+
53
+
41
54
  ## Installation
42
55
 
43
56
  FlexLayout is in the npm repository. install using:
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./view/Layout";
2
+ export * from "./view/OptimizedLayout";
2
3
  export * from "./model/Action";
3
4
  export * from "./model/Actions";
4
5
  export * from "./model/BorderNode";
package/dist/index.mjs CHANGED
@@ -1316,6 +1316,14 @@ var TabNode = class _TabNode extends Node {
1316
1316
  }
1317
1317
  };
1318
1318
 
1319
+ // src/model/ICloseType.ts
1320
+ var ICloseType = /* @__PURE__ */ ((ICloseType2) => {
1321
+ ICloseType2[ICloseType2["Visible"] = 1] = "Visible";
1322
+ ICloseType2[ICloseType2["Always"] = 2] = "Always";
1323
+ ICloseType2[ICloseType2["Selected"] = 3] = "Selected";
1324
+ return ICloseType2;
1325
+ })(ICloseType || {});
1326
+
1319
1327
  // src/view/Utils.tsx
1320
1328
  function isDesktop() {
1321
1329
  const desktop = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
@@ -1422,6 +1430,18 @@ function isSafari() {
1422
1430
  const userAgent = navigator.userAgent;
1423
1431
  return userAgent.includes("Safari") && !userAgent.includes("Chrome") && !userAgent.includes("Chromium");
1424
1432
  }
1433
+ function isTabClosable(node, selected) {
1434
+ const closeType = node.getCloseType();
1435
+ if (selected || closeType === 2 /* Always */) {
1436
+ return true;
1437
+ }
1438
+ if (closeType === 1 /* Visible */) {
1439
+ if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
1440
+ return true;
1441
+ }
1442
+ }
1443
+ return false;
1444
+ }
1425
1445
 
1426
1446
  // src/model/Utils.ts
1427
1447
  function adjustSelectedIndex(parent, removedIndex) {
@@ -1651,7 +1671,15 @@ var TabSetNode = class _TabSetNode extends Node {
1651
1671
  }
1652
1672
  /** @internal */
1653
1673
  setContentRect(rect) {
1674
+ const changed = !this.contentRect.equals(rect);
1654
1675
  this.contentRect = rect;
1676
+ if (changed && rect.width > 0 && rect.height > 0) {
1677
+ for (const child of this.children) {
1678
+ if (child instanceof TabNode) {
1679
+ child.setRect(rect);
1680
+ }
1681
+ }
1682
+ }
1655
1683
  }
1656
1684
  /** @internal */
1657
1685
  getContentRect() {
@@ -3157,7 +3185,15 @@ var BorderNode = class _BorderNode extends Node {
3157
3185
  }
3158
3186
  /** @internal */
3159
3187
  setContentRect(r) {
3188
+ const changed = !this.contentRect.equals(r);
3160
3189
  this.contentRect = r;
3190
+ if (changed && r.width > 0 && r.height > 0) {
3191
+ for (const child of this.children) {
3192
+ if (child instanceof TabNode) {
3193
+ child.setRect(r);
3194
+ }
3195
+ }
3196
+ }
3161
3197
  }
3162
3198
  /** @internal */
3163
3199
  isEnableDrop() {
@@ -3537,6 +3573,7 @@ var Splitter = (props) => {
3537
3573
  function BorderTab(props) {
3538
3574
  const { layout, border, show } = props;
3539
3575
  const selfRef = React3.useRef(null);
3576
+ const isFirstRender = React3.useRef(true);
3540
3577
  React3.useLayoutEffect(() => {
3541
3578
  const outerRect = layout.getBoundingClientRect(selfRef.current);
3542
3579
  const contentRect = Rect.getContentRect(selfRef.current).relativeTo(layout.getDomRect());
@@ -3544,9 +3581,12 @@ function BorderTab(props) {
3544
3581
  border.setOuterRect(outerRect);
3545
3582
  if (!border.getContentRect().equals(contentRect)) {
3546
3583
  border.setContentRect(contentRect);
3547
- layout.redrawInternal("border content rect");
3584
+ if (!isFirstRender.current) {
3585
+ layout.redrawInternal("border content rect");
3586
+ }
3548
3587
  }
3549
3588
  }
3589
+ isFirstRender.current = false;
3550
3590
  });
3551
3591
  let horizontal = true;
3552
3592
  const style2 = {};
@@ -3574,16 +3614,6 @@ import * as React8 from "react";
3574
3614
 
3575
3615
  // src/view/BorderButton.tsx
3576
3616
  import * as React4 from "react";
3577
-
3578
- // src/model/ICloseType.ts
3579
- var ICloseType = /* @__PURE__ */ ((ICloseType2) => {
3580
- ICloseType2[ICloseType2["Visible"] = 1] = "Visible";
3581
- ICloseType2[ICloseType2["Always"] = 2] = "Always";
3582
- ICloseType2[ICloseType2["Selected"] = 3] = "Selected";
3583
- return ICloseType2;
3584
- })(ICloseType || {});
3585
-
3586
- // src/view/BorderButton.tsx
3587
3617
  var BorderButton = (props) => {
3588
3618
  const { layout, node, selected, border, icons, path } = props;
3589
3619
  const selfRef = React4.useRef(null);
@@ -3617,20 +3647,8 @@ var BorderButton = (props) => {
3617
3647
  layout.setEditingTab(void 0);
3618
3648
  }
3619
3649
  };
3620
- const isClosable = () => {
3621
- const closeType = node.getCloseType();
3622
- if (selected || closeType === 2 /* Always */) {
3623
- return true;
3624
- }
3625
- if (closeType === 1 /* Visible */) {
3626
- if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
3627
- return true;
3628
- }
3629
- }
3630
- return false;
3631
- };
3632
3650
  const onClose = (event) => {
3633
- if (isClosable()) {
3651
+ if (isTabClosable(node, selected)) {
3634
3652
  layout.doAction(Actions.deleteTab(node.getId()));
3635
3653
  } else {
3636
3654
  onClick();
@@ -3834,12 +3852,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3834
3852
  React7.useLayoutEffect(() => {
3835
3853
  userControlledLeft.current = false;
3836
3854
  }, [node.getSelectedNode(), node.getRect().width, node.getRect().height]);
3855
+ const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3837
3856
  React7.useLayoutEffect(() => {
3838
- const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3839
3857
  if (nodeRect.width > 0 && nodeRect.height > 0) {
3840
3858
  updateVisibleTabs();
3841
3859
  }
3842
- });
3860
+ }, [nodeRect.width, nodeRect.height]);
3843
3861
  const instance = toolbarRef.current;
3844
3862
  React7.useEffect(() => {
3845
3863
  if (!instance) {
@@ -3879,15 +3897,15 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3879
3897
  if (firstRender.current === true) {
3880
3898
  tabsTruncated.current = false;
3881
3899
  }
3882
- const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3900
+ const nodeRect2 = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3883
3901
  const lastChild = node.getChildren()[node.getChildren().length - 1];
3884
3902
  const stickyButtonsSize = stickyButtonsRef.current === null ? 0 : getSize(stickyButtonsRef.current.getBoundingClientRect());
3885
- if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 || nodeRect.width !== lastRect.current.width || // incase rect changed between first render and second
3886
- nodeRect.height !== lastRect.current.height) {
3903
+ if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 || nodeRect2.width !== lastRect.current.width || // incase rect changed between first render and second
3904
+ nodeRect2.height !== lastRect.current.height) {
3887
3905
  lastHiddenCount.current = hiddenTabs.length;
3888
- lastRect.current = nodeRect;
3906
+ lastRect.current = nodeRect2;
3889
3907
  const enabled = node instanceof TabSetNode ? node.isEnableTabStrip() === true : true;
3890
- let endPos = getFar(nodeRect) - stickyButtonsSize;
3908
+ let endPos = getFar(nodeRect2) - stickyButtonsSize;
3891
3909
  if (toolbarRef.current !== null) {
3892
3910
  endPos -= getSize(toolbarRef.current.getBoundingClientRect());
3893
3911
  }
@@ -3901,12 +3919,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3901
3919
  const selectedRect = selectedTab.getTabRect();
3902
3920
  const selectedStart = getNear(selectedRect) - tabMargin;
3903
3921
  const selectedEnd = getFar(selectedRect) + tabMargin;
3904
- if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect)) {
3905
- shiftPos = getNear(nodeRect) - selectedStart;
3922
+ if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect2)) {
3923
+ shiftPos = getNear(nodeRect2) - selectedStart;
3906
3924
  } else {
3907
- if (selectedEnd > endPos || selectedStart < getNear(nodeRect)) {
3908
- if (selectedStart < getNear(nodeRect)) {
3909
- shiftPos = getNear(nodeRect) - selectedStart;
3925
+ if (selectedEnd > endPos || selectedStart < getNear(nodeRect2)) {
3926
+ if (selectedStart < getNear(nodeRect2)) {
3927
+ shiftPos = getNear(nodeRect2) - selectedStart;
3910
3928
  }
3911
3929
  if (selectedEnd + shiftPos > endPos) {
3912
3930
  shiftPos = endPos - selectedEnd;
@@ -3920,7 +3938,7 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3920
3938
  const hidden = [];
3921
3939
  for (let i = 0; i < node.getChildren().length; i++) {
3922
3940
  const child = node.getChildren()[i];
3923
- if (getNear(child.getTabRect()) + diff < getNear(nodeRect) || getFar(child.getTabRect()) + diff > endPos) {
3941
+ if (getNear(child.getTabRect()) + diff < getNear(nodeRect2) || getFar(child.getTabRect()) + diff > endPos) {
3924
3942
  hidden.push({ node: child, index: i });
3925
3943
  }
3926
3944
  }
@@ -4371,21 +4389,9 @@ var TabButton = (props) => {
4371
4389
  layout.setEditingTab(void 0);
4372
4390
  }
4373
4391
  };
4374
- const isClosable = () => {
4375
- const closeType = node.getCloseType();
4376
- if (selected || closeType === 2 /* Always */) {
4377
- return true;
4378
- }
4379
- if (closeType === 1 /* Visible */) {
4380
- if (window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
4381
- return true;
4382
- }
4383
- }
4384
- return false;
4385
- };
4386
4392
  const onClose = (event) => {
4387
4393
  event.preventDefault();
4388
- if (isClosable()) {
4394
+ if (isTabClosable(node, selected)) {
4389
4395
  layout.doAction(Actions.deleteTab(node.getId()));
4390
4396
  } else {
4391
4397
  onClick();
@@ -4502,6 +4508,7 @@ var TabSet = (props) => {
4502
4508
  const overflowbuttonRef = React15.useRef(null);
4503
4509
  const stickyButtonsRef = React15.useRef(null);
4504
4510
  const icons = layout.getIcons();
4511
+ const isFirstRender = React15.useRef(true);
4505
4512
  React15.useEffect(() => {
4506
4513
  node.setRect(layout.getBoundingClientRect(selfRef.current));
4507
4514
  if (tabStripRef.current) {
@@ -4510,8 +4517,11 @@ var TabSet = (props) => {
4510
4517
  const newContentRect = Rect.getContentRect(contentRef.current).relativeTo(layout.getDomRect());
4511
4518
  if (!node.getContentRect().equals(newContentRect)) {
4512
4519
  node.setContentRect(newContentRect);
4513
- layout.redrawInternal("tabset content rect " + newContentRect);
4520
+ if (!isFirstRender.current) {
4521
+ layout.redrawInternal("tabset content rect " + newContentRect);
4522
+ }
4514
4523
  }
4524
+ isFirstRender.current = false;
4515
4525
  });
4516
4526
  const { selfRef, position, userControlledLeft, hiddenTabs, onMouseWheel, tabsTruncated } = useTabOverflow(node, Orientation.HORZ, buttonBarRef, stickyButtonsRef);
4517
4527
  const onOverflowClick = (event) => {
@@ -4924,7 +4934,7 @@ var Tab = (props) => {
4924
4934
  firstSelect.current = false;
4925
4935
  }
4926
4936
  }
4927
- });
4937
+ }, [selected]);
4928
4938
  const onPointerDown = () => {
4929
4939
  const parent = node.getParent();
4930
4940
  if (parent instanceof TabSetNode) {
@@ -5764,6 +5774,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
5764
5774
  this.showOverlay(false);
5765
5775
  this.dragEnterCount = 0;
5766
5776
  this.dragging = false;
5777
+ this.props.onDragStateChange?.(false);
5767
5778
  if (this.outlineDiv) {
5768
5779
  this.selfRef.current.removeChild(this.outlineDiv);
5769
5780
  this.outlineDiv = void 0;
@@ -5795,6 +5806,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
5795
5806
  rootdiv.appendChild(this.outlineDiv);
5796
5807
  this.dragging = true;
5797
5808
  this.showOverlay(true);
5809
+ this.props.onDragStateChange?.(true);
5798
5810
  if (!this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === void 0) {
5799
5811
  this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
5800
5812
  }
@@ -5880,6 +5892,161 @@ var DragState = class {
5880
5892
  }
5881
5893
  };
5882
5894
 
5895
+ // src/view/OptimizedLayout.tsx
5896
+ import * as React20 from "react";
5897
+ import { useCallback, useEffect as useEffect6, useMemo, useRef as useRef12, useState as useState4 } from "react";
5898
+ function TabRef({ node, onRectChange, onVisibilityChange }) {
5899
+ useEffect6(() => {
5900
+ const handleResize = (params) => {
5901
+ onRectChange(node.getId(), params.rect);
5902
+ };
5903
+ const handleVisibility = (params) => {
5904
+ onVisibilityChange(node.getId(), params.visible);
5905
+ };
5906
+ node.setEventListener("resize", handleResize);
5907
+ node.setEventListener("visibility", handleVisibility);
5908
+ const parent = node.getParent();
5909
+ if (parent) {
5910
+ const contentRect = parent.getContentRect();
5911
+ if (contentRect && contentRect.width > 0 && contentRect.height > 0) {
5912
+ onRectChange(node.getId(), contentRect);
5913
+ }
5914
+ }
5915
+ onVisibilityChange(node.getId(), node.isSelected());
5916
+ return () => {
5917
+ node.removeEventListener("resize");
5918
+ node.removeEventListener("visibility");
5919
+ };
5920
+ }, [node, onRectChange, onVisibilityChange]);
5921
+ return null;
5922
+ }
5923
+ function TabContainer({
5924
+ tabs,
5925
+ renderTab,
5926
+ isDragging,
5927
+ classNameMapper
5928
+ }) {
5929
+ const getClassName = useCallback(
5930
+ (defaultClassName) => {
5931
+ return classNameMapper ? classNameMapper(defaultClassName) : defaultClassName;
5932
+ },
5933
+ [classNameMapper]
5934
+ );
5935
+ return /* @__PURE__ */ React20.createElement(
5936
+ "div",
5937
+ {
5938
+ style: {
5939
+ position: "absolute",
5940
+ inset: 0,
5941
+ // CRITICAL: The container itself always has pointer-events: none
5942
+ // so it doesn't block clicks on elements beneath it (like FlexLayout's tab bar).
5943
+ // Individual tab panels have pointer-events: auto to receive clicks.
5944
+ // During drag, we also disable pointer events on the children to prevent
5945
+ // dragleave events on the Layout element.
5946
+ pointerEvents: "none",
5947
+ // Ensure tab container doesn't block FlexLayout's tab bar interactions
5948
+ zIndex: 0
5949
+ },
5950
+ "data-layout-path": "/tab-container"
5951
+ },
5952
+ Array.from(tabs.entries()).map(([nodeId, tabInfo]) => {
5953
+ const { node, rect, visible } = tabInfo;
5954
+ const contentClassName = node.getContentClassName();
5955
+ const hasValidDimensions = rect.width > 0 && rect.height > 0;
5956
+ return /* @__PURE__ */ React20.createElement(
5957
+ "div",
5958
+ {
5959
+ key: nodeId,
5960
+ role: "tabpanel",
5961
+ "data-tab-id": nodeId,
5962
+ className: getClassName("flexlayout__tab") + (contentClassName ? " " + contentClassName : ""),
5963
+ style: {
5964
+ position: "absolute",
5965
+ display: visible ? "flex" : "none",
5966
+ left: hasValidDimensions ? rect.x : 0,
5967
+ top: hasValidDimensions ? rect.y : 0,
5968
+ width: hasValidDimensions ? rect.width : "100%",
5969
+ height: hasValidDimensions ? rect.height : "100%",
5970
+ overflow: "auto",
5971
+ // Tab panels receive pointer events when visible and not dragging
5972
+ pointerEvents: visible && !isDragging ? "auto" : "none"
5973
+ }
5974
+ },
5975
+ renderTab(node)
5976
+ );
5977
+ })
5978
+ );
5979
+ }
5980
+ function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }) {
5981
+ const [isDragging, setIsDragging] = useState4(false);
5982
+ const [tabs, setTabs] = useState4(() => /* @__PURE__ */ new Map());
5983
+ const tabNodesRef = useRef12(/* @__PURE__ */ new Map());
5984
+ useEffect6(() => {
5985
+ const newTabNodes = /* @__PURE__ */ new Map();
5986
+ model.visitNodes((node) => {
5987
+ if (node instanceof TabNode) {
5988
+ newTabNodes.set(node.getId(), node);
5989
+ }
5990
+ });
5991
+ tabNodesRef.current = newTabNodes;
5992
+ setTabs((prevTabs) => {
5993
+ const nextTabs = /* @__PURE__ */ new Map();
5994
+ for (const [nodeId, node] of newTabNodes) {
5995
+ const existing = prevTabs.get(nodeId);
5996
+ if (existing) {
5997
+ nextTabs.set(nodeId, { ...existing, node });
5998
+ } else {
5999
+ const parent = node.getParent();
6000
+ const contentRect = parent?.getContentRect() ?? Rect.empty();
6001
+ nextTabs.set(nodeId, {
6002
+ node,
6003
+ rect: contentRect,
6004
+ visible: node.isSelected()
6005
+ });
6006
+ }
6007
+ }
6008
+ return nextTabs;
6009
+ });
6010
+ }, [model]);
6011
+ const handleRectChange = useCallback((nodeId, rect) => {
6012
+ setTabs((prevTabs) => {
6013
+ const existing = prevTabs.get(nodeId);
6014
+ if (!existing || existing.rect.equals(rect)) {
6015
+ return prevTabs;
6016
+ }
6017
+ const nextTabs = new Map(prevTabs);
6018
+ nextTabs.set(nodeId, { ...existing, rect });
6019
+ return nextTabs;
6020
+ });
6021
+ }, []);
6022
+ const handleVisibilityChange = useCallback((nodeId, visible) => {
6023
+ setTabs((prevTabs) => {
6024
+ const existing = prevTabs.get(nodeId);
6025
+ if (!existing || existing.visible === visible) {
6026
+ return prevTabs;
6027
+ }
6028
+ const nextTabs = new Map(prevTabs);
6029
+ nextTabs.set(nodeId, { ...existing, visible });
6030
+ return nextTabs;
6031
+ });
6032
+ }, []);
6033
+ const handleDragStateChange = useCallback(
6034
+ (dragging) => {
6035
+ setIsDragging(dragging);
6036
+ onDragStateChange?.(dragging);
6037
+ },
6038
+ [onDragStateChange]
6039
+ );
6040
+ const factory = useCallback(
6041
+ (node) => {
6042
+ return /* @__PURE__ */ React20.createElement(TabRef, { key: node.getId(), node, onRectChange: handleRectChange, onVisibilityChange: handleVisibilityChange });
6043
+ },
6044
+ [handleRectChange, handleVisibilityChange]
6045
+ );
6046
+ const tabsForContainer = useMemo(() => tabs, [tabs]);
6047
+ return /* @__PURE__ */ React20.createElement("div", { style: { position: "relative", width: "100%", height: "100%" } }, /* @__PURE__ */ React20.createElement(Layout, { model, factory, classNameMapper, onDragStateChange: handleDragStateChange, ...layoutProps }), /* @__PURE__ */ React20.createElement(TabContainer, { tabs: tabsForContainer, renderTab, isDragging, classNameMapper }));
6048
+ }
6049
+
5883
6050
  // src/model/walk.ts
5884
6051
  function findJsonNodeById(model, id) {
5885
6052
  let result;
@@ -5939,6 +6106,7 @@ export {
5939
6106
  LayoutWindow,
5940
6107
  Model,
5941
6108
  Node,
6109
+ OptimizedLayout,
5942
6110
  Orientation,
5943
6111
  Rect,
5944
6112
  RowNode,
@@ -0,0 +1,8 @@
1
+ declare const localStorageMock: {
2
+ getItem: (key: string) => string;
3
+ setItem: (key: string, value: string) => void;
4
+ removeItem: (key: string) => void;
5
+ clear: () => void;
6
+ readonly length: number;
7
+ key: (index: number) => string;
8
+ };
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
@@ -49,6 +49,8 @@ export interface ILayoutProps {
49
49
  onTabSetPlaceHolder?: TabSetPlaceHolderCallback;
50
50
  /** Name given to popout windows, defaults to 'Popout Window' */
51
51
  popoutWindowName?: string;
52
+ /** callback for when drag state changes, useful for OptimizedLayout to set pointer-events: none on external tab container during drag */
53
+ onDragStateChange?: (isDragging: boolean) => void;
52
54
  }
53
55
  /**
54
56
  * A React component that hosts a multi-tabbed layout
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import { TabNode } from "../model/TabNode";
3
+ import { ILayoutProps } from "./Layout";
4
+ /**
5
+ * Props for OptimizedLayout - similar to Layout but with `renderTab` instead of `factory`
6
+ */
7
+ export interface IOptimizedLayoutProps extends Omit<ILayoutProps, "factory"> {
8
+ /** Function to render tab content - receives TabNode, returns React element */
9
+ renderTab: (node: TabNode) => React.ReactNode;
10
+ }
11
+ /**
12
+ * OptimizedLayout - A wrapper around FlexLayout that renders tab content outside of
13
+ * FlexLayout's DOM structure for better performance.
14
+ *
15
+ * Key benefits:
16
+ * 1. Tab components are NOT re-rendered when Model changes
17
+ * 2. Tab state (scroll position, form inputs, etc.) is preserved across layout mutations
18
+ * 3. Only CSS properties change when layout changes - no React re-renders
19
+ *
20
+ * The component works by:
21
+ * 1. Rendering FlexLayout with TabRef placeholders instead of actual tab content
22
+ * 2. TabRef components listen to resize/visibility events from TabNodes
23
+ * 3. A sibling TabContainer renders the actual tab content with absolute positioning
24
+ * 4. During drag operations, TabContainer uses pointer-events: none to prevent
25
+ * interfering with FlexLayout's drag overlay
26
+ *
27
+ * @see https://github.com/caplin/FlexLayout/issues/456
28
+ */
29
+ export declare function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }: IOptimizedLayoutProps): React.JSX.Element;