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}
);
})