Files
lumina/components/ui/tabs.tsx
2025-02-07 19:36:13 +01:00

136 lines
3.5 KiB
TypeScript

"use client";
import { cn } from "@/lib/utils";
import Link, { LinkProps } from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import * as React from "react";
import { Suspense } from "react";
interface Context {
defaultValue: string;
hrefFor: (value: string) => LinkProps["href"];
searchParam: string;
selected: string;
}
const TabsContext = React.createContext<Context>(null as any);
export function Tabs(props: {
children: React.ReactNode;
className?: string;
defaultValue: string;
searchParam?: string;
}) {
const { children, className, searchParam = "tab", ...other } = props;
return (
<Suspense fallback={<div>Loading...</div>}>
<TabsContentWrapper {...props} />
</Suspense>
);
}
function TabsContentWrapper(props: {
children: React.ReactNode;
className?: string;
defaultValue: string;
searchParam?: string;
}) {
const { children, className, searchParam = "tab", ...other } = props;
const searchParams = useSearchParams()!;
const selected = searchParams.get(searchParam) || props.defaultValue;
const pathname = usePathname();
const hrefFor: Context["hrefFor"] = React.useCallback(
(value) => {
const params = new URLSearchParams(searchParams);
if (value === props.defaultValue) {
params.delete(searchParam);
} else {
params.set(searchParam, value);
}
const asString = params.toString();
return pathname + (asString ? "?" + asString : "");
},
[searchParams, props.searchParam],
);
return (
<TabsContext.Provider value={{ ...other, hrefFor, searchParam, selected }}>
<div className={className}>{children}</div>
</TabsContext.Provider>
);
}
const useContext = () => {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error(
"Tabs compound components cannot be rendered outside the Tabs component",
);
}
return context;
};
export function TabsList(props: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
{...props}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
props.className,
)}
/>
);
}
export const TabsTrigger = (props: {
children: React.ReactNode;
className?: string;
value: string;
}) => {
const context = useContext();
return (
<Link
{...props}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
props.className,
)}
data-state={context.selected === props.value ? "active" : "inactive"}
href={context.hrefFor(props.value)}
scroll={false}
shallow={true}
/>
);
};
export function TabsContent(props: {
children: React.ReactNode;
className?: string;
value: string;
}) {
const context = useContext();
if (context.selected !== props.value) {
return null;
}
return (
<div
{...props}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
props.className,
)}
/>
);
}