mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 06:45:46 +00:00
feat: add Collapse component
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import React, { useCallback, useId, useMemo, useState } from 'react';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export type CollapseVariant = 'default' | 'arrow' | 'plus';
|
||||
|
||||
export type CollapseProps = {
|
||||
/** Unique name used when `asRadio` is true (Accordion single-open). */
|
||||
name?: string;
|
||||
/** If provided, component is controlled. */
|
||||
open?: boolean;
|
||||
/** Initial open state for uncontrolled usage. */
|
||||
defaultOpen?: boolean;
|
||||
/** Callback when open state changes. */
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/** Title row content. Accepts string or custom node. */
|
||||
title?: React.ReactNode;
|
||||
/** Optional secondary text displayed under/next to title. */
|
||||
subtitle?: React.ReactNode;
|
||||
/** Content of the panel. */
|
||||
children?: React.ReactNode;
|
||||
/** Visual variant: default / arrow / plus */
|
||||
variant?: CollapseVariant;
|
||||
/** Add a bordered look */
|
||||
bordered?: boolean;
|
||||
/** Disable interactions */
|
||||
disabled?: boolean;
|
||||
/** Allow only one open at a time by switching to radio input */
|
||||
asRadio?: boolean;
|
||||
/** Extra classnames */
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
export const Collapse = ({
|
||||
name,
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
variant = 'default',
|
||||
bordered,
|
||||
disabled,
|
||||
asRadio = false,
|
||||
className,
|
||||
titleClassName,
|
||||
contentClassName,
|
||||
}: CollapseProps) => {
|
||||
const inputId = useId();
|
||||
const isControlled = typeof open === 'boolean';
|
||||
const [internalOpen, setInternalOpen] = useState(!!defaultOpen);
|
||||
const isOpen = isControlled ? !!open : internalOpen;
|
||||
|
||||
// Manage change from checkbox/radio
|
||||
const handleChange = useCallback(
|
||||
(next: boolean) => {
|
||||
if (!isControlled) setInternalOpen(next);
|
||||
onOpenChange?.(next);
|
||||
},
|
||||
[isControlled, onOpenChange]
|
||||
);
|
||||
|
||||
const inputType = asRadio ? 'radio' : 'checkbox';
|
||||
|
||||
const rootClass = cn(
|
||||
'collapse',
|
||||
variant === 'arrow' && 'collapse-arrow',
|
||||
variant === 'plus' && 'collapse-plus',
|
||||
bordered && 'border base-content/20 border-opacity-20 rounded-box',
|
||||
disabled && 'opacity-60 pointer-events-none',
|
||||
className
|
||||
);
|
||||
|
||||
const titleNode = useMemo(() => {
|
||||
if (subtitle) {
|
||||
return (
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span>{title}</span>
|
||||
<span className='text-sm opacity-70'>{subtitle}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div>{title}</div>;
|
||||
}, [title, subtitle]);
|
||||
|
||||
return (
|
||||
<div className={rootClass} data-open={isOpen}>
|
||||
<input
|
||||
id={inputId}
|
||||
type={inputType}
|
||||
name={asRadio ? name : undefined}
|
||||
className='peer p-0 hidden'
|
||||
checked={isControlled ? isOpen : undefined}
|
||||
defaultChecked={!isControlled ? isOpen : undefined}
|
||||
onChange={(e) => handleChange(e.currentTarget.checked)}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={`${inputId}-content`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'collapse-title p-0',
|
||||
'focus:outline-none focus-visible:ring focus-visible:ring-primary/40',
|
||||
titleClassName
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleChange(!isOpen);
|
||||
}
|
||||
}}
|
||||
onClick={() => handleChange(!isOpen)}
|
||||
>
|
||||
{titleNode}
|
||||
</div>
|
||||
|
||||
<div
|
||||
id={`${inputId}-content`}
|
||||
className={cn('collapse-content p-0!', contentClassName)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Collapse;
|
||||
Reference in New Issue
Block a user