Skip to content

Commit

Permalink
fix(tabs): memoize tabRef array so it regenerate when children change
Browse files Browse the repository at this point in the history
Updates implementation of `tabRefs` to `useMemo`, this means they will regenerate if the children
change at all

fix #4498
  • Loading branch information
edleeks87 committed Oct 25, 2021
1 parent 4713dea commit eef4267
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 19 deletions.
42 changes: 23 additions & 19 deletions src/components/tabs/tabs.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import React, {
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
createRef,
cloneElement,
Children,
} from "react";
import PropTypes from "prop-types";
import styledSystemPropTypes from "@styled-system/prop-types";
Expand Down Expand Up @@ -37,7 +41,19 @@ const Tabs = ({
headerWidth,
...rest
}) => {
const tabRefs = useRef([]);
/** The children nodes converted into an Array */
const filteredChildren = useMemo(
() => Children.toArray(children).filter((child) => child),
[children]
);

/** Array of refs to the TabTitle nodes */
const tabRefs = useMemo(
() =>
Array.from({ length: filteredChildren.length }).map(() => createRef()),
[filteredChildren.length]
);

const previousSelectedTabId = useRef(selectedTabId);
const [selectedTabIdState, setSelectedTabIdState] = useState();
const { isInSidebar } = useContext(DrawerSidebarContext);
Expand All @@ -47,7 +63,7 @@ const Tabs = ({

useLayoutEffect(() => {
const selectedTab =
selectedTabId || React.Children.toArray(children)[0].props.tabId;
selectedTabId || Children.toArray(children)[0].props.tabId;

setSelectedTabIdState(selectedTab);
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down Expand Up @@ -106,13 +122,7 @@ const Tabs = ({
};

/** Focuses the tab for the reference specified */
const focusTab = (ref) => ref.focus();

/** The children nodes converted into an Array */
const filteredChildren = React.useMemo(
() => React.Children.toArray(children).filter((child) => child),
[children]
);
const focusTab = (ref) => ref.current.focus();

/** Array of the tabIds for the child nodes */
const tabIds = () => {
Expand All @@ -132,7 +142,7 @@ const Tabs = ({
newIndex = 0;
}
const nextTabId = ids[newIndex];
const nextRef = tabRefs.current[newIndex];
const nextRef = tabRefs[newIndex];
updateVisibleTab(nextTabId);
focusTab(nextRef);
};
Expand All @@ -156,12 +166,6 @@ const Tabs = ({
/** Returns true/false for if the given tab id is selected. */
const isTabSelected = (tabId) => tabId === selectedTabIdState;

const addRef = (ref) => {
if (ref && !tabRefs.current.includes(ref)) {
tabRefs.current.push(ref);
}
};

/** Build the headers for the tab component */
const renderTabHeaders = () => {
const tabTitles = filteredChildren.map((child, index) => {
Expand Down Expand Up @@ -218,7 +222,7 @@ const Tabs = ({
key={tabId}
onClick={handleTabClick}
onKeyDown={handleKeyDown(index)}
ref={(node) => addRef(node)}
ref={tabRefs[index]}
tabIndex={isTabSelected(tabId) ? "0" : "-1"}
title={title}
href={href}
Expand Down Expand Up @@ -270,7 +274,7 @@ const Tabs = ({
});

return tab
? React.cloneElement(tab, {
? cloneElement(tab, {
isTabSelected: isTabSelected(tab.props.tabId),
})
: null;
Expand All @@ -285,7 +289,7 @@ const Tabs = ({
}

const tabs = filteredChildren.map((child) => {
return React.cloneElement(child, {
return cloneElement(child, {
...child.props,
role: "tabpanel",
position,
Expand Down
91 changes: 91 additions & 0 deletions src/components/tabs/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TabContext } from "./tab/index";
import { rootTagTest } from "../../__internal__/utils/helpers/tags/tags-specs";
import StyledTabs from "./tabs.style";
import StyledTab from "./tab/tab.style";
import { StyledTabTitle } from "./__internal__/tab-title/tab-title.style";
import {
assertStyleMatch,
simulate,
Expand Down Expand Up @@ -734,6 +735,96 @@ describe("Tabs", () => {
expect(tabTitle.at(1).props().info).toEqual(false);
});
});

describe("Keyboard behaviour", () => {
let container;
let wrapper;
const tabTitles = ["tab-1", "tab-2", "tab-3"];

const ConditionalChildrenMock = () => {
const [showAllTabs, setShowAllTabs] = React.useState(true);

const generateTab = (tabTitle) => (
<Tab title={tabTitle} tabId={tabTitle} key={tabTitle}>
{tabTitle}
</Tab>
);

return (
<>
<button
id="foo"
type="button"
onClick={() => setShowAllTabs((prev) => !prev)}
>
Toggle children
</button>
<Tabs>
{!showAllTabs && generateTab(tabTitles[0])}
{showAllTabs &&
tabTitles.map((tabTitle) => generateTab(tabTitle))}
</Tabs>
</>
);
};

beforeEach(() => {
container = document.createElement("div");
container.id = "enzymeContainer";
document.body.appendChild(container);

wrapper = mount(<ConditionalChildrenMock />, {
attachTo: document.getElementById("enzymeContainer"),
});
});

afterEach(() => {
if (container?.parentNode) {
container.parentNode.removeChild(container);
}

container = null;
});

const runFocusExpectations = (keyDown, array) =>
array.forEach((index) => {
const child = wrapper.update().find(StyledTabTitle).at(index);
expect(child.getDOMNode()).toBeFocused();
simulate.keydown[keyDown](child);
});

const toggleChildren = () => {
act(() => {
wrapper.find("#foo").prop("onClick")();
});

expect(wrapper.update().find(StyledTabTitle).length).toEqual(1);

act(() => {
wrapper.find("#foo").prop("onClick")();
});

expect(wrapper.update().find(StyledTabTitle).length).toEqual(3);
};

it("is consistent when navigating with the arrow keys and the composition of the children changes", () => {
wrapper.find(StyledTabTitle).first().getDOMNode().focus();

runFocusExpectations("pressLeftArrow", [0, 2, 1, 0, 2]);

toggleChildren();

wrapper.find(StyledTabTitle).first().getDOMNode().focus();

runFocusExpectations("pressLeftArrow", [0, 2, 1, 0, 2]);

toggleChildren();

wrapper.find(StyledTabTitle).first().getDOMNode().focus();

runFocusExpectations("pressRightArrow", [0, 1, 2, 0]);
});
});
});

describe("tags", () => {
Expand Down

0 comments on commit eef4267

Please sign in to comment.