diff --git a/src/components/reusable/Select.tsx b/src/components/reusable/Select.tsx index 1df7961..6182029 100644 --- a/src/components/reusable/Select.tsx +++ b/src/components/reusable/Select.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback, type SelectHTMLAttributes } from 'react'; +import { useState, useRef, useEffect, useLayoutEffect, useCallback, type SelectHTMLAttributes } from 'react'; import { createPortal } from 'react-dom'; import { ChevronDown, Search, X, Check } from 'lucide-react'; import { cn } from '../../lib/cn'; @@ -63,10 +63,17 @@ export function Select({ const spaceBelow = window.innerHeight - rect.bottom; const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight; + const calculatedWidth = Math.max(rect.width, 280); + let leftPos = rect.left; + if (leftPos + calculatedWidth > window.innerWidth - 12) { + leftPos = Math.max(12, window.innerWidth - calculatedWidth - 12); + } + setDropdownStyle({ position: 'fixed', - left: `${rect.left}px`, - width: `${rect.width}px`, + left: `${leftPos}px`, + width: `${calculatedWidth}px`, + maxWidth: 'calc(100vw - 24px)', zIndex: 999999, ...(openUpwards ? { bottom: `${window.innerHeight - rect.top + 4}px` } @@ -75,7 +82,7 @@ export function Select({ } }, []); - useEffect(() => { + useLayoutEffect(() => { if (isOpen) { updatePosition(); const handleScrollOrResize = () => updatePosition(); @@ -85,6 +92,8 @@ export function Select({ window.removeEventListener('resize', handleScrollOrResize); window.removeEventListener('scroll', handleScrollOrResize, true); }; + } else { + setDropdownStyle({}); } }, [isOpen, updatePosition]); @@ -102,7 +111,26 @@ export function Select({ return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const handleSelect = (val: string) => { + const handleOpenToggle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (!disabled) { + const next = !isOpen; + if (next) { + updatePosition(); + onDropdownOpen?.(); + } else { + setDropdownStyle({}); + } + setIsOpen(next); + } + }; + + const handleSelect = (val: string, e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } if (rest.onChange) { const syntheticEvent = { target: { value: val, name: rest.name, id: rest.id }, @@ -125,13 +153,7 @@ export function Select({ {/* Trigger Box */}
{ - if (!disabled) { - const next = !isOpen; - setIsOpen(next); - if (next) onDropdownOpen?.(); - } - }} + onClick={handleOpenToggle} className={cn( "relative bg-card rounded-md h-[42px] border px-3 flex items-center justify-between cursor-pointer transition-all duration-150 select-none", disabled && "opacity-60 cursor-not-allowed bg-slate-50", @@ -145,7 +167,7 @@ export function Select({
{/* Portaled Dropdown Menu Overlay */} - {isOpen && !disabled && createPortal( + {isOpen && !disabled && Boolean(dropdownStyle.position) && createPortal(
setSearchQuery('')} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setSearchQuery(''); + }} className="text-muted hover:text-strong p-0.5 rounded cursor-pointer" > @@ -182,13 +208,14 @@ export function Select({ return (
handleSelect(opt.value)} + onClick={(e) => handleSelect(opt.value, e)} + title={opt.label} className={cn( "px-3 py-2 text-sm flex items-center justify-between cursor-pointer transition-colors", isSelected ? "bg-blue-50/50 text-blue-700 font-semibold" : "hover:bg-black/5 text-strong" )} > - {opt.label} + {opt.label} {isSelected && }
); @@ -271,13 +298,21 @@ export function MultiSelect({ const currentValues = Array.isArray(value) ? value : []; - const handleOpenToggle = () => { + const handleOpenToggle = (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } if (!disabled) { - if (!isOpen) { + const next = !isOpen; + if (next) { setDraftValues(currentValues); setSearchQuery(''); + updatePosition(); + } else { + setDropdownStyle({}); } - setIsOpen(!isOpen); + setIsOpen(next); } }; @@ -292,10 +327,17 @@ export function MultiSelect({ const spaceBelow = window.innerHeight - rect.bottom; const openUpwards = spaceBelow < dropdownHeight && rect.top > dropdownHeight; + const calculatedWidth = Math.max(rect.width, 280); + let leftPos = rect.left; + if (leftPos + calculatedWidth > window.innerWidth - 12) { + leftPos = Math.max(12, window.innerWidth - calculatedWidth - 12); + } + setDropdownStyle({ position: 'fixed', - left: `${rect.left}px`, - width: `${rect.width}px`, + left: `${leftPos}px`, + width: `${calculatedWidth}px`, + maxWidth: 'calc(100vw - 24px)', zIndex: 999999, ...(openUpwards ? { bottom: `${window.innerHeight - rect.top + 4}px` } @@ -304,7 +346,7 @@ export function MultiSelect({ } }, []); - useEffect(() => { + useLayoutEffect(() => { if (isOpen) { updatePosition(); const handleScrollOrResize = () => updatePosition(); @@ -314,6 +356,8 @@ export function MultiSelect({ window.removeEventListener('resize', handleScrollOrResize); window.removeEventListener('scroll', handleScrollOrResize, true); }; + } else { + setDropdownStyle({}); } }, [isOpen, updatePosition]); @@ -331,18 +375,30 @@ export function MultiSelect({ return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const toggleOption = (val: string) => { + const toggleOption = (val: string, e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } setDraftValues((prev) => prev.includes(val) ? prev.filter((v) => v !== val) : [...prev, val] ); }; - const handleApply = () => { + const handleApply = (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } onChange?.(draftValues); setIsOpen(false); }; - const handleClear = () => { + const handleClear = (e?: React.MouseEvent) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } setDraftValues([]); }; @@ -382,7 +438,7 @@ export function MultiSelect({
{/* Portaled Dropdown Menu Overlay */} - {isOpen && !disabled && createPortal( + {isOpen && !disabled && Boolean(dropdownStyle.position) && createPortal(
setSearchQuery('')} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setSearchQuery(''); + }} className="text-muted hover:text-strong p-0.5 rounded cursor-pointer" > @@ -419,9 +479,10 @@ export function MultiSelect({ return (
toggleOption(opt.value)} + onClick={(e) => toggleOption(opt.value, e)} + title={opt.label} className={cn( - "px-3 py-2 text-xs flex items-center gap-2.5 cursor-pointer transition-colors select-none", + "px-3 py-2 text-xs flex items-start gap-2.5 cursor-pointer transition-colors select-none", isSelected ? "bg-blue-50/70 text-blue-800 font-semibold" : "hover:bg-black/5 text-strong" )} > @@ -429,9 +490,9 @@ export function MultiSelect({ type="checkbox" checked={isSelected} onChange={() => {}} - className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 pointer-events-none h-3.5 w-3.5" + className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 pointer-events-none h-3.5 w-3.5 mt-0.5 shrink-0" /> - {opt.label} + {opt.label}
); })