@aptre/flex-layout 0.3.4 → 0.4.0

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 (70) 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 +199 -52
  7. package/dist/model/IJsonModel.d.ts +4 -4
  8. package/dist/test/setup.d.ts +8 -0
  9. package/dist/test/unit-setup.d.ts +1 -0
  10. package/dist/view/Layout.d.ts +2 -0
  11. package/dist/view/OptimizedLayout.d.ts +29 -0
  12. package/package.json +119 -102
  13. package/tsconfig.json +18 -18
  14. package/typedoc/assets/hierarchy.js +1 -1
  15. package/typedoc/assets/main.js +5 -5
  16. package/typedoc/assets/navigation.js +1 -1
  17. package/typedoc/assets/search.js +1 -1
  18. package/typedoc/assets/style.css +251 -228
  19. package/typedoc/classes/Action.html +4 -4
  20. package/typedoc/classes/Actions.html +74 -74
  21. package/typedoc/classes/BorderNode.html +25 -25
  22. package/typedoc/classes/BorderSet.html +2 -2
  23. package/typedoc/classes/DockLocation.html +10 -10
  24. package/typedoc/classes/DropInfo.html +7 -7
  25. package/typedoc/classes/Layout.html +96 -96
  26. package/typedoc/classes/LayoutWindow.html +12 -12
  27. package/typedoc/classes/Model.html +42 -42
  28. package/typedoc/classes/Node.html +12 -12
  29. package/typedoc/classes/Orientation.html +6 -6
  30. package/typedoc/classes/Rect.html +26 -26
  31. package/typedoc/classes/RowNode.html +16 -16
  32. package/typedoc/classes/TabNode.html +39 -39
  33. package/typedoc/classes/TabSetNode.html +39 -39
  34. package/typedoc/enums/CLASSES.html +94 -94
  35. package/typedoc/enums/I18nLabel.html +11 -11
  36. package/typedoc/enums/ICloseType.html +4 -4
  37. package/typedoc/functions/OptimizedLayout.html +18 -0
  38. package/typedoc/functions/findJsonNodeById.html +4 -4
  39. package/typedoc/functions/walkJsonModel.html +3 -3
  40. package/typedoc/hierarchy.html +1 -1
  41. package/typedoc/index.html +1 -1
  42. package/typedoc/interfaces/IBorderAttributes.html +25 -25
  43. package/typedoc/interfaces/IDraggable.html +1 -1
  44. package/typedoc/interfaces/IDropTarget.html +1 -1
  45. package/typedoc/interfaces/IGlobalAttributes.html +99 -99
  46. package/typedoc/interfaces/IIcons.html +9 -9
  47. package/typedoc/interfaces/IJsonBorderNode.html +27 -27
  48. package/typedoc/interfaces/IJsonModel.html +5 -5
  49. package/typedoc/interfaces/IJsonPopout.html +3 -3
  50. package/typedoc/interfaces/IJsonRect.html +5 -5
  51. package/typedoc/interfaces/IJsonRowNode.html +8 -8
  52. package/typedoc/interfaces/IJsonTabNode.html +53 -53
  53. package/typedoc/interfaces/IJsonTabSetNode.html +52 -52
  54. package/typedoc/interfaces/ILayoutProps.html +41 -39
  55. package/typedoc/interfaces/IOptimizedLayoutProps.html +42 -0
  56. package/typedoc/interfaces/IRowAttributes.html +7 -7
  57. package/typedoc/interfaces/ITabAttributes.html +53 -53
  58. package/typedoc/interfaces/ITabRenderValues.html +7 -7
  59. package/typedoc/interfaces/ITabSetAttributes.html +47 -47
  60. package/typedoc/interfaces/ITabSetRenderValues.html +7 -7
  61. package/typedoc/interfaces/VisitorResult.html +5 -5
  62. package/typedoc/types/DragRectRenderCallback.html +1 -1
  63. package/typedoc/types/IBorderLocation.html +1 -1
  64. package/typedoc/types/ITabLocation.html +1 -1
  65. package/typedoc/types/JsonNode.html +2 -2
  66. package/typedoc/types/ModelVisitor.html +5 -5
  67. package/typedoc/types/NodeMouseEvent.html +1 -1
  68. package/typedoc/types/ShowOverflowMenuCallback.html +1 -1
  69. package/typedoc/types/TabSetPlaceHolderCallback.html +1 -1
  70. 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) {
@@ -3537,6 +3557,7 @@ var Splitter = (props) => {
3537
3557
  function BorderTab(props) {
3538
3558
  const { layout, border, show } = props;
3539
3559
  const selfRef = React3.useRef(null);
3560
+ const isFirstRender = React3.useRef(true);
3540
3561
  React3.useLayoutEffect(() => {
3541
3562
  const outerRect = layout.getBoundingClientRect(selfRef.current);
3542
3563
  const contentRect = Rect.getContentRect(selfRef.current).relativeTo(layout.getDomRect());
@@ -3544,9 +3565,12 @@ function BorderTab(props) {
3544
3565
  border.setOuterRect(outerRect);
3545
3566
  if (!border.getContentRect().equals(contentRect)) {
3546
3567
  border.setContentRect(contentRect);
3547
- layout.redrawInternal("border content rect");
3568
+ if (!isFirstRender.current) {
3569
+ layout.redrawInternal("border content rect");
3570
+ }
3548
3571
  }
3549
3572
  }
3573
+ isFirstRender.current = false;
3550
3574
  });
3551
3575
  let horizontal = true;
3552
3576
  const style2 = {};
@@ -3574,16 +3598,6 @@ import * as React8 from "react";
3574
3598
 
3575
3599
  // src/view/BorderButton.tsx
3576
3600
  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
3601
  var BorderButton = (props) => {
3588
3602
  const { layout, node, selected, border, icons, path } = props;
3589
3603
  const selfRef = React4.useRef(null);
@@ -3617,20 +3631,8 @@ var BorderButton = (props) => {
3617
3631
  layout.setEditingTab(void 0);
3618
3632
  }
3619
3633
  };
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
3634
  const onClose = (event) => {
3633
- if (isClosable()) {
3635
+ if (isTabClosable(node, selected)) {
3634
3636
  layout.doAction(Actions.deleteTab(node.getId()));
3635
3637
  } else {
3636
3638
  onClick();
@@ -3834,12 +3836,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3834
3836
  React7.useLayoutEffect(() => {
3835
3837
  userControlledLeft.current = false;
3836
3838
  }, [node.getSelectedNode(), node.getRect().width, node.getRect().height]);
3839
+ const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3837
3840
  React7.useLayoutEffect(() => {
3838
- const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3839
3841
  if (nodeRect.width > 0 && nodeRect.height > 0) {
3840
3842
  updateVisibleTabs();
3841
3843
  }
3842
- });
3844
+ }, [nodeRect.width, nodeRect.height]);
3843
3845
  const instance = toolbarRef.current;
3844
3846
  React7.useEffect(() => {
3845
3847
  if (!instance) {
@@ -3879,15 +3881,15 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3879
3881
  if (firstRender.current === true) {
3880
3882
  tabsTruncated.current = false;
3881
3883
  }
3882
- const nodeRect = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3884
+ const nodeRect2 = node instanceof TabSetNode ? node.getRect() : node.getTabHeaderRect();
3883
3885
  const lastChild = node.getChildren()[node.getChildren().length - 1];
3884
3886
  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) {
3887
+ if (firstRender.current === true || lastHiddenCount.current === 0 && hiddenTabs.length !== 0 || nodeRect2.width !== lastRect.current.width || // incase rect changed between first render and second
3888
+ nodeRect2.height !== lastRect.current.height) {
3887
3889
  lastHiddenCount.current = hiddenTabs.length;
3888
- lastRect.current = nodeRect;
3890
+ lastRect.current = nodeRect2;
3889
3891
  const enabled = node instanceof TabSetNode ? node.isEnableTabStrip() === true : true;
3890
- let endPos = getFar(nodeRect) - stickyButtonsSize;
3892
+ let endPos = getFar(nodeRect2) - stickyButtonsSize;
3891
3893
  if (toolbarRef.current !== null) {
3892
3894
  endPos -= getSize(toolbarRef.current.getBoundingClientRect());
3893
3895
  }
@@ -3901,12 +3903,12 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3901
3903
  const selectedRect = selectedTab.getTabRect();
3902
3904
  const selectedStart = getNear(selectedRect) - tabMargin;
3903
3905
  const selectedEnd = getFar(selectedRect) + tabMargin;
3904
- if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect)) {
3905
- shiftPos = getNear(nodeRect) - selectedStart;
3906
+ if (getSize(selectedRect) + 2 * tabMargin >= endPos - getNear(nodeRect2)) {
3907
+ shiftPos = getNear(nodeRect2) - selectedStart;
3906
3908
  } else {
3907
- if (selectedEnd > endPos || selectedStart < getNear(nodeRect)) {
3908
- if (selectedStart < getNear(nodeRect)) {
3909
- shiftPos = getNear(nodeRect) - selectedStart;
3909
+ if (selectedEnd > endPos || selectedStart < getNear(nodeRect2)) {
3910
+ if (selectedStart < getNear(nodeRect2)) {
3911
+ shiftPos = getNear(nodeRect2) - selectedStart;
3910
3912
  }
3911
3913
  if (selectedEnd + shiftPos > endPos) {
3912
3914
  shiftPos = endPos - selectedEnd;
@@ -3920,7 +3922,7 @@ var useTabOverflow = (node, orientation, toolbarRef, stickyButtonsRef) => {
3920
3922
  const hidden = [];
3921
3923
  for (let i = 0; i < node.getChildren().length; i++) {
3922
3924
  const child = node.getChildren()[i];
3923
- if (getNear(child.getTabRect()) + diff < getNear(nodeRect) || getFar(child.getTabRect()) + diff > endPos) {
3925
+ if (getNear(child.getTabRect()) + diff < getNear(nodeRect2) || getFar(child.getTabRect()) + diff > endPos) {
3924
3926
  hidden.push({ node: child, index: i });
3925
3927
  }
3926
3928
  }
@@ -4371,21 +4373,9 @@ var TabButton = (props) => {
4371
4373
  layout.setEditingTab(void 0);
4372
4374
  }
4373
4375
  };
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
4376
  const onClose = (event) => {
4387
4377
  event.preventDefault();
4388
- if (isClosable()) {
4378
+ if (isTabClosable(node, selected)) {
4389
4379
  layout.doAction(Actions.deleteTab(node.getId()));
4390
4380
  } else {
4391
4381
  onClick();
@@ -4502,6 +4492,7 @@ var TabSet = (props) => {
4502
4492
  const overflowbuttonRef = React15.useRef(null);
4503
4493
  const stickyButtonsRef = React15.useRef(null);
4504
4494
  const icons = layout.getIcons();
4495
+ const isFirstRender = React15.useRef(true);
4505
4496
  React15.useEffect(() => {
4506
4497
  node.setRect(layout.getBoundingClientRect(selfRef.current));
4507
4498
  if (tabStripRef.current) {
@@ -4510,8 +4501,11 @@ var TabSet = (props) => {
4510
4501
  const newContentRect = Rect.getContentRect(contentRef.current).relativeTo(layout.getDomRect());
4511
4502
  if (!node.getContentRect().equals(newContentRect)) {
4512
4503
  node.setContentRect(newContentRect);
4513
- layout.redrawInternal("tabset content rect " + newContentRect);
4504
+ if (!isFirstRender.current) {
4505
+ layout.redrawInternal("tabset content rect " + newContentRect);
4506
+ }
4514
4507
  }
4508
+ isFirstRender.current = false;
4515
4509
  });
4516
4510
  const { selfRef, position, userControlledLeft, hiddenTabs, onMouseWheel, tabsTruncated } = useTabOverflow(node, Orientation.HORZ, buttonBarRef, stickyButtonsRef);
4517
4511
  const onOverflowClick = (event) => {
@@ -4924,7 +4918,7 @@ var Tab = (props) => {
4924
4918
  firstSelect.current = false;
4925
4919
  }
4926
4920
  }
4927
- });
4921
+ }, [selected]);
4928
4922
  const onPointerDown = () => {
4929
4923
  const parent = node.getParent();
4930
4924
  if (parent instanceof TabSetNode) {
@@ -5764,6 +5758,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
5764
5758
  this.showOverlay(false);
5765
5759
  this.dragEnterCount = 0;
5766
5760
  this.dragging = false;
5761
+ this.props.onDragStateChange?.(false);
5767
5762
  if (this.outlineDiv) {
5768
5763
  this.selfRef.current.removeChild(this.outlineDiv);
5769
5764
  this.outlineDiv = void 0;
@@ -5795,6 +5790,7 @@ var LayoutInternal = class _LayoutInternal extends React19.Component {
5795
5790
  rootdiv.appendChild(this.outlineDiv);
5796
5791
  this.dragging = true;
5797
5792
  this.showOverlay(true);
5793
+ this.props.onDragStateChange?.(true);
5798
5794
  if (!this.isDraggingOverWindow && this.props.model.getMaximizedTabset(this.windowId) === void 0) {
5799
5795
  this.setState({ showEdges: this.props.model.isEnableEdgeDock() });
5800
5796
  }
@@ -5880,6 +5876,156 @@ var DragState = class {
5880
5876
  }
5881
5877
  };
5882
5878
 
5879
+ // src/view/OptimizedLayout.tsx
5880
+ import * as React20 from "react";
5881
+ import { useCallback, useEffect as useEffect6, useMemo, useRef as useRef12, useState as useState4 } from "react";
5882
+ function TabRef({ node, onRectChange, onVisibilityChange }) {
5883
+ useEffect6(() => {
5884
+ const handleResize = (params) => {
5885
+ onRectChange(node.getId(), params.rect);
5886
+ };
5887
+ const handleVisibility = (params) => {
5888
+ onVisibilityChange(node.getId(), params.visible);
5889
+ };
5890
+ node.setEventListener("resize", handleResize);
5891
+ node.setEventListener("visibility", handleVisibility);
5892
+ const parent = node.getParent();
5893
+ if (parent) {
5894
+ const contentRect = parent.getContentRect();
5895
+ if (contentRect && contentRect.width > 0 && contentRect.height > 0) {
5896
+ onRectChange(node.getId(), contentRect);
5897
+ }
5898
+ }
5899
+ onVisibilityChange(node.getId(), node.isSelected());
5900
+ return () => {
5901
+ node.removeEventListener("resize");
5902
+ node.removeEventListener("visibility");
5903
+ };
5904
+ }, [node, onRectChange, onVisibilityChange]);
5905
+ return null;
5906
+ }
5907
+ function TabContainer({
5908
+ tabs,
5909
+ renderTab,
5910
+ isDragging,
5911
+ classNameMapper
5912
+ }) {
5913
+ const getClassName = useCallback(
5914
+ (defaultClassName) => {
5915
+ return classNameMapper ? classNameMapper(defaultClassName) : defaultClassName;
5916
+ },
5917
+ [classNameMapper]
5918
+ );
5919
+ return /* @__PURE__ */ React20.createElement(
5920
+ "div",
5921
+ {
5922
+ style: {
5923
+ position: "absolute",
5924
+ inset: 0,
5925
+ // CRITICAL: Disable pointer events during drag to prevent drag overlay from disappearing
5926
+ // When tabs are rendered outside FlexLayout's DOM, dragging over them triggers dragleave
5927
+ // on the Layout element, causing dragEnterCount to drop to 0 and clearing the drag UI.
5928
+ pointerEvents: isDragging ? "none" : "auto",
5929
+ // Ensure tab container doesn't block FlexLayout's tab bar interactions when not dragging
5930
+ zIndex: 0
5931
+ },
5932
+ "data-layout-path": "/tab-container"
5933
+ },
5934
+ Array.from(tabs.entries()).map(([nodeId, tabInfo]) => {
5935
+ const { node, rect, visible } = tabInfo;
5936
+ const contentClassName = node.getContentClassName();
5937
+ return /* @__PURE__ */ React20.createElement(
5938
+ "div",
5939
+ {
5940
+ key: nodeId,
5941
+ role: "tabpanel",
5942
+ "data-tab-id": nodeId,
5943
+ className: getClassName("flexlayout__tab") + (contentClassName ? " " + contentClassName : ""),
5944
+ style: {
5945
+ position: "absolute",
5946
+ display: visible ? "flex" : "none",
5947
+ left: rect.x,
5948
+ top: rect.y,
5949
+ width: rect.width,
5950
+ height: rect.height,
5951
+ overflow: "auto"
5952
+ }
5953
+ },
5954
+ renderTab(node)
5955
+ );
5956
+ })
5957
+ );
5958
+ }
5959
+ function OptimizedLayout({ model, renderTab, classNameMapper, onDragStateChange, ...layoutProps }) {
5960
+ const [isDragging, setIsDragging] = useState4(false);
5961
+ const [tabs, setTabs] = useState4(() => /* @__PURE__ */ new Map());
5962
+ const tabNodesRef = useRef12(/* @__PURE__ */ new Map());
5963
+ useEffect6(() => {
5964
+ const newTabNodes = /* @__PURE__ */ new Map();
5965
+ model.visitNodes((node) => {
5966
+ if (node instanceof TabNode) {
5967
+ newTabNodes.set(node.getId(), node);
5968
+ }
5969
+ });
5970
+ tabNodesRef.current = newTabNodes;
5971
+ setTabs((prevTabs) => {
5972
+ const nextTabs = /* @__PURE__ */ new Map();
5973
+ for (const [nodeId, node] of newTabNodes) {
5974
+ const existing = prevTabs.get(nodeId);
5975
+ if (existing) {
5976
+ nextTabs.set(nodeId, { ...existing, node });
5977
+ } else {
5978
+ const parent = node.getParent();
5979
+ const contentRect = parent?.getContentRect() ?? Rect.empty();
5980
+ nextTabs.set(nodeId, {
5981
+ node,
5982
+ rect: contentRect,
5983
+ visible: node.isSelected()
5984
+ });
5985
+ }
5986
+ }
5987
+ return nextTabs;
5988
+ });
5989
+ }, [model]);
5990
+ const handleRectChange = useCallback((nodeId, rect) => {
5991
+ setTabs((prevTabs) => {
5992
+ const existing = prevTabs.get(nodeId);
5993
+ if (!existing || existing.rect.equals(rect)) {
5994
+ return prevTabs;
5995
+ }
5996
+ const nextTabs = new Map(prevTabs);
5997
+ nextTabs.set(nodeId, { ...existing, rect });
5998
+ return nextTabs;
5999
+ });
6000
+ }, []);
6001
+ const handleVisibilityChange = useCallback((nodeId, visible) => {
6002
+ setTabs((prevTabs) => {
6003
+ const existing = prevTabs.get(nodeId);
6004
+ if (!existing || existing.visible === visible) {
6005
+ return prevTabs;
6006
+ }
6007
+ const nextTabs = new Map(prevTabs);
6008
+ nextTabs.set(nodeId, { ...existing, visible });
6009
+ return nextTabs;
6010
+ });
6011
+ }, []);
6012
+ const handleDragStateChange = useCallback(
6013
+ (dragging) => {
6014
+ setIsDragging(dragging);
6015
+ onDragStateChange?.(dragging);
6016
+ },
6017
+ [onDragStateChange]
6018
+ );
6019
+ const factory = useCallback(
6020
+ (node) => {
6021
+ return /* @__PURE__ */ React20.createElement(TabRef, { key: node.getId(), node, onRectChange: handleRectChange, onVisibilityChange: handleVisibilityChange });
6022
+ },
6023
+ [handleRectChange, handleVisibilityChange]
6024
+ );
6025
+ const tabsForContainer = useMemo(() => tabs, [tabs]);
6026
+ 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 }));
6027
+ }
6028
+
5883
6029
  // src/model/walk.ts
5884
6030
  function findJsonNodeById(model, id) {
5885
6031
  let result;
@@ -5939,6 +6085,7 @@ export {
5939
6085
  LayoutWindow,
5940
6086
  Model,
5941
6087
  Node,
6088
+ OptimizedLayout,
5942
6089
  Orientation,
5943
6090
  Rect,
5944
6091
  RowNode,
@@ -424,7 +424,7 @@ export interface IRowAttributes {
424
424
  */
425
425
  id?: string;
426
426
  /** The type of this node (always "row") */
427
- type?: "row";
427
+ type: "row";
428
428
  /**
429
429
  relative weight for sizing of this row in parent row
430
430
 
@@ -560,7 +560,7 @@ export interface ITabSetAttributes {
560
560
  */
561
561
  tabLocation?: ITabLocation;
562
562
  /** The type of this node (always "tabset") */
563
- type?: "tabset";
563
+ type: "tabset";
564
564
  /**
565
565
  relative weight for sizing of this tabset in parent row
566
566
 
@@ -721,7 +721,7 @@ export interface ITabAttributes {
721
721
  */
722
722
  tabsetClassName?: string;
723
723
  /** The type of this node (always "tab") */
724
- type?: "tab";
724
+ type: "tab";
725
725
  }
726
726
  export interface IBorderAttributes {
727
727
  /**
@@ -791,5 +791,5 @@ export interface IBorderAttributes {
791
791
  */
792
792
  size?: number;
793
793
  /** The type of this node (always "border") */
794
- type?: "border";
794
+ type: "border";
795
795
  }
@@ -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;