new-personal-site/components/lib/elements/Tabs.tsx
Benjamin Toby a0a0ab8ee4 Updates
2025-07-20 10:35:54 +01:00

136 lines
4.7 KiB
TypeScript

import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import Border from "./Border";
import Stack from "../layout/Stack";
import Row from "../layout/Row";
import twuiSlugify from "../utils/slugify";
export type TWUITabsObject = {
title: string;
value?: string;
content: React.ReactNode;
defaultActive?: boolean;
};
export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
tabsContentArray: (TWUITabsObject | TWUITabsObject[] | undefined | null)[];
tabsBorderProps?: React.ComponentProps<typeof Border>;
tabsButtonsWrapperProps?: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
centered?: boolean;
debounce?: number;
/**
* React Component to display when switching
*/
switchComponent?: ReactNode;
setActiveValue?: React.Dispatch<React.SetStateAction<string | undefined>>;
};
/**
* # Tabs Component
* @className twui-tabs-wrapper
* @className twui-tab-buttons
* @className twui-tab-button-active
* @className twui-tab-buttons-wrapper
*/
export default function Tabs({
tabsContentArray,
tabsBorderProps,
tabsButtonsWrapperProps,
centered,
debounce = 100,
switchComponent,
setActiveValue: existingSetActiveValue,
...props
}: TWUI_TOGGLE_PROPS) {
const finalTabsContentArray = tabsContentArray
.flat()
.filter((ct) => Boolean(ct?.title)) as TWUITabsObject[];
const values = finalTabsContentArray.map(
(obj) => obj.value || twuiSlugify(obj.title)
);
const defaultActiveObj = finalTabsContentArray.find(
(ctn) => ctn.defaultActive
);
const [activeValue, setActiveValue] = React.useState(
defaultActiveObj
? defaultActiveObj?.value || twuiSlugify(defaultActiveObj.title)
: values[0] || undefined
);
const targetContent = finalTabsContentArray.find(
(ctn) =>
ctn.value == activeValue || twuiSlugify(ctn.title) == activeValue
);
React.useEffect(() => {
existingSetActiveValue?.(activeValue);
}, [activeValue]);
return (
<Stack
{...props}
className={twMerge("w-full", "twui-tabs-wrapper", props.className)}
>
<div
{...tabsButtonsWrapperProps}
className={twMerge(
"w-full",
"twui-tab-buttons-wrapper",
tabsButtonsWrapperProps?.className
)}
>
<Border
className="p-0 w-full overflow-hidden"
{...tabsBorderProps}
>
<Row
className={twMerge(
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
centered && "justify-center"
)}
>
{values.map((value, index) => {
const targetObject = finalTabsContentArray.find(
(ctn) =>
ctn.value == value ||
twuiSlugify(ctn.title) == value
);
const isActive = value == activeValue;
return (
<span
className={twMerge(
"px-6 py-2 rounded-default -ml-[1px] whitespace-nowrap",
isActive
? "bg-primary dark:bg-primary-dark text-white outline-none twui-tab-button-active"
: "text-slate-400 dark:text-white/40 hover:text-slate-800 dark:hover:text-white" +
" cursor-pointer",
"twui-tab-buttons"
)}
onClick={() => {
setActiveValue(undefined);
setTimeout(() => {
setActiveValue(value);
}, debounce);
}}
key={index}
>
{targetObject?.title}
</span>
);
})}
</Row>
</Border>
</div>
{activeValue ? targetContent?.content : switchComponent || null}
</Stack>
);
}