sendou.ink/app/components/elements/Tabs.tsx

151 lines
3.8 KiB
TypeScript

import clsx from "clsx";
import { TriangleAlert } from "lucide-react";
import {
Tab,
TabList,
type TabListProps,
TabPanel,
type TabPanelProps,
type TabProps,
Tabs,
type TabsProps,
} from "react-aria-components";
import { useMainContentWidth } from "~/hooks/useMainContentWidth";
import buttonStyles from "./Button.module.css";
import styles from "./Tabs.module.css";
interface SendouTabsProps extends TabsProps {
/** Should there be padding above the panels. Defaults to true, pass in false if the panel content is managing its own padding. */
padded?: boolean;
/** Hide tabs if only one tab shown? Defaults to true. */
disappearing?: boolean;
/** When orientation is "vertical", switch to horizontal once the main content width drops below this many pixels. */
horizontalBelow?: number;
}
/**
* Renders a set of accessible tabs using the provided props.
*
* This component is a wrapper around the `Tabs` component, forwarding all props.
*
* @param props - The properties to pass to the underlying `Tabs` component.
* @returns The rendered tab interface.
*
* @url https://react-spectrum.adobe.com/react-aria/Tabs.html
*
* @example
* <SendouTabs>
* <SendouTabList>
* <Tab id="shooter">Shooter</Tab>
* <Tab id="roller">Roller</Tab>
* <Tab id="charger">Charger</Tab>
* </SendouTabList>
* <SendouTabPanel id="shooter">
* Splattershot, Aerospray, etc.
* </SendouTabPanel>
* <SendouTabPanel id="roller">
* Splat Roller, Dynamo Roller, etc.
* </SendouTabPanel>
* <SendouTabPanel id="charger">
* Splat Charger, E-liter, etc.
* </SendouTabPanel>
* </SendouTabs>
*/
export function SendouTabs({
padded = true,
disappearing = true,
horizontalBelow,
className,
orientation,
onSelectionChange,
...rest
}: SendouTabsProps) {
const mainWidth = useMainContentWidth();
const collapsedToHorizontal =
orientation === "vertical" &&
typeof horizontalBelow === "number" &&
mainWidth > 0 &&
mainWidth < horizontalBelow;
const effectiveOrientation = collapsedToHorizontal
? "horizontal"
: orientation;
const isVertical = effectiveOrientation === "vertical";
return (
<Tabs
orientation={effectiveOrientation}
onSelectionChange={onSelectionChange}
className={clsx(className, styles.root, {
[styles.padded]: padded,
[styles.disappearing]: disappearing,
[styles.vertical]: isVertical,
})}
{...rest}
/>
);
}
interface SendouTabProps extends TabProps {
icon?: React.ReactNode;
number?: number;
/** Render a warning-colored alert icon to draw attention to this tab. */
alert?: boolean;
children?: React.ReactNode;
}
export function SendouTab({
icon,
children,
number,
alert,
...rest
}: SendouTabProps) {
return (
<Tab className={styles.tabContainer} {...rest}>
<div className={clsx(buttonStyles.button, styles.tabButton)}>
{icon}
{children}
{typeof number === "number" && number !== 0 && (
<span className={styles.tabNumber}>{number}</span>
)}
{alert ? <TriangleAlert className={styles.tabAlert} /> : null}
</div>
</Tab>
);
}
interface SendouTabListProps<T extends object> extends TabListProps<T> {
sticky?: boolean;
/** Should tabs take 100% width with equal distribution? */
fullWidth?: boolean;
}
export function SendouTabList<T extends object>({
sticky,
fullWidth,
...rest
}: SendouTabListProps<T>) {
return (
<div className={clsx(styles.tabListContainer, "scrollbar")}>
<TabList
className={clsx(styles.tabList, {
// invisible: cantSwitchTabs && !disappearing,
// hidden: cantSwitchTabs && disappearing,
[styles.sticky]: sticky,
[styles.fullWidth]: fullWidth,
})}
{...rest}
/>
</div>
);
}
interface SendouTabPanelProps extends TabPanelProps {
className?: string;
}
export function SendouTabPanel({ className, ...rest }: SendouTabPanelProps) {
return <TabPanel className={clsx(className, styles.tabPanel)} {...rest} />;
}