feat: enhance TaskStatusDropdownSimple with portal-based dropdown and position calculation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import Badge from "@/components/ui/Badge";
|
||||
|
||||
export default function TaskStatusDropdown({
|
||||
@@ -8,10 +9,17 @@ export default function TaskStatusDropdown({
|
||||
size = "sm",
|
||||
showDropdown = true,
|
||||
onStatusChange,
|
||||
}) {
|
||||
const [status, setStatus] = useState(task.status);
|
||||
}) { const [status, setStatus] = useState(task.status);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0, position: 'bottom' });
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const buttonRef = useRef(null);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const statusConfig = {
|
||||
pending: {
|
||||
@@ -65,14 +73,87 @@ export default function TaskStatusDropdown({
|
||||
alert("Error updating task status");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
} };
|
||||
// Calculate dropdown position to avoid clipping
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!buttonRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const dropdownHeight = 180; // Estimated dropdown height
|
||||
const dropdownWidth = 140; // Estimated dropdown width
|
||||
|
||||
// Check if there's enough space below
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom;
|
||||
const spaceAbove = buttonRect.top;
|
||||
|
||||
// Determine vertical position
|
||||
let y, position;
|
||||
if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
|
||||
// Show above
|
||||
y = buttonRect.top - dropdownHeight - 4;
|
||||
position = 'top';
|
||||
} else {
|
||||
// Show below
|
||||
y = buttonRect.bottom + 4;
|
||||
position = 'bottom';
|
||||
}
|
||||
|
||||
// Determine horizontal position (align right edge of dropdown with right edge of button)
|
||||
let x = buttonRect.right - dropdownWidth;
|
||||
|
||||
// Ensure dropdown doesn't go off screen
|
||||
if (x < 8) {
|
||||
x = 8; // Minimum margin from left edge
|
||||
} else if (x + dropdownWidth > viewportWidth - 8) {
|
||||
x = viewportWidth - dropdownWidth - 8; // Ensure it fits within viewport
|
||||
}
|
||||
|
||||
setDropdownPosition({ x, y, position });
|
||||
}; const handleToggle = () => {
|
||||
if (!isOpen) {
|
||||
calculateDropdownPosition();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(event.target) &&
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isOpen) {
|
||||
calculateDropdownPosition();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('resize', handleScroll);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('resize', handleScroll);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const currentConfig = statusConfig[status] || {
|
||||
label: "Unknown",
|
||||
variant: "default",
|
||||
};
|
||||
|
||||
if (!showDropdown) {
|
||||
return (
|
||||
<Badge variant={currentConfig.variant} size={size}>
|
||||
@@ -81,16 +162,38 @@ export default function TaskStatusDropdown({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(
|
||||
"TaskStatusDropdown button clicked, current isOpen:",
|
||||
isOpen
|
||||
);
|
||||
setIsOpen(!isOpen);
|
||||
// Portal dropdown component
|
||||
const DropdownPortal = () => {
|
||||
if (!mounted || !isOpen) return null;
|
||||
return createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed bg-white border border-gray-200 rounded-md shadow-lg z-[9999] min-w-[140px]"
|
||||
style={{
|
||||
left: `${dropdownPosition.x}px`,
|
||||
top: `${dropdownPosition.y}px`,
|
||||
}}
|
||||
> {Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||
<button
|
||||
key={statusKey}
|
||||
onClick={() => {
|
||||
handleChange(statusKey);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<Badge variant={config.variant} size="sm">
|
||||
{config.label}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}; return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleToggle}
|
||||
disabled={loading}
|
||||
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 rounded-md"
|
||||
>
|
||||
@@ -120,39 +223,8 @@ export default function TaskStatusDropdown({
|
||||
</Badge>
|
||||
</button>
|
||||
|
||||
{/* Simple dropdown for debugging */}
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border-2 border-red-500 rounded-md shadow-lg z-[9999] min-w-[120px]">
|
||||
<div className="bg-yellow-100 p-2 text-xs text-center border-b">
|
||||
DEBUG: TaskStatus Dropdown is visible
|
||||
</div>
|
||||
{Object.entries(statusConfig).map(([statusKey, config]) => (
|
||||
<button
|
||||
key={statusKey}
|
||||
onClick={() => {
|
||||
console.log("TaskStatus Option clicked:", statusKey);
|
||||
handleChange(statusKey);
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 hover:bg-gray-50 transition-colors first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<Badge variant={config.variant} size="sm">
|
||||
{config.label}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[9998] bg-black bg-opacity-10"
|
||||
onClick={() => {
|
||||
console.log("TaskStatus Backdrop clicked");
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Portal-based dropdown */}
|
||||
<DropdownPortal />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user