Skip to content

Commit

Permalink
[core] feat(Tabs): new lazy prop
Browse files Browse the repository at this point in the history
  • Loading branch information
kalekseev committed Jan 18, 2024
1 parent 573c6ea commit 91c5744
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 5 deletions.
44 changes: 39 additions & 5 deletions packages/core/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +118,7 @@ export interface TabsProps extends Props {

export interface TabsState {
indicatorWrapperStyle?: React.CSSProperties;
lazyRendered?: readonly TabId[];
selectedTabId?: TabId;
}

Expand Down Expand Up @@ -139,10 +147,9 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {

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;
}
Expand All @@ -156,7 +163,10 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {
constructor(props: TabsProps) {
super(props);
const selectedTabId = this.getInitialSelectedTabId();
this.state = { selectedTabId };
this.state = {
lazyRendered: props.lazy && selectedTabId !== undefined ? [selectedTabId] : [],
selectedTabId,
};
}

public render() {
Expand All @@ -166,6 +176,7 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {

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 ? (
Expand Down Expand Up @@ -292,7 +303,13 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {
private handleTabClick = (newTabId: TabId, event: React.MouseEvent<HTMLElement>) => {
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,
}),
);
}
};

Expand Down Expand Up @@ -362,3 +379,20 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {
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,
};
}
29 changes: 29 additions & 0 deletions packages/core/test/tabs/tabsTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,35 @@ describe("<Tabs>", () => {
}
});

it("lazy renders tab panel on activate", () => {
const wrapper = mount(
<Tabs id={ID} lazy={true}>
{getTabsContents()}
</Tabs>,
);
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(
<Tabs id={ID} lazy={true} selectedTabId={TAB_IDS[1]}>
{getTabsContents()}
</Tabs>,
);
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(<Tabs id={ID}>{getTabsContents()}</Tabs>);
wrapper.find(TAB).forEach(title => {
Expand Down

0 comments on commit 91c5744

Please sign in to comment.