From df2a40b861244be7d3be5658eb7e4167188960b5 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Wed, 25 Jun 2025 15:40:20 +0530 Subject: [PATCH] feat(assignee-selector): enhance dropdown functionality and position handling - Added button reference and dropdown position state to improve dropdown positioning. - Implemented useCallback for updating dropdown position on scroll and resize events. - Enhanced click outside handling to close the dropdown correctly. - Utilized createPortal for rendering the dropdown, ensuring it overlays correctly in the DOM. - Improved styling and behavior of the dropdown button based on its open state. --- .../src/components/AssigneeSelector.tsx | 89 +++++++++++++++---- .../components/task-management/task-row.tsx | 37 ++++---- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index 02ac057c..a2174678 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -1,4 +1,5 @@ -import React, { useState, useRef, useEffect, useMemo } from 'react'; +import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import { useSelector } from 'react-redux'; import { PlusOutlined, UserAddOutlined } from '@ant-design/icons'; import { RootState } from '@/app/store'; @@ -24,7 +25,9 @@ const AssigneeSelector: React.FC = ({ const [isOpen, setIsOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [teamMembers, setTeamMembers] = useState({ data: [], total: 0 }); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); const dropdownRef = useRef(null); + const buttonRef = useRef(null); const searchInputRef = useRef(null); const { projectId } = useSelector((state: RootState) => state.projectReducer); @@ -38,20 +41,61 @@ const AssigneeSelector: React.FC = ({ ); }, [teamMembers, searchQuery]); - // Close dropdown when clicking outside + // Update dropdown position + const updateDropdownPosition = useCallback(() => { + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY + 2, + left: rect.left + window.scrollX, + }); + } + }, []); + + // Close dropdown when clicking outside and handle scroll useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && + buttonRef.current && !buttonRef.current.contains(event.target as Node)) { setIsOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); + const handleScroll = () => { + if (isOpen) { + updateDropdownPosition(); + } + }; - const handleDropdownToggle = () => { + const handleResize = () => { + if (isOpen) { + updateDropdownPosition(); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); + window.addEventListener('resize', handleResize); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); + window.removeEventListener('resize', handleResize); + }; + } else { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [isOpen, updateDropdownPosition]); + + const handleDropdownToggle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (!isOpen) { + updateDropdownPosition(); + // Prepare team members data when opening const assignees = task?.assignees?.map(assignee => assignee.team_member_id); const membersData = (members?.data || []).map(member => ({ @@ -61,12 +105,14 @@ const AssigneeSelector: React.FC = ({ const sortedMembers = sortTeamMembers(membersData); setTeamMembers({ data: sortedMembers }); + setIsOpen(true); // Focus search input after opening setTimeout(() => { searchInputRef.current?.focus(); }, 0); + } else { + setIsOpen(false); } - setIsOpen(!isOpen); }; const handleMemberToggle = (memberId: string, checked: boolean) => { @@ -91,30 +137,40 @@ const AssigneeSelector: React.FC = ({ }; return ( -
+ <> - {isOpen && ( + {isOpen && createPortal(
{/* Header */}
@@ -211,9 +267,10 @@ const AssigneeSelector: React.FC = ({ Invite member
-
+
, + document.body )} - + ); }; diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index ec4cdfdb..6420b0ae 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react' import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useSelector } from 'react-redux'; +import DOMPurify from 'dompurify'; import { Input, Typography, @@ -86,18 +87,24 @@ const TaskKey = React.memo<{ taskKey: string; isDarkMode: boolean }>(({ taskKey, )); -const TaskDescription = React.memo<{ description?: string; isDarkMode: boolean }>(({ description, isDarkMode }) => ( - - {description || ''} - -)); +const TaskDescription = React.memo<{ description?: string; isDarkMode: boolean }>(({ description, isDarkMode }) => { + if (!description) return null; + + const sanitizedDescription = DOMPurify.sanitize(description); + + return ( + + + + ); +}); const TaskProgress = React.memo<{ progress: number; isDarkMode: boolean }>(({ progress, isDarkMode }) => ( = React.memo(({ case 'members': return ( -
-
+
+
{task.assignee_names && task.assignee_names.length > 0 && ( = React.memo(({ {/* Fixed Columns */} {fixedColumns && fixedColumns.length > 0 && (
sum + col.width, 0), }}