Tab component from Headless UI

This commit is contained in:
Kalle (Sendou) 2021-10-30 16:33:38 +03:00
parent b4c5270ed6
commit b1b49729c8
7 changed files with 122 additions and 85 deletions

View File

@ -0,0 +1,81 @@
import { Tab as HeadlessTab } from "@headlessui/react";
import { ComponentProps } from "react";
import { css } from "stitches.config";
export function Tab(props: any) {
return (
<HeadlessTab
{...props}
className={({ selected }) => (selected ? tab({ type: "active" }) : tab())}
/>
);
}
function Group(props: ComponentProps<typeof HeadlessTab["Group"]>) {
return <HeadlessTab.Group {...props} />;
}
function List({
tabsCount,
...props
}: ComponentProps<typeof HeadlessTab["List"]> & { tabsCount: number }) {
return (
<HeadlessTab.List
{...props}
className={container()}
style={{ "--tabs-count": tabsCount }}
/>
);
}
function Panels(props: ComponentProps<typeof HeadlessTab["Panels"]>) {
return <HeadlessTab.Panels {...props} />;
}
function Panel(props: ComponentProps<typeof HeadlessTab["Panel"]>) {
return <HeadlessTab.Panel {...props} />;
}
Tab.Group = Group;
Tab.List = List;
Tab.Panels = Panels;
Tab.Panel = Panel;
const container = css({
display: "grid",
justifyContent: "center",
placeItems: "center",
gap: "$10",
gridTemplateColumns: "repeat(var(--tabs-count), 85px)",
});
const tab = css({
all: "unset",
fontSize: "$sm",
cursor: "pointer",
"&::after": {
display: "block",
width: "1.25rem",
height: "3px",
borderBottom: "3px solid",
content: '""',
borderColor: "transparent",
},
"&:hover::after": {
borderColor: "$themeTransparent",
},
variants: {
type: {
active: {
fontWeight: "$bold",
"&::after": {
borderColor: "$theme !important",
},
},
},
},
});

View File

@ -1,67 +0,0 @@
import styled from "@emotion/styled";
import { ReactNode } from "react";
const Container = ({
children,
tabsCount,
}: {
children: ReactNode;
tabsCount: number;
}) => {
return <S.Container tabsCount={tabsCount}>{children}</S.Container>;
};
const Tab = ({
children,
active,
onClick,
}: {
children: ReactNode;
active?: boolean;
onClick?: () => void;
}) => {
return (
<S.TabButton active={active} onClick={onClick}>
{children}
</S.TabButton>
);
};
const S = {
Container: styled.div<{ tabsCount: number }>`
display: grid;
grid-template-columns: repeat(${(props) => props.tabsCount}, 85px);
justify-content: center;
place-items: center;
gap: 2.5rem;
`,
TabButton: styled.button<{ active?: boolean }>`
all: unset;
font-weight: ${(props) => (props.active ? "bold" : 500)};
font-size: 0.9rem;
cursor: pointer;
::after {
display: block;
width: 1.25rem;
height: 3px;
border-bottom: 3px solid
${(props) =>
props.active ? "var(--colors-theme) !important" : "transparent"};
content: "";
}
:hover::after {
border-color: var(--colors-theme-transparent);
}
:focus-visible::after {
border-color: var(--colors-theme-transparent);
}
`,
};
export default {
Container,
Tab,
};

View File

@ -46,7 +46,7 @@ export function Layout({ children }: { children: ReactNode }) {
<S_Nav>
<S_NavItems>
{navItems.map((navItem) => (
<S_NavItemColumn>
<S_NavItemColumn key={navItem.title}>
<S_NavGroupTitle>{navItem.title}</S_NavGroupTitle>
{navItem.items.map((item) => (
<S_NavLink key={item} href="/">
@ -123,7 +123,7 @@ const S_NavLink = stitchesStyled("a", {
textTransform: "capitalize",
transition: "0.2s transform",
":hover": {
"&:hover": {
transform: "translateX(2px)",
},
});
@ -131,7 +131,7 @@ const S_NavLink = stitchesStyled("a", {
// TODO: figure out whether to server items from asset or public and migrate all images there
// + if not using next image do some adjusting of the image size, format etc.
const S_NavLinkImage = stitchesStyled("img", {
width: "$7",
width: "1.75rem",
marginRight: "$2",
});

View File

@ -7,6 +7,7 @@
"name": "@sendou.ink/frontend",
"dependencies": {
"@emotion/styled": "^11.3.0",
"@headlessui/react": "^1.4.1",
"@mantine/core": "^3.0.5",
"@mantine/hooks": "^3.0.5",
"@mantine/next": "^3.0.5",
@ -865,6 +866,18 @@
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
"integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw=="
},
"node_modules/@headlessui/react": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@mantine/core": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-3.0.5.tgz",
@ -8413,6 +8426,12 @@
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
"integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw=="
},
"@headlessui/react": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"requires": {}
},
"@mantine/core": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-3.0.5.tgz",

View File

@ -8,6 +8,7 @@
},
"dependencies": {
"@emotion/styled": "^11.3.0",
"@headlessui/react": "^1.4.1",
"@mantine/core": "^3.0.5",
"@mantine/hooks": "^3.0.5",
"@mantine/next": "^3.0.5",

View File

@ -3,6 +3,7 @@ import { createGetInitialProps } from "@mantine/next";
const getInitialProps = createGetInitialProps();
// TODO: https://stitches.dev/blog/using-nextjs-with-stitches
export default class _Document extends Document {
static getInitialProps = getInitialProps;
}

View File

@ -1,7 +1,6 @@
import styled from "@emotion/styled";
import { useState } from "react";
import { InfoBanner } from "../../../components/tournament/InfoBanner";
import Tabs from "../../../components/common/Tabs";
import { InfoBanner } from "components/tournament/InfoBanner";
import { Tab } from "components/common/Tab";
const tabs = [
{ name: "Overview", id: "info" },
@ -13,21 +12,24 @@ const tabs = [
];
export default function TournamentPage() {
const [activeTab, setActiveTab] = useState("info");
return (
<S.Container>
<InfoBanner />
<Tabs.Container tabsCount={tabs.length}>
{tabs.map((tab) => (
<Tabs.Tab
key={tab.id}
active={activeTab === tab.id}
onClick={() => setActiveTab(tab.id)}
>
{tab.name}
</Tabs.Tab>
))}
</Tabs.Container>
<Tab.Group>
<Tab.List tabsCount={tabs.length}>
{tabs.map((tab) => (
<Tab key={tab.id}>{tab.name}</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel>
<Tab.Panel>Content 4</Tab.Panel>
<Tab.Panel>Content 5</Tab.Panel>
<Tab.Panel>Content 6</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</S.Container>
);
}