mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
chore: update Dropdown component
This commit is contained in:
@@ -1,111 +1,109 @@
|
|||||||
'use client';
|
import React, { ReactNode, useState, useRef } from 'react';
|
||||||
|
|
||||||
import { ReactNode, useRef, useEffect, useState } from 'react';
|
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
interface DropdownProps {
|
export interface DropdownProps {
|
||||||
trigger: ReactNode;
|
trigger: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
position?:
|
className?: {
|
||||||
| 'top'
|
wrapper?: string;
|
||||||
| 'bottom'
|
trigger?: string;
|
||||||
| 'left'
|
content?: string;
|
||||||
| 'right'
|
};
|
||||||
| 'top-start'
|
|
||||||
| 'top-end'
|
|
||||||
| 'bottom-start'
|
|
||||||
| 'bottom-end'
|
|
||||||
| 'left-start'
|
|
||||||
| 'left-end'
|
|
||||||
| 'right-start'
|
|
||||||
| 'right-end';
|
|
||||||
align?: 'start' | 'center' | 'end';
|
align?: 'start' | 'center' | 'end';
|
||||||
|
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
hover?: boolean;
|
hover?: boolean;
|
||||||
className?: string;
|
defaultOpen?: boolean;
|
||||||
contentClassName?: string;
|
open?: boolean;
|
||||||
|
close?: boolean;
|
||||||
|
controlled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown = ({
|
const Dropdown = ({
|
||||||
trigger,
|
trigger,
|
||||||
children,
|
children,
|
||||||
position = 'bottom',
|
|
||||||
align = 'start',
|
|
||||||
hover = false,
|
|
||||||
className,
|
className,
|
||||||
contentClassName,
|
align,
|
||||||
|
direction,
|
||||||
|
hover,
|
||||||
|
defaultOpen = false,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
controlled = false,
|
||||||
}: DropdownProps) => {
|
}: DropdownProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Handle click outside to close dropdown
|
const toggleDropdown = () => {
|
||||||
useEffect(() => {
|
if (!controlled) {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const newState = !isOpen;
|
||||||
if (
|
setIsOpen(newState);
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// Build position classes
|
|
||||||
const getPositionClasses = () => {
|
|
||||||
const classes: string[] = [];
|
|
||||||
|
|
||||||
// Handle combined positions like 'top-start'
|
|
||||||
if (position.includes('-')) {
|
|
||||||
const [pos, al] = position.split('-');
|
|
||||||
classes.push(`dropdown-${pos}`);
|
|
||||||
classes.push(`dropdown-${al}`);
|
|
||||||
} else {
|
|
||||||
classes.push(`dropdown-${position}`);
|
|
||||||
if (align !== 'start') {
|
|
||||||
classes.push(`dropdown-${align}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes.join(' ');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = (e: React.MouseEvent) => {
|
const getWrapperClasses = () => {
|
||||||
e.preventDefault();
|
const openState = controlled ? open : isOpen;
|
||||||
e.stopPropagation();
|
|
||||||
// alert('clicked');
|
return cn(
|
||||||
setIsOpen(!isOpen);
|
'dropdown',
|
||||||
|
{
|
||||||
|
'dropdown-start': align === 'start',
|
||||||
|
'dropdown-center': align === 'center',
|
||||||
|
'dropdown-end': align === 'end',
|
||||||
|
'dropdown-top': direction === 'top',
|
||||||
|
'dropdown-bottom': direction === 'bottom',
|
||||||
|
'dropdown-left': direction === 'left',
|
||||||
|
'dropdown-right': direction === 'right',
|
||||||
|
'dropdown-hover': hover,
|
||||||
|
'dropdown-open': openState && !close,
|
||||||
|
'dropdown-close': close,
|
||||||
|
},
|
||||||
|
className?.wrapper
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTriggerClasses = () => {
|
||||||
|
return cn(className?.trigger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContentClasses = () => {
|
||||||
|
return cn(
|
||||||
|
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
|
||||||
|
className?.content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controlled) {
|
||||||
|
return (
|
||||||
|
<div className={getWrapperClasses()}>
|
||||||
|
{trigger}
|
||||||
|
{open && !close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||||
ref={dropdownRef}
|
<div
|
||||||
className={cn(
|
tabIndex={0}
|
||||||
'dropdown',
|
role='button'
|
||||||
getPositionClasses(),
|
className={getTriggerClasses()}
|
||||||
hover && 'dropdown-hover',
|
onClick={toggleDropdown}
|
||||||
isOpen && 'dropdown-open',
|
onKeyDown={(e) => {
|
||||||
className
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
)}
|
e.preventDefault();
|
||||||
>
|
toggleDropdown();
|
||||||
{/* Trigger Button */}
|
}
|
||||||
<div onClick={handleToggle} className='cursor-pointer'>
|
}}
|
||||||
|
>
|
||||||
{trigger}
|
{trigger}
|
||||||
</div>
|
</div>
|
||||||
|
{!close && (
|
||||||
{/* Dropdown Content - Only render when open */}
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
{isOpen && (
|
|
||||||
<div
|
|
||||||
tabIndex={-1}
|
|
||||||
className={cn('dropdown-content z-[10]', contentClassName)}
|
|
||||||
onClick={() => setIsOpen(false)} // Close on item click
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user