tududi/frontend/components/Shared/ProjectDropdown.tsx
Antonis Anastasiadis 4878c71618 Lint & format (#159)
Co-authored-by: antanst <>
2025-07-15 10:44:02 +03:00

212 lines
9.7 KiB
TypeScript

import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Project } from '../../entities/Project';
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
interface ProjectDropdownProps {
projectName: string;
onProjectSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
dropdownOpen: boolean;
filteredProjects: Project[];
onProjectSelection: (project: Project) => void;
onCreateProject: () => void;
isCreatingProject: boolean;
onShowAllProjects: () => void;
allProjects: Project[];
placeholder?: string;
disabled?: boolean;
}
const ProjectDropdown: React.FC<ProjectDropdownProps> = ({
projectName,
onProjectSearch,
dropdownOpen,
filteredProjects,
onProjectSelection,
onCreateProject,
isCreatingProject,
onShowAllProjects,
allProjects,
placeholder,
disabled = false,
}) => {
const { t } = useTranslation();
const dropdownRef = useRef<HTMLDivElement>(null);
// Scroll to dropdown when it opens, keeping input visible
useEffect(() => {
if (dropdownOpen && dropdownRef.current) {
// Small delay to ensure the dropdown is rendered
setTimeout(() => {
const dropdownElement = dropdownRef.current;
if (dropdownElement) {
// Find the input field to keep it visible
const inputElement =
dropdownElement.parentElement?.querySelector('input');
// Find the appropriate scroll container (modal or window)
const modalScrollContainer = dropdownElement.closest(
'.absolute.inset-0.overflow-y-auto'
);
if (modalScrollContainer && inputElement) {
// We're inside a modal - scroll the modal container
const containerRect =
modalScrollContainer.getBoundingClientRect();
const dropdownRect =
dropdownElement.getBoundingClientRect();
const inputRect = inputElement.getBoundingClientRect();
// Only scroll if dropdown extends below the visible area
if (dropdownRect.bottom > containerRect.bottom - 50) {
// Calculate very conservative scroll - prioritize keeping input visible
const inputDistanceFromTop =
inputRect.top - containerRect.top;
const minInputVisible = 60; // Keep at least 60px of input visible
// Only scroll if we can maintain input visibility
if (inputDistanceFromTop > minInputVisible) {
const maxAllowedScroll =
inputDistanceFromTop - minInputVisible;
const neededScroll =
dropdownRect.bottom -
containerRect.bottom +
30;
const scrollAmount = Math.min(
maxAllowedScroll,
neededScroll
);
if (scrollAmount > 0) {
modalScrollContainer.scrollBy({
top: scrollAmount,
behavior: 'smooth',
});
}
}
}
} else if (inputElement) {
// We're not in a modal - scroll the window
const dropdownRect =
dropdownElement.getBoundingClientRect();
const inputRect = inputElement.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Only scroll if dropdown extends below the viewport
if (dropdownRect.bottom > viewportHeight - 50) {
// Calculate very conservative scroll - prioritize keeping input visible
const inputDistanceFromTop = inputRect.top;
const minInputVisible = 60; // Keep at least 60px of input visible
// Only scroll if we can maintain input visibility
if (inputDistanceFromTop > minInputVisible) {
const maxAllowedScroll =
inputDistanceFromTop - minInputVisible;
const neededScroll =
dropdownRect.bottom - viewportHeight + 30;
const scrollAmount = Math.min(
maxAllowedScroll,
neededScroll
);
if (scrollAmount > 0) {
window.scrollBy({
top: scrollAmount,
behavior: 'smooth',
});
}
}
}
}
}
}, 200);
}
}, [dropdownOpen]);
return (
<div className="relative">
<div className="relative">
<input
type="text"
placeholder={
placeholder ||
t(
'forms.task.projectSearchPlaceholder',
'Search or create a project...'
)
}
value={projectName}
onChange={onProjectSearch}
disabled={disabled}
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm px-3 py-2 pr-10 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 disabled:bg-gray-100 disabled:dark:bg-gray-800 disabled:cursor-not-allowed"
autoComplete="off"
/>
<button
type="button"
onClick={onShowAllProjects}
disabled={disabled}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
aria-label={
dropdownOpen ? 'Hide projects' : 'Show all projects'
}
>
{dropdownOpen ? (
<ChevronUpIcon className="h-5 w-5" />
) : (
<ChevronDownIcon className="h-5 w-5" />
)}
</button>
</div>
{dropdownOpen && (
<div
ref={dropdownRef}
className="absolute mt-1 bg-white dark:bg-gray-800 shadow-lg rounded-md w-full z-50 border border-gray-200 dark:border-gray-700"
>
{(() => {
// Show filtered projects if user is typing, otherwise show all projects
const projectsToShow = projectName
? filteredProjects
: allProjects;
return projectsToShow.length > 0 ? (
projectsToShow.map((project) => (
<button
key={project.id}
type="button"
onClick={() => onProjectSelection(project)}
className="block w-full text-gray-700 dark:text-gray-300 text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
{project.name}
</button>
))
) : (
<div className="px-4 py-2 text-gray-500 dark:text-gray-400">
{projectName
? t(
'forms.task.noMatchingProjects',
'No matching projects'
)
: 'No projects available'}
</div>
);
})()}
{projectName && (
<button
type="button"
onClick={onCreateProject}
disabled={isCreatingProject}
className="block w-full text-left px-4 py-2 bg-blue-500 text-white hover:bg-blue-600 transition-colors disabled:bg-blue-400 disabled:cursor-not-allowed"
>
{isCreatingProject
? t('forms.task.creatingProject', 'Creating...')
: t('forms.task.createProject', '+ Create') +
` "${projectName}"`}
</button>
)}
</div>
)}
</div>
);
};
export default ProjectDropdown;