Skip to content

Commit 289ee7a

Browse files
committed
#1949 add tab modes feature
1 parent 84ada72 commit 289ee7a

File tree

2 files changed

+112
-67
lines changed

2 files changed

+112
-67
lines changed

client/packages/lowcoder/src/comps/comps/tabs/tabbedContainerComp.tsx

Lines changed: 104 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { NameGenerator } from "comps/utils";
1515
import { ScrollBar, Section, sectionNames } from "lowcoder-design";
1616
import { HintPlaceHolder } from "lowcoder-design";
1717
import _ from "lodash";
18-
import React, {useContext, useEffect,useState } from "react";
18+
import React, {useContext, useMemo } from "react";
1919
import styled, { css } from "styled-components";
2020
import { IContainer } from "../containerBase/iContainer";
2121
import { SimpleContainerComp } from "../containerBase/simpleContainerComp";
@@ -47,10 +47,9 @@ const EVENT_OPTIONS = [
4747
] as const;
4848

4949
const TAB_BEHAVIOR_OPTIONS = [
50-
{ label: "Lazy Loading", value: "lazy" },
51-
{ label: "Remember State", value: "remember" },
52-
{ label: "Destroy Inactive", value: "destroy" },
53-
{ label: "Keep Alive (render all)", value: "keep-alive" },
50+
{ label: trans("tabbedContainer.tabBehaviorLazy"), value: "lazy" },
51+
{ label: trans("tabbedContainer.tabBehaviorKeepAlive"), value: "keep-alive" },
52+
{ label: trans("tabbedContainer.tabBehaviorDestroy"), value: "destroy" },
5453
] as const;
5554

5655
const TabBehaviorControl = dropdownControl(TAB_BEHAVIOR_OPTIONS, "lazy");
@@ -153,7 +152,8 @@ const StyledTabs = styled(Tabs)<{
153152
$bodyStyle: TabBodyStyleType;
154153
$isMobile?: boolean;
155154
$showHeader?: boolean;
156-
$animationStyle:AnimationStyleType
155+
$animationStyle:AnimationStyleType;
156+
$isDestroyPane?: boolean;
157157
}>`
158158
&.ant-tabs {
159159
height: 100%;
@@ -166,7 +166,6 @@ const StyledTabs = styled(Tabs)<{
166166
167167
.ant-tabs-content {
168168
height: 100%;
169-
170169
}
171170
172171
.ant-tabs-nav {
@@ -183,16 +182,71 @@ const StyledTabs = styled(Tabs)<{
183182
margin-right: -24px;
184183
}
185184
186-
${(props) => props.$style && getStyle(
187-
props.$style,
188-
props.$headerStyle,
189-
props.$bodyStyle,
190-
)}
185+
${(props) =>
186+
props.$style && getStyle(props.$style, props.$headerStyle, props.$bodyStyle)}
187+
188+
/* Conditional styling for all modes except Destroy Inactive Pane */
189+
${(props) => !props.$isDestroyPane && `
190+
.ant-tabs-content-holder { position: relative; }
191+
192+
.ant-tabs-tabpane[aria-hidden="true"],
193+
.ant-tabs-tabpane-hidden {
194+
display: block !important;
195+
visibility: hidden !important;
196+
position: absolute !important;
197+
inset: 0;
198+
pointer-events: none;
199+
}
200+
`}
191201
`;
192202

193203
const ContainerInTab = (props: ContainerBaseProps) => {
204+
return <InnerGrid {...props} emptyRows={15} hintPlaceholder={HintPlaceHolder} />;
205+
};
206+
207+
type TabPaneContentProps = {
208+
autoHeight: boolean;
209+
showVerticalScrollbar: boolean;
210+
paddingWidth: number;
211+
horizontalGridCells: number;
212+
bodyBackground: string;
213+
layoutView: any;
214+
itemsView: any;
215+
positionParamsView: any;
216+
dispatch: DispatchType;
217+
};
218+
219+
const TabPaneContent: React.FC<TabPaneContentProps> = ({
220+
autoHeight,
221+
showVerticalScrollbar,
222+
paddingWidth,
223+
horizontalGridCells,
224+
bodyBackground,
225+
layoutView,
226+
itemsView,
227+
positionParamsView,
228+
dispatch,
229+
}) => {
230+
const gridItems = useMemo(() => gridItemCompToGridItems(itemsView), [itemsView]);
231+
194232
return (
195-
<InnerGrid {...props} emptyRows={15} hintPlaceholder={HintPlaceHolder} />
233+
<BackgroundColorContext.Provider value={bodyBackground}>
234+
<ScrollBar
235+
style={{ height: autoHeight ? "auto" : "100%", margin: "0px", padding: "0px" }}
236+
hideScrollbar={!showVerticalScrollbar}
237+
overflow={autoHeight ? "hidden" : "scroll"}
238+
>
239+
<ContainerInTab
240+
layout={layoutView}
241+
items={gridItems}
242+
horizontalGridCells={horizontalGridCells}
243+
positionParams={positionParamsView}
244+
dispatch={dispatch}
245+
autoHeight={autoHeight}
246+
containerPadding={[paddingWidth, 20]}
247+
/>
248+
</ScrollBar>
249+
</BackgroundColorContext.Provider>
196250
);
197251
};
198252

@@ -212,13 +266,6 @@ const TabbedContainer = (props: TabbedContainerProps) => {
212266
const selectedTab = visibleTabs.find((tab) => tab.key === props.selectedTabKey.value);
213267
const activeKey = selectedTab? selectedTab.key: visibleTabs.length > 0 ? visibleTabs[0].key : undefined;
214268

215-
// Placeholder-based lazy loading — only for "lazy" mode
216-
const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set());
217-
useEffect(() => {
218-
if (tabBehavior === "lazy" && activeKey) {
219-
setLoadedTabs((prev: Set<string>) => new Set([...prev, activeKey]));
220-
}
221-
}, [tabBehavior, activeKey]);
222269

223270
const editorState = useContext(EditorContext);
224271
const maxWidth = editorState.getAppSettings().maxWidth;
@@ -229,7 +276,7 @@ const TabbedContainer = (props: TabbedContainerProps) => {
229276
const tabItems = visibleTabs.map((tab) => {
230277
const id = String(tab.id);
231278
const childDispatch = wrapDispatch(wrapDispatch(dispatch, "containers"), id);
232-
const containerProps = containers[id].children;
279+
const containerChildren = containers[id].children;
233280
const hasIcon = tab.icon.props.value;
234281

235282
const label = (
@@ -240,50 +287,25 @@ const TabbedContainer = (props: TabbedContainerProps) => {
240287
</>
241288
);
242289

243-
// Item-level forceRender mapping
244-
const forceRender: boolean = tabBehavior === "keep-alive";
245-
246-
// Render content (placeholder only for "lazy" & not yet opened)
247-
const renderTabContent = () => {
248-
if (tabBehavior === "lazy" && !loadedTabs.has(tab.key)) {
249-
return (
250-
<div
251-
style={{
252-
display: "flex",
253-
justifyContent: "center",
254-
alignItems: "center",
255-
height: "200px",
256-
color: "#999",
257-
fontSize: "14px",
258-
}}
259-
>
260-
Click to load tab content
261-
</div>
262-
);
263-
}
264-
265-
return (
266-
<BackgroundColorContext.Provider value={bodyStyle.background}>
267-
<ScrollBar style={{ height: props.autoHeight ? "auto" : "100%", margin: "0px", padding: "0px" }} hideScrollbar={!props.showVerticalScrollbar} overflow={props.autoHeight ? 'hidden':'scroll'}>
268-
<ContainerInTab
269-
layout={containerProps.layout.getView()}
270-
items={gridItemCompToGridItems(containerProps.items.getView())}
271-
horizontalGridCells={horizontalGridCells}
272-
positionParams={containerProps.positionParams.getView()}
273-
dispatch={childDispatch}
274-
autoHeight={props.autoHeight}
275-
containerPadding={[paddingWidth, 20]}
276-
/>
277-
</ScrollBar>
278-
</BackgroundColorContext.Provider>
279-
);
280-
};
290+
const forceRender = tabBehavior === "keep-alive";
281291

282292
return {
283293
label,
284294
key: tab.key,
285-
forceRender, // true only for keep-alive
286-
children: renderTabContent(),
295+
forceRender,
296+
children: (
297+
<TabPaneContent
298+
autoHeight={props.autoHeight}
299+
showVerticalScrollbar={props.showVerticalScrollbar}
300+
paddingWidth={paddingWidth}
301+
horizontalGridCells={horizontalGridCells}
302+
bodyBackground={bodyStyle.background}
303+
layoutView={containerChildren.layout.getView()}
304+
itemsView={containerChildren.items.getView()}
305+
positionParamsView={containerChildren.positionParams.getView()}
306+
dispatch={childDispatch}
307+
/>
308+
),
287309
};
288310
});
289311

@@ -299,13 +321,11 @@ const TabbedContainer = (props: TabbedContainerProps) => {
299321
$headerStyle={headerStyle}
300322
$bodyStyle={bodyStyle}
301323
$showHeader={showHeader}
324+
$isDestroyPane={tabBehavior === "destroy"}
302325
onChange={(key) => {
303326
if (key !== props.selectedTabKey.value) {
304327
props.selectedTabKey.onChange(key);
305328
props.onEvent("change");
306-
if (tabBehavior === "lazy") {
307-
setLoadedTabs((prev: Set<string>) => new Set([...prev, key]));
308-
}
309329
}
310330
}}
311331
animated
@@ -344,7 +364,25 @@ export const TabbedContainerBaseComp = (function () {
344364
{disabledPropertyView(children)}
345365
{hiddenPropertyView(children)}
346366
{children.showHeader.propertyView({ label: trans("tabbedContainer.showTabs") })}
347-
{children.tabBehavior.propertyView({ label: "Tab Behavior" })}
367+
{children.tabBehavior.propertyView({
368+
label: trans("tabbedContainer.tabBehavior"),
369+
tooltip: (
370+
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
371+
<div>
372+
<b>{trans("tabbedContainer.tabBehaviorLazy")}:</b>
373+
&nbsp;{trans("tabbedContainer.tabBehaviorLazyTooltip")}
374+
</div>
375+
<div>
376+
<b>{trans("tabbedContainer.tabBehaviorKeepAlive")}:</b>
377+
&nbsp;{trans("tabbedContainer.tabBehaviorKeepAliveTooltip")}
378+
</div>
379+
<div>
380+
<b>{trans("tabbedContainer.tabBehaviorDestroy")}:</b>
381+
&nbsp;{trans("tabbedContainer.tabBehaviorDestroyTooltip")}
382+
</div>
383+
</div>
384+
),
385+
})}
348386
</Section>
349387
)}
350388

@@ -435,6 +473,7 @@ class TabbedContainerImplComp extends TabbedContainerBaseComp implements IContai
435473
return this;
436474
}
437475
}
476+
438477
let newInstance = super.reduce(action);
439478
if (action.type === CompActionTypes.UPDATE_NODES_V2) {
440479
// Need eval to get the value in StringControl
@@ -489,4 +528,3 @@ export const TabbedContainerComp = withExposingConfigs(TabbedContainerImplComp,
489528
new NameConfig("selectedTabKey", trans("tabbedContainer.selectedTabKeyDesc")),
490529
NameConfigHidden,
491530
]);
492-

client/packages/lowcoder/src/i18n/locales/en.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3102,7 +3102,14 @@ export const en = {
31023102
"gutter" : "Gap",
31033103
"gutterTooltip" : "The distance between tabs in px",
31043104
"tabsCentered" : "Centered Tabs",
3105-
"destroyInactiveTab": "Destroy Inactive TabPane"
3105+
"destroyInactiveTab": "Destroy Inactive TabPane",
3106+
"tabBehavior": "Tab Behavior",
3107+
"tabBehaviorLazy": "Lazy",
3108+
"tabBehaviorKeepAlive": "Keep Alive",
3109+
"tabBehaviorDestroy": "Destroy Inactive",
3110+
"tabBehaviorLazyTooltip": "Render tabs only when they are first activated. Hidden tabs are not rendered until selected.",
3111+
"tabBehaviorKeepAliveTooltip": "Keep all tab contents mounted and initialized. Hidden tabs remain mounted but are visually hidden.",
3112+
"tabBehaviorDestroyTooltip": "Unmount contents of inactive tabs to free resources. Hidden tabs are destroyed until selected again."
31063113
},
31073114
"formComp": {
31083115
"containerPlaceholder": "Drag Components from the Right Pane or",

0 commit comments

Comments
 (0)