From 91c5744275d22bb1e50e20107ecfc08394751c17 Mon Sep 17 00:00:00 2001 From: Konstantin Alekseev Date: Thu, 18 Jan 2024 10:42:23 +0200 Subject: [PATCH] [core] feat(Tabs): new lazy prop --- packages/core/src/components/tabs/tabs.tsx | 44 +++++++++++++++++++--- packages/core/test/tabs/tabsTests.tsx | 29 ++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index 8962d4b1463..2e6f93ef006 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -68,6 +68,13 @@ export interface TabsProps extends Props { */ large?: boolean; + /** + * If set to `true`, the hidden tabs won't be rendered until activated. + * + * @default false + */ + lazy?: boolean; + /** * Whether inactive tab panels should be removed from the DOM and unmounted in React. * This can be a performance enhancement when rendering many complex panels, but requires @@ -111,6 +118,7 @@ export interface TabsProps extends Props { export interface TabsState { indicatorWrapperStyle?: React.CSSProperties; + lazyRendered?: readonly TabId[]; selectedTabId?: TabId; } @@ -139,10 +147,9 @@ export class Tabs extends AbstractPureComponent { public static displayName = `${DISPLAYNAME_PREFIX}.Tabs`; - public static getDerivedStateFromProps({ selectedTabId }: TabsProps) { + public static getDerivedStateFromProps({ selectedTabId, lazy }: TabsProps, { lazyRendered }: TabsState) { if (selectedTabId !== undefined) { - // keep state in sync with controlled prop, so state is canonical source of truth - return { selectedTabId }; + return buildNextState({ selectedTabId, lazy, lazyRendered }); } return null; } @@ -156,7 +163,10 @@ export class Tabs extends AbstractPureComponent { constructor(props: TabsProps) { super(props); const selectedTabId = this.getInitialSelectedTabId(); - this.state = { selectedTabId }; + this.state = { + lazyRendered: props.lazy && selectedTabId !== undefined ? [selectedTabId] : [], + selectedTabId, + }; } public render() { @@ -166,6 +176,7 @@ export class Tabs extends AbstractPureComponent { const tabPanels = this.getTabChildren() .filter(this.props.renderActiveTabPanelOnly ? tab => tab.props.id === selectedTabId : () => true) + .filter(this.props.lazy ? tab => (this.state.lazyRendered ?? []).includes(tab.props.id) : () => true) .map(this.renderTabPanel); const tabIndicator = this.props.animate ? ( @@ -292,7 +303,13 @@ export class Tabs extends AbstractPureComponent { private handleTabClick = (newTabId: TabId, event: React.MouseEvent) => { this.props.onChange?.(newTabId, this.state.selectedTabId, event); if (this.props.selectedTabId === undefined) { - this.setState({ selectedTabId: newTabId }); + this.setState( + buildNextState({ + lazy: this.props.lazy, + lazyRendered: this.state.lazyRendered, + selectedTabId: newTabId, + }), + ); } }; @@ -362,3 +379,20 @@ export class Tabs extends AbstractPureComponent { function isTabElement(child: any): child is TabElement { return Utils.isElementOfType(child, Tab); } + +function buildNextState({ + lazy, + lazyRendered, + selectedTabId, +}: { + selectedTabId: TabId; + lazy: boolean | undefined; + lazyRendered: readonly TabId[] | undefined; +}): TabsState { + const renderedIds = lazyRendered ?? []; + return { + lazyRendered: !lazy ? [] : renderedIds.includes(selectedTabId) ? renderedIds : [...renderedIds, selectedTabId], + // keep state in sync with controlled prop, so state is canonical source of truth + selectedTabId, + }; +} diff --git a/packages/core/test/tabs/tabsTests.tsx b/packages/core/test/tabs/tabsTests.tsx index 37d60252554..9a5799a8e83 100644 --- a/packages/core/test/tabs/tabsTests.tsx +++ b/packages/core/test/tabs/tabsTests.tsx @@ -140,6 +140,35 @@ describe("", () => { } }); + it("lazy renders tab panel on activate", () => { + const wrapper = mount( + + {getTabsContents()} + , + ); + assert.deepEqual(wrapper.state("lazyRendered"), [TAB_IDS[0]]); + assert.lengthOf(wrapper.find("strong"), 1); + findTabById(wrapper, TAB_IDS[1]).simulate("click"); + assert.lengthOf(wrapper.find("strong"), 2); + assert.deepEqual(wrapper.state("lazyRendered"), [TAB_IDS[0], TAB_IDS[1]]); + findTabById(wrapper, TAB_IDS[2]).simulate("click"); + assert.lengthOf(wrapper.find("strong"), 3); + assert.deepEqual(wrapper.state("lazyRendered"), TAB_IDS); + }); + + it("lazy renders tab panel on selected tab id change", () => { + const wrapper = mount( + + {getTabsContents()} + , + ); + assert.deepEqual(wrapper.state("lazyRendered"), [TAB_IDS[1]]); + assert.lengthOf(wrapper.find("strong"), 1); + wrapper.setProps({ id: ID, lazy: true, selectedTabId: TAB_IDS[2] }); + assert.deepEqual(wrapper.state("lazyRendered"), [TAB_IDS[1], TAB_IDS[2]]); + assert.lengthOf(wrapper.find("strong"), 2); + }); + it("sets aria-* attributes with matching Ds", () => { const wrapper = mount({getTabsContents()}); wrapper.find(TAB).forEach(title => {