mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
137 lines
3.3 KiB
TypeScript
137 lines
3.3 KiB
TypeScript
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react';
|
|
import { cn } from '@/lib/helper';
|
|
|
|
export interface TabItem {
|
|
id: string;
|
|
label: ReactNode;
|
|
content?: ReactNode;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export interface TabsProps
|
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
|
tabs: TabItem[];
|
|
variant?: 'bordered' | 'lifted' | 'boxed';
|
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
placement?: 'top' | 'bottom';
|
|
/** Tab yang aktif secara default (uncontrolled mode) */
|
|
defaultActiveId?: string;
|
|
/** Tab yang aktif (controlled mode, dikontrol parent) */
|
|
activeTabId?: string;
|
|
className?:
|
|
| string
|
|
| {
|
|
container?: string;
|
|
wrapper?: string;
|
|
tab?: string;
|
|
content?: string;
|
|
};
|
|
onTabChange?: (tabId: string) => void;
|
|
}
|
|
|
|
const Tabs = ({
|
|
tabs,
|
|
variant,
|
|
size = 'md',
|
|
placement = 'top',
|
|
defaultActiveId,
|
|
activeTabId: controlledActiveId,
|
|
className,
|
|
onTabChange,
|
|
...props
|
|
}: TabsProps) => {
|
|
// State internal hanya dipakai kalau `activeTabId` (controlled) tidak diset
|
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(
|
|
defaultActiveId || tabs[0]?.id || ''
|
|
);
|
|
|
|
const isControlled = controlledActiveId !== undefined;
|
|
const activeTabId = isControlled ? controlledActiveId : uncontrolledActiveId;
|
|
|
|
const handleTabChange = (tabId: string) => {
|
|
if (tabId === activeTabId) return;
|
|
if (!isControlled) setUncontrolledActiveId(tabId);
|
|
onTabChange?.(tabId);
|
|
};
|
|
|
|
const {
|
|
container: containerClassName,
|
|
wrapper: wrapperClassName,
|
|
tab: tabClassName,
|
|
content: contentClassName,
|
|
} = typeof className === 'object'
|
|
? className
|
|
: { wrapper: className, tab: undefined };
|
|
|
|
const getTabsClasses = () => {
|
|
const variantClasses: Record<string, string> = {
|
|
bordered: 'tabs-bordered',
|
|
lifted: 'tabs-lift',
|
|
boxed: 'tabs-box',
|
|
};
|
|
|
|
const sizeClasses: Record<string, string> = {
|
|
xs: 'tabs-xs',
|
|
sm: 'tabs-sm',
|
|
md: '',
|
|
lg: 'tabs-lg',
|
|
xl: 'tabs-xl',
|
|
};
|
|
|
|
const placementClasses: Record<string, string> = {
|
|
top: '',
|
|
bottom: 'tabs-bottom',
|
|
};
|
|
|
|
return cn(
|
|
'tabs',
|
|
variant && variantClasses[variant],
|
|
sizeClasses[size],
|
|
placementClasses[placement],
|
|
wrapperClassName
|
|
);
|
|
};
|
|
|
|
const getTabClasses = (isActive: boolean, isDisabled?: boolean) =>
|
|
cn(
|
|
'tab',
|
|
{
|
|
'tab-active': isActive,
|
|
'tab-disabled': isDisabled,
|
|
},
|
|
tabClassName
|
|
);
|
|
|
|
const activeContent = tabs.find((tab) => tab.id === activeTabId)?.content;
|
|
|
|
return (
|
|
<div
|
|
{...props}
|
|
className={cn(
|
|
'w-full',
|
|
typeof className === 'string' ? className : containerClassName
|
|
)}
|
|
>
|
|
<div role='tablist' className={getTabsClasses()}>
|
|
{tabs.map(({ id, label, disabled }) => (
|
|
<button
|
|
key={id}
|
|
role='tab'
|
|
className={getTabClasses(id === activeTabId, disabled)}
|
|
onClick={() => !disabled && handleTabChange(id)}
|
|
disabled={disabled}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{activeContent && (
|
|
<div className={cn('mt-4', contentClassName)}>{activeContent}</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Tabs;
|