study / web /src /components /ui /tabs.tsx
xiaolongmr
整合功能更新并排除本地恢复产物
2dc6fe4
Raw History Blame Contribute Delete
5.96 kB
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
type TabsContextValue = {
value: string;
setValue: (value: string) => void;
orientation: "horizontal" | "vertical";
};
const TabsContext = React.createContext<TabsContextValue | null>(null);
function useTabsContext(component: string) {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error(`${component} must be used inside Tabs`);
}
return context;
}
function Tabs({
className,
defaultValue = "",
value,
onValueChange,
orientation = "horizontal",
children,
...props
}: React.HTMLAttributes<HTMLDivElement> & {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
orientation?: "horizontal" | "vertical";
}) {
const [internalValue, setInternalValue] = React.useState(defaultValue);
const currentValue = value ?? internalValue;
const setValue = React.useCallback(
(nextValue: string) => {
if (value === undefined) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
},
[onValueChange, value],
);
return (
<TabsContext.Provider value={{ value: currentValue, setValue, orientation }}>
<div
data-slot="tabs"
data-orientation={orientation}
className={cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className)}
{...props}
>
{children}
</div>
</TabsContext.Provider>
);
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
},
);
function TabsList({
className,
variant = "default",
...props
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof tabsListVariants>) {
const { orientation } = useTabsContext("TabsList");
return (
<div
role="tablist"
aria-orientation={orientation}
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
}
function TabsTrigger({
className,
value,
disabled,
onClick,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }) {
const { value: currentValue, setValue } = useTabsContext("TabsTrigger");
const active = currentValue === value;
return (
<button
type="button"
role="tab"
aria-selected={active}
tabIndex={active ? 0 : -1}
disabled={disabled}
data-slot="tabs-trigger"
data-state={active ? "active" : "inactive"}
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-[background-color,border-color,color,box-shadow] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
onClick={(event) => {
onClick?.(event);
if (!event.defaultPrevented && !disabled) {
setValue(value);
}
}}
{...props}
/>
);
}
function TabsContent({
className,
value,
keepMounted = false,
...props
}: React.HTMLAttributes<HTMLDivElement> & { value: string; keepMounted?: boolean }) {
const { value: currentValue } = useTabsContext("TabsContent");
const active = currentValue === value;
const [hasBeenActive, setHasBeenActive] = React.useState(active);
React.useEffect(() => {
if (active) {
setHasBeenActive(true);
}
}, [active]);
if (!active && (!keepMounted || !hasBeenActive)) {
return null;
}
return (
<div
role="tabpanel"
data-slot="tabs-content"
data-state={active ? "active" : "inactive"}
hidden={!active}
aria-hidden={active ? undefined : true}
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };