From 6df4302396ab3a206d20cc4123ac4defe4daa73f Mon Sep 17 00:00:00 2001 From: RKWojs Date: Sat, 21 Jun 2025 00:08:33 +0200 Subject: [PATCH] feat: enhance TaskStatusDropdownSimple with portal-based dropdown and position calculation --- src/components/ProjectTasksList.js | 6 +- src/components/TaskStatusDropdownSimple.js | 166 +++++++++++++++------ 2 files changed, 121 insertions(+), 51 deletions(-) diff --git a/src/components/ProjectTasksList.js b/src/components/ProjectTasksList.js index ee3e470..d9fa6b3 100644 --- a/src/components/ProjectTasksList.js +++ b/src/components/ProjectTasksList.js @@ -313,8 +313,7 @@ export default function ProjectTasksList() { {task.max_wait_days} days - - + - ); - const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => { + ); const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => { const filteredTasks = filterTasks(tasks); const groupedTasks = groupTasksByName(filteredTasks); const colSpan = showTimeLeft ? "8" : "7"; diff --git a/src/components/TaskStatusDropdownSimple.js b/src/components/TaskStatusDropdownSimple.js index 9021c4c..aba5258 100644 --- a/src/components/TaskStatusDropdownSimple.js +++ b/src/components/TaskStatusDropdownSimple.js @@ -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 ( @@ -81,16 +162,38 @@ export default function TaskStatusDropdown({ ); } - return ( -
- + ))} +
, + document.body + ); + }; return ( + <> + - {/* Simple dropdown for debugging */} - {isOpen && ( -
-
- DEBUG: TaskStatus Dropdown is visible -
- {Object.entries(statusConfig).map(([statusKey, config]) => ( - - ))} -
- )} - - {/* Backdrop */} - {isOpen && ( -
{ - console.log("TaskStatus Backdrop clicked"); - setIsOpen(false); - }} - /> - )} -
+ {/* Portal-based dropdown */} + + ); }