-
Notifications
You must be signed in to change notification settings - Fork 4
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
Showing
12 changed files
with
378 additions
and
1 deletion.
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
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 @@ | ||
import cx from "classnames"; | ||
import React, { ReactNode } from "react"; | ||
import Btn from "../Btn"; | ||
import { useTabsContext } from "./context"; | ||
import css from "./index.module.css"; | ||
|
||
type Props = { | ||
children: ReactNode; | ||
className?: string; | ||
index: number; | ||
name?: string; | ||
renderOnlyChild?: boolean; | ||
hide?: boolean; | ||
}; | ||
|
||
export default function Tab(props: Props) { | ||
const { activeTabIndex, setActiveTabIndex } = useTabsContext(); | ||
const isActive = props.index === activeTabIndex; | ||
const tabLabel = `tab${props.name ? `-${props.name}` : ""}`; | ||
const label = `${isActive ? "active-" : ""}${tabLabel}`; | ||
|
||
if (props.hide) return null; | ||
|
||
return ( | ||
<li | ||
data-cy={label} | ||
aria-label={label} | ||
className={cx(css.tab, props.className, { | ||
[css.activeTab]: isActive, | ||
})} | ||
> | ||
{props.renderOnlyChild ? ( | ||
props.children | ||
) : ( | ||
<Btn onClick={() => setActiveTabIndex(props.index)}> | ||
{props.children} | ||
</Btn> | ||
)} | ||
</li> | ||
); | ||
} |
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 @@ | ||
import cx from "classnames"; | ||
import React, { ReactNode } from "react"; | ||
import css from "./index.module.css"; | ||
|
||
type Props = { | ||
children: ReactNode[]; | ||
className?: string; | ||
}; | ||
|
||
export default function TabList(props: Props) { | ||
return <ul className={cx(css.tabList, props.className)}>{props.children}</ul>; | ||
} |
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,14 @@ | ||
import React, { ReactNode } from "react"; | ||
import { useTabsContext } from "./context"; | ||
|
||
type Props = { | ||
children: ReactNode; | ||
className?: string; | ||
index: number; | ||
}; | ||
|
||
export default function TabPanel(props: Props) { | ||
const { activeTabIndex } = useTabsContext(); | ||
if (props.index !== activeTabIndex) return null; | ||
return <div className={props.className}>{props.children}</div>; | ||
} |
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,36 @@ | ||
import { createCustomContext } from "@dolthub/react-contexts"; | ||
import { useContextWithError } from "@dolthub/react-hooks"; | ||
import React, { ReactNode, useMemo, useState } from "react"; | ||
|
||
type TabsContextType = { | ||
activeTabIndex: number; | ||
setActiveTabIndex: React.Dispatch<React.SetStateAction<number>>; | ||
}; | ||
|
||
export const TabsContext = createCustomContext<TabsContextType>("TabsContext"); | ||
|
||
type Props = { | ||
children: ReactNode; | ||
initialActiveIndex?: number; | ||
}; | ||
|
||
export function TabsProvider(props: Props) { | ||
const [activeTabIndex, setActiveTabIndex] = useState( | ||
props.initialActiveIndex ?? 0, | ||
); | ||
|
||
const value = useMemo(() => { | ||
return { | ||
activeTabIndex, | ||
setActiveTabIndex, | ||
}; | ||
}, [activeTabIndex, setActiveTabIndex]); | ||
|
||
return ( | ||
<TabsContext.Provider value={value}>{props.children}</TabsContext.Provider> | ||
); | ||
} | ||
|
||
export function useTabsContext(): TabsContextType { | ||
return useContextWithError(TabsContext); | ||
} |
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,39 @@ | ||
.tabList { | ||
@apply w-full overflow-x-auto flex border-b border-ld-lightgrey; | ||
|
||
@media (max-width: 756px) { | ||
/* Hide scrollbar for IE, Edge, and Firefox */ | ||
-ms-overflow-style: none; | ||
scrollbar-width: none; | ||
/* Hide scrollbar for Chrome, Safari and Opera */ | ||
&::-webkit-scrollbar { | ||
@apply hidden; | ||
} | ||
} | ||
} | ||
|
||
.tab { | ||
@apply px-4 mx-3; | ||
|
||
button, | ||
a { | ||
@apply flex justify-center items-center mt-1 min-w-full px-0 font-semibold pb-2; | ||
} | ||
} | ||
|
||
.tab:hover, | ||
.activeTab { | ||
@apply text-acc-1 border-b-3 border-acc-1; | ||
|
||
a { | ||
@apply text-acc-1; | ||
} | ||
} | ||
|
||
.tabs { | ||
@apply w-full; | ||
|
||
@screen md { | ||
@apply w-auto; | ||
} | ||
} |
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 cx from "classnames"; | ||
import React, { ReactNode } from "react"; | ||
import Tab from "./Tab"; | ||
import TabList from "./TabList"; | ||
import TabPanel from "./TabPanel"; | ||
import { TabsProvider } from "./context"; | ||
import css from "./index.module.css"; | ||
|
||
type Props = { | ||
children: ReactNode[]; | ||
initialActiveIndex?: number; | ||
className?: string; | ||
}; | ||
|
||
function Tabs({ children, ...props }: Props) { | ||
return ( | ||
<TabsProvider {...props}> | ||
<div className={cx(css.tabs, props.className)}>{children}</div> | ||
</TabsProvider> | ||
); | ||
} | ||
|
||
export { Tab, TabList, TabPanel, Tabs }; |
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,93 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import React from "react"; | ||
import { Tab, TabList, TabPanel, Tabs } from "../Tabs"; | ||
|
||
const meta: Meta<typeof Tabs> = { | ||
title: "Tabs", | ||
component: Tabs, | ||
tags: ["autodocs"], | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof Tabs>; | ||
|
||
function Panel(props: { text: string; idx: number }) { | ||
return ( | ||
<TabPanel index={props.idx}> | ||
<div className="my-6 mx-3">{props.text}</div> | ||
</TabPanel> | ||
); | ||
} | ||
|
||
const tabs = ["Tab 1", "Tab 2"]; | ||
const panels = [ | ||
<Panel key="1" text="Content 1 for Tab 1" idx={0} />, | ||
<Panel key="2" text="Content 2 for Tab 2" idx={1} />, | ||
]; | ||
|
||
export const Basic: Story = { | ||
args: { | ||
initialActiveIndex: 0, | ||
children: [ | ||
<TabList key="tabList"> | ||
{tabs.map((tab, index) => ( | ||
<Tab key={tab} index={index}> | ||
{tab} | ||
</Tab> | ||
))} | ||
</TabList>, | ||
...panels, | ||
], | ||
}, | ||
}; | ||
|
||
export const SecondActive: Story = { | ||
args: { | ||
initialActiveIndex: 1, | ||
children: [ | ||
<TabList key="tabList"> | ||
{tabs.map((tab, index) => ( | ||
<Tab key={tab} index={index}> | ||
{tab} | ||
</Tab> | ||
))} | ||
</TabList>, | ||
...panels, | ||
], | ||
}, | ||
}; | ||
|
||
export const HideLastTab: Story = { | ||
args: { | ||
initialActiveIndex: 0, | ||
children: [ | ||
<TabList key="tabList"> | ||
{tabs.map((tab, index) => ( | ||
<Tab key={tab} index={index}> | ||
{tab} | ||
</Tab> | ||
))} | ||
<Tab index={2} hide> | ||
Tab 3 | ||
</Tab> | ||
</TabList>, | ||
...panels, | ||
], | ||
}, | ||
}; | ||
|
||
export const TabWithLink: Story = { | ||
args: { | ||
children: [ | ||
<TabList key="tabList"> | ||
{tabs.map((tab, index) => ( | ||
<Tab key={tab} index={index} renderOnlyChild> | ||
<a href="#">{tab}</a> | ||
</Tab> | ||
))} | ||
</TabList>, | ||
...panels, | ||
], | ||
}, | ||
}; |
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,114 @@ | ||
import { fireEvent, render, screen } from "@testing-library/react"; | ||
import React from "react"; | ||
import { Tab, TabList, TabPanel, Tabs } from "../Tabs"; | ||
|
||
const mocks = [ | ||
{ tabWord: "zero", panelWord: "Cero" }, | ||
{ tabWord: "one", panelWord: "Uno" }, | ||
{ tabWord: "two", panelWord: "Dos" }, | ||
{ tabWord: "three", panelWord: "Tres", hide: true }, | ||
]; | ||
|
||
const testingActiveTab = ( | ||
elementCollection: HTMLElement[], | ||
activeIndex: number, | ||
) => { | ||
elementCollection.forEach(currentTab => { | ||
if (currentTab === elementCollection[activeIndex]) { | ||
expect(currentTab).toHaveAttribute("aria-label", "active-tab"); | ||
expect(currentTab).not.toHaveAttribute("aria-label", "tab"); | ||
} else { | ||
expect(currentTab).not.toHaveAttribute("aria-label", "active-tab"); | ||
expect(currentTab).toHaveAttribute("aria-label", "tab"); | ||
} | ||
}); | ||
}; | ||
|
||
describe("test Tabs", () => { | ||
it(`renders active tabs and panels correctly`, () => { | ||
render( | ||
<Tabs> | ||
<TabList> | ||
{mocks.map((mock, i) => ( | ||
<Tab | ||
index={i} | ||
data-cy={mock.tabWord} | ||
key={`tab-${mock.tabWord}`} | ||
aria-label="tab" | ||
hide={mock.hide} | ||
> | ||
{mock.tabWord} | ||
</Tab> | ||
))} | ||
</TabList> | ||
{mocks.map((mock, i) => ( | ||
<TabPanel index={i} key={`tabPanel-${mock.panelWord}`}> | ||
{mock.panelWord} | ||
</TabPanel> | ||
))} | ||
</Tabs>, | ||
); | ||
const listItems = screen.getAllByRole("listitem"); | ||
const buttons = screen.getAllByRole("button"); | ||
|
||
for (let i = 0; i < mocks.length; i++) { | ||
if (mocks[i].hide) { | ||
expect(screen.queryByText(mocks[i].panelWord)).not.toBeInTheDocument(); | ||
expect(screen.queryByText(mocks[i].tabWord)).not.toBeInTheDocument(); | ||
} else { | ||
if (i !== 0) { | ||
fireEvent.click(buttons[i]); | ||
} | ||
|
||
testingActiveTab(listItems, i); | ||
const firstPage = screen.queryByText("Cero"); | ||
const secondPage = screen.queryByText("Uno"); | ||
const thirdPage = screen.queryByText("Dos"); | ||
const pages = [firstPage, secondPage, thirdPage]; | ||
|
||
pages.forEach((page, idx) => { | ||
if (i === idx) { | ||
expect(page).toBeInTheDocument(); | ||
} else { | ||
expect(page).not.toBeInTheDocument(); | ||
} | ||
}); | ||
} | ||
} | ||
}); | ||
it(`renders initialActiveIndex correctly`, () => { | ||
render( | ||
<Tabs initialActiveIndex={2}> | ||
<TabList aria-label="tabList"> | ||
{mocks.map((mock, i) => ( | ||
<Tab | ||
index={i} | ||
data-cy={mock.tabWord} | ||
key={`tab-${mock.tabWord}`} | ||
aria-label="tab" | ||
> | ||
{mock.tabWord} | ||
</Tab> | ||
))} | ||
</TabList> | ||
{mocks.map((mock, i) => ( | ||
<TabPanel index={i} key={`tabPanel-${mock.panelWord}`}> | ||
{mock.panelWord} | ||
</TabPanel> | ||
))} | ||
</Tabs>, | ||
); | ||
|
||
const listItems = screen.getAllByRole("listitem"); | ||
|
||
testingActiveTab(listItems, 2); | ||
|
||
const firstPage = screen.queryByText("Cero"); | ||
const secondPage = screen.queryByText("Uno"); | ||
const thirdPage = screen.queryByText("Dos"); | ||
|
||
expect(thirdPage).toBeInTheDocument(); | ||
expect(firstPage).not.toBeInTheDocument(); | ||
expect(secondPage).not.toBeInTheDocument(); | ||
}); | ||
}); |
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
Oops, something went wrong.