turash/bugulma/frontend/components/ui/Accordion.tsx

105 lines
2.7 KiB
TypeScript

import React, { useState } from 'react';
import { clsx } from 'clsx';
import { ChevronDown } from 'lucide-react';
export interface AccordionItem {
id: string;
title: string;
content: React.ReactNode;
defaultOpen?: boolean;
disabled?: boolean;
}
export interface AccordionProps {
items: AccordionItem[];
allowMultiple?: boolean;
className?: string;
itemClassName?: string;
}
/**
* Accordion/Collapsible component
*/
export const Accordion = ({
items,
allowMultiple = false,
className,
itemClassName,
}: AccordionProps) => {
const [openItems, setOpenItems] = useState<Set<string>>(
new Set(items.filter((item) => item.defaultOpen).map((item) => item.id))
);
const toggleItem = (id: string) => {
setOpenItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
if (!allowMultiple) {
newSet.clear();
}
newSet.add(id);
}
return newSet;
});
};
return (
<div className={clsx('space-y-2', className)}>
{items.map((item) => {
const isOpen = openItems.has(item.id);
return (
<div
key={item.id}
className={clsx('border rounded-lg overflow-hidden', itemClassName)}
>
<button
type="button"
onClick={() => !item.disabled && toggleItem(item.id)}
disabled={item.disabled}
className={clsx(
'w-full flex items-center justify-between p-4',
'text-left font-medium',
'hover:bg-muted/50 transition-colors',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
'opacity-50 cursor-not-allowed': item.disabled,
}
)}
aria-expanded={isOpen}
aria-controls={`accordion-content-${item.id}`}
>
<span>{item.title}</span>
<ChevronDown
className={clsx(
'h-4 w-4 text-muted-foreground transition-transform flex-shrink-0',
{
'rotate-180': isOpen,
}
)}
/>
</button>
<div
id={`accordion-content-${item.id}`}
className={clsx(
'overflow-hidden transition-all duration-200',
{
'max-h-0': !isOpen,
'max-h-[2000px]': isOpen,
}
)}
>
<div className="p-4 pt-0 text-sm text-muted-foreground">
{item.content}
</div>
</div>
</div>
);
})}
</div>
);
};