-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
01479f4
commit d7ec2b1
Showing
5 changed files
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
```jsx | ||
<Tab initialOpenIndex={1}> | ||
<Tab.Link title="Tab 1" /> | ||
<Tab.Link title="Tab 2" /> | ||
<Tab.Link title="Tab 3" /> | ||
<Tab.Panel index={0}>Stuff Goes here</Tab.Panel> | ||
<Tab.Panel index={1}>Other Goes here</Tab.Panel> | ||
<Tab.Panel index={2}>More Stuff Goes here</Tab.Panel> | ||
</Tab> | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* eslint-disable react/jsx-key */ | ||
/** @jsxImportSource theme-ui */ | ||
|
||
import Tab, { TabProps } from "./Tab" | ||
import { Meta, Story } from "@storybook/react/types-6-0" | ||
import Paper from "../Paper/Paper" | ||
|
||
export default { | ||
title: "Design System/Tab", | ||
component: Tab, | ||
decorators: [ | ||
(Story) => ( | ||
<Paper sx={{ p: 0, width: 500 }}> | ||
<Story /> | ||
</Paper> | ||
), | ||
], | ||
} as Meta | ||
|
||
const Template: Story<TabProps> = (props) => ( | ||
<Tab {...props} initialOpenIndex={1} /> | ||
) | ||
|
||
// Each story then reuses that template | ||
export const Default = Template.bind({}) | ||
Default.args = { | ||
children: [ | ||
<Tab.Link title="Tab 1" />, | ||
<Tab.Link title="Tab 2" />, | ||
<Tab.Link title="Tab 3" />, | ||
<Tab.Panel index={0}>Stuff Goes here</Tab.Panel>, | ||
<Tab.Panel index={1}>Other Goes here</Tab.Panel>, | ||
<Tab.Panel index={2}>More Stuff Goes here</Tab.Panel>, | ||
], | ||
} | ||
|
||
export const WithInitialOpen = Template.bind({}) | ||
WithInitialOpen.args = { | ||
...Default.args, | ||
initialOpenIndex: 0, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { render, screen, waitFor } from "../../tests/utils" | ||
import { axe } from "jest-axe" | ||
import Tab from "./Tab" | ||
import userEvent from "@testing-library/user-event" | ||
|
||
describe("Tab", () => { | ||
test("do not renders closed tab", async () => { | ||
// Arrange | ||
render( | ||
<Tab initialOpenIndex={1}> | ||
<Tab.Link title="ITEM_TITLE_0" /> | ||
<Tab.Link title="ITEM_TITLE_1" /> | ||
<Tab.Link title="ITEM_TITLE_2" /> | ||
<Tab.Panel index={0}>ITEM_PANEL_0</Tab.Panel> | ||
<Tab.Panel index={1}>ITEM_PANEL_1</Tab.Panel> | ||
<Tab.Panel index={2}>ITEM_PANEL_2</Tab.Panel> | ||
</Tab> | ||
) | ||
// Assert | ||
expect(screen.queryByText("ITEM_TITLE_0")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_TITLE_1")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_TITLE_2")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_0")).not.toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_1")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_2")).not.toBeInTheDocument() | ||
}) | ||
|
||
test("opens when clicked", async () => { | ||
// Arrange | ||
render( | ||
<Tab initialOpenIndex={1}> | ||
<Tab.Link title="ITEM_TITLE_0" /> | ||
<Tab.Link title="ITEM_TITLE_1" /> | ||
<Tab.Link title="ITEM_TITLE_2" /> | ||
<Tab.Panel index={0}>ITEM_PANEL_0</Tab.Panel> | ||
<Tab.Panel index={1}>ITEM_PANEL_1</Tab.Panel> | ||
<Tab.Panel index={2}>ITEM_PANEL_2</Tab.Panel> | ||
</Tab> | ||
) | ||
// Assert | ||
expect(screen.queryByText("ITEM_TITLE_0")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_TITLE_1")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_TITLE_2")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_0")).not.toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_1")).toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_2")).not.toBeInTheDocument() | ||
|
||
userEvent.click(screen.getByText("ITEM_TITLE_2")) | ||
await waitFor(() => | ||
expect(screen.queryByText("ITEM_PANEL_2")).toBeInTheDocument() | ||
) | ||
|
||
userEvent.click(screen.getByText("ITEM_TITLE_0")) | ||
await waitFor(() => { | ||
expect(screen.queryByText("ITEM_PANEL_2")).not.toBeInTheDocument() | ||
expect(screen.queryByText("ITEM_PANEL_0")).toBeInTheDocument() | ||
}) | ||
}) | ||
|
||
test("passes a11y check", async () => { | ||
// Arrange | ||
const { container } = render( | ||
<Tab initialOpenIndex={1}> | ||
<Tab.Link title="ITEM_TITLE_0" /> | ||
<Tab.Link title="ITEM_TITLE_1" /> | ||
<Tab.Link title="ITEM_TITLE_2" /> | ||
<Tab.Panel index={0}>ITEM_PANEL_0</Tab.Panel> | ||
<Tab.Panel index={1}>ITEM_PANEL_1</Tab.Panel> | ||
<Tab.Panel index={2}>ITEM_PANEL_2</Tab.Panel> | ||
</Tab> | ||
) | ||
const results = await axe(container) | ||
|
||
// Assert | ||
expect(results).toHaveNoViolations() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/** @jsxImportSource theme-ui */ | ||
import { get } from "@theme-ui/css" | ||
import React, { useCallback, useState } from "react" | ||
import { useTabContext } from "./context" | ||
import { TabContext } from "./context" | ||
|
||
export interface TabProps { | ||
initialOpenIndex?: number | ||
children: React.ReactNode | ||
activeBackgroundColor?: | ||
| "primary" | ||
| "secondary" | ||
| "text" | ||
| "success" | ||
| "warning" | ||
} | ||
|
||
const Tab = ({ | ||
children, | ||
initialOpenIndex, | ||
activeBackgroundColor = "secondary", | ||
}: TabProps) => { | ||
const [openIndex, setOpenIndex] = useState<number | undefined>( | ||
initialOpenIndex | ||
) | ||
|
||
const toggleOpen = useCallback( | ||
(index: number) => { | ||
if (openIndex === index) setOpenIndex(undefined) | ||
else setOpenIndex(index) | ||
}, | ||
[openIndex] | ||
) | ||
|
||
const allItems = React.Children.toArray(children) | ||
.filter((child) => { | ||
return React.isValidElement(child) | ||
}) | ||
.map((child, index) => ( | ||
<TabContext.Provider | ||
value={{ index, toggleOpen, openIndex, activeBackgroundColor }} | ||
key={index} | ||
> | ||
{child} | ||
</TabContext.Provider> | ||
)) | ||
|
||
const tabLink = allItems.filter((item) => | ||
item.props.children.props.hasOwnProperty("title") | ||
) | ||
const tabPanel = allItems.filter((item) => | ||
item.props.children.props.hasOwnProperty("index") | ||
) | ||
return ( | ||
<> | ||
<div sx={{ display: "flex", flexDirection: "row" }}> | ||
{tabLink.map((item) => item)} | ||
</div> | ||
<div>{tabPanel.map((item) => item)}</div> | ||
</> | ||
) | ||
} | ||
|
||
export interface TabLinkProps { | ||
title: string | ||
// children: React.ReactNode | ||
// className?: HTMLAttributes<HTMLDivElement>["className"] | ||
} | ||
|
||
const TabLink = ({ title }: TabLinkProps) => { | ||
const { index, toggleOpen, openIndex, activeBackgroundColor } = | ||
useTabContext() | ||
const isOpen = openIndex === index | ||
return ( | ||
<div | ||
role="button" | ||
aria-disabled="false" | ||
aria-expanded={isOpen} | ||
sx={{ | ||
px: 6, | ||
py: 2, | ||
variant: "text.body1", | ||
color: "text.40", | ||
cursor: "pointer", | ||
position: "relative", | ||
overflow: "hidden", | ||
borderRadius: 2, | ||
height: "36px", | ||
borderBottomLeftRadius: 0, | ||
borderBottomRightRadius: 0, | ||
border: isOpen ? "1px solid #E8E8E9" : "none", | ||
borderBottom: "none", | ||
borderTop: "none", | ||
boxSizing: "border-box", | ||
backgroundColor: (t) => | ||
isOpen ? get(t, `colors.${activeBackgroundColor}.97`) : "transparent", | ||
transition: "background-color 150ms linear", | ||
}} | ||
onClick={() => toggleOpen(index)} | ||
> | ||
<span | ||
sx={{ | ||
position: "absolute", | ||
top: 0, | ||
left: 0, | ||
height: "2px", | ||
width: "100%", | ||
backgroundColor: isOpen ? "primary" : "transparent", | ||
}} | ||
/> | ||
<span sx={{ variant: "text.heading4" }}>{title}</span> | ||
</div> | ||
) | ||
} | ||
|
||
export interface TabPanelProps { | ||
children: React.ReactNode | ||
index: number | ||
} | ||
|
||
const TabPanel = ({ children, index }: TabPanelProps) => { | ||
const { openIndex } = useTabContext() | ||
const isOpen = openIndex === index | ||
return ( | ||
<> | ||
{isOpen && ( | ||
<div | ||
id={index.toString()} | ||
sx={{ | ||
display: "block", | ||
position: "relative", | ||
py: 5, | ||
px: 6, | ||
borderRadius: "12px", | ||
borderTopLeftRadius: openIndex == 0 && isOpen ? "0" : "12px", | ||
border: "1px solid #E8E8E9", | ||
backgroundColor: "#F3F3F3", | ||
}} | ||
> | ||
{children} | ||
</div> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
Tab.Link = TabLink | ||
Tab.Panel = TabPanel | ||
export default Tab |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { createContext, useContext } from "react" | ||
|
||
export interface TabContext { | ||
index: number | ||
openIndex?: number | ||
toggleOpen: (index: number) => void | ||
activeBackgroundColor: | ||
| "primary" | ||
| "secondary" | ||
| "text" | ||
| "success" | ||
| "warning" | ||
} | ||
|
||
export const TabContext = createContext<TabContext | null>(null) | ||
|
||
export function useTabContext() { | ||
const context = useContext(TabContext) | ||
if (!context) { | ||
throw new Error("Tab.Link needs to be wrapped in an Tab component") | ||
} | ||
return context | ||
} |