Skip to content

Commit

Permalink
tabs: add tests to catch regressions
Browse files Browse the repository at this point in the history
  • Loading branch information
Chance Strickland committed Jun 11, 2020
1 parent 23612ec commit 91eabcb
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 18 deletions.
62 changes: 55 additions & 7 deletions packages/tabs/__tests__/tabs.test.tsx
Expand Up @@ -51,7 +51,9 @@ describe("<Tabs />", () => {
});

it("renders as a custom component", () => {
const Wrapper = (props: any) => <div data-testid="wrap" {...props} />;
const Wrapper = React.forwardRef<any, any>((props, ref) => (
<div ref={ref} data-testid="wrap" {...props} />
));
const { getByTestId } = render(
<Tabs as={Wrapper}>
<TabList>
Expand Down Expand Up @@ -106,7 +108,9 @@ describe("<Tabs />", () => {
expect(getByTestId("list").tagName).toBe("UL");
});
it("renders as a custom component", () => {
const List = (props: any) => <ul data-testid="list" {...props} />;
const List = React.forwardRef<any, any>((props, ref) => (
<ul data-testid="list" ref={ref} {...props} />
));
const { getByTestId } = render(
<Tabs>
<TabList as={List}>
Expand Down Expand Up @@ -161,7 +165,9 @@ describe("<Tabs />", () => {
expect(getByText("Tab 1").tagName).toBe("LI");
});
it("renders as a custom component", () => {
const ListItem = (props: any) => <li {...props} />;
const ListItem = React.forwardRef<any, any>((props, ref) => (
<li ref={ref} {...props} />
));
const { getByText } = render(
<Tabs>
<TabList as="ul">
Expand Down Expand Up @@ -214,7 +220,9 @@ describe("<Tabs />", () => {
expect(getByText("Panel 1").tagName).toBe("P");
});
it("renders as a custom component", () => {
const Panel = (props: any) => <p {...props} />;
const Panel = React.forwardRef<any, any>((props, ref) => (
<p ref={ref} {...props} />
));
const { getByText } = render(
<Tabs>
<TabList>
Expand Down Expand Up @@ -246,6 +254,46 @@ describe("<Tabs />", () => {
expect(style.borderStyle).toBe("dashed");
expect(style.borderColor).toBe("red");
});
it("is hidden or not based on selected state", () => {
const { getByText } = render(
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Panel 1</TabPanel>
<TabPanel>Panel 2</TabPanel>
</TabPanels>
</Tabs>
);
expect(getByText("Panel 1")).toBeVisible();
expect(getByText("Panel 2")).not.toBeVisible();
});
it("can interact with elements on initial load", () => {
const Comp = () => {
let input = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
input.current!.focus();
}, []);
return (
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>
<input type="text" ref={input} />
</TabPanel>
<TabPanel>Panel 2</TabPanel>
</TabPanels>
</Tabs>
);
};
const { getByRole } = render(<Comp />);
expect(getByRole("textbox")).toHaveFocus();
});
});

describe("<TabPanels />", () => {
Expand All @@ -265,9 +313,9 @@ describe("<Tabs />", () => {
expect(getByTestId("panels").tagName).toBe("SECTION");
});
it("renders as a custom component", () => {
const Panels = (props: any) => (
<section data-testid="panels" {...props} />
);
const Panels = React.forwardRef<any, any>((props, ref) => (
<section data-testid="panels" ref={ref} {...props} />
));
const { getByTestId } = render(
<Tabs>
<TabList>
Expand Down
31 changes: 20 additions & 11 deletions packages/tabs/src/index.tsx
Expand Up @@ -619,36 +619,45 @@ export const TabPanel = forwardRefWithAs<TabPanelProps, "div">(
TabsContext
);
let ownRef = useRef<HTMLElement | null>(null);
let isMountedRef = useRef<boolean>(false);

let index = useDescendant(
{ element: ownRef.current! },
TabPanelDescendantsContext
);
let isSelected = index === selectedIndex;

let id = makeId(tabsId, "panel", index);

// Because useDescendant will always return -1 on the first render,
// `isSelected` will briefly be false for all tabs. We set a tab panel's
// hidden attribute based `isSelected` being false, meaning that all tabs
// are initially hidden. This makes it impossible for consumers to do
// certain things, like focus an element inside the active tab panel when
// the page loads. So what we can do is track that a panel is "ready" to be
// hidden once effects are run (descendants work their magic in
// useLayoutEffect, so we can set our ref in useEffecct to run later). We
// can use a ref instead of state because we're always geting a re-render
// anyway thanks to descendants. This is a little more coupled to the
// implementation details of descendants than I'd like, but we'll add a test
// to (hopefully) catch any regressions.
let isSelected = index === selectedIndex;
let readyToHide = useRef(false);
let hidden = readyToHide.current ? !isSelected : false;
React.useEffect(() => {
readyToHide.current = true;
}, []);

let ref = useForkedRef(
forwardedRef,
ownRef,
isSelected ? selectedPanelRef : null
);

React.useEffect(() => {
isMountedRef.current = true;
}, []);

return (
<Comp
// Each element with role `tabpanel` has the property `aria-labelledby`
// referring to its associated tab element.
aria-labelledby={makeId(tabsId, "tab", index)}
// During the initial render `isSelected` would be `false`
// and hide the children, which prevents focusing via refs on mount.
// As a workaround, we wait for the component to mount, and then set the `hidden` attribute.
// I guess this is hackish, but it works.
hidden={isMountedRef.current ? !isSelected : false}
hidden={hidden}
// Each element that contains the content panel for a tab has role
// `tabpanel`.
// https://www.w3.org/TR/wai-aria-practices-1.2/#tabpanel
Expand Down

0 comments on commit 91eabcb

Please sign in to comment.