first commit
This commit is contained in:
202
components/ui/accordion.tsx
Normal file
202
components/ui/accordion.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AccordionContextValue {
|
||||
value: string[];
|
||||
onValueChange: (value: string[]) => void;
|
||||
}
|
||||
|
||||
const AccordionContext = React.createContext<AccordionContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
interface AccordionProps {
|
||||
type?: "single" | "multiple";
|
||||
defaultValue?: string | string[];
|
||||
value?: string | string[];
|
||||
onValueChange?: (value: string | string[]) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
||||
({ type = "single", defaultValue, value, onValueChange, children, className }, ref) => {
|
||||
const [internalValue, setInternalValue] = React.useState<string[]>(
|
||||
defaultValue
|
||||
? Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [defaultValue]
|
||||
: []
|
||||
);
|
||||
|
||||
const controlledValue = value
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: [value]
|
||||
: undefined;
|
||||
|
||||
const currentValue = controlledValue ?? internalValue;
|
||||
|
||||
const handleValueChange = React.useCallback(
|
||||
(newValue: string[]) => {
|
||||
if (!controlledValue) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
if (onValueChange) {
|
||||
onValueChange(type === "single" ? newValue[0] || "" : newValue);
|
||||
}
|
||||
},
|
||||
[controlledValue, onValueChange, type]
|
||||
);
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
value: currentValue,
|
||||
onValueChange: handleValueChange,
|
||||
}),
|
||||
[currentValue, handleValueChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={contextValue}>
|
||||
<div ref={ref} className={cn("space-y-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
Accordion.displayName = "Accordion";
|
||||
|
||||
interface AccordionItemProps {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
||||
({ value, children, className }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border border-gray-200 overflow-hidden", className)}
|
||||
data-value={value}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
interface AccordionTriggerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionTriggerProps
|
||||
>(({ children, className }, ref) => {
|
||||
const context = React.useContext(AccordionContext);
|
||||
if (!context) {
|
||||
throw new Error("AccordionTrigger must be used within Accordion");
|
||||
}
|
||||
|
||||
const item = React.useContext(ItemContext);
|
||||
if (!item) {
|
||||
throw new Error("AccordionTrigger must be used within AccordionItem");
|
||||
}
|
||||
|
||||
const isOpen = context.value.includes(item.value);
|
||||
|
||||
const handleClick = () => {
|
||||
const newValue = isOpen
|
||||
? context.value.filter((v) => v !== item.value)
|
||||
: [...context.value, item.value];
|
||||
context.onValueChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between p-4 text-left font-medium text-primary transition-all hover:bg-secondary [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
data-state={isOpen ? "open" : "closed"}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-5 w-5 shrink-0 transition-transform duration-200" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
AccordionTrigger.displayName = "AccordionTrigger";
|
||||
|
||||
interface AccordionContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ItemContext = React.createContext<{ value: string } | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionContentProps
|
||||
>(({ children, className }, ref) => {
|
||||
const context = React.useContext(AccordionContext);
|
||||
if (!context) {
|
||||
throw new Error("AccordionContent must be used within Accordion");
|
||||
}
|
||||
|
||||
const item = React.useContext(ItemContext);
|
||||
if (!item) {
|
||||
throw new Error("AccordionContent must be used within AccordionItem");
|
||||
}
|
||||
|
||||
const isOpen = context.value.includes(item.value);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden transition-all",
|
||||
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className={cn("p-4 pt-0 text-gray-700", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
AccordionContent.displayName = "AccordionContent";
|
||||
|
||||
const AccordionItemWithContext = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionItemProps
|
||||
>(({ value, children, ...props }, ref) => {
|
||||
return (
|
||||
<ItemContext.Provider value={{ value }}>
|
||||
<AccordionItem ref={ref} value={value} {...props}>
|
||||
{children}
|
||||
</AccordionItem>
|
||||
</ItemContext.Provider>
|
||||
);
|
||||
});
|
||||
AccordionItemWithContext.displayName = "AccordionItem";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
AccordionItemWithContext as AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user