diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx index c52ef32847..0976dd02e1 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChart.tsx @@ -12,6 +12,8 @@ import { eachDayOfInterval, isMonday, differenceInDays } from 'date-fns'; import { getMonday } from '../../../utils/datetime.utils'; import { toDateString } from 'shared'; import { GANTT_CHART_CELL_SIZE, GANTT_CHART_GAP_SIZE } from '../../../utils/gantt.utils'; +import { useRef, useCallback, useEffect } from 'react'; + export interface GanttEditability { highlightTaskComparator: HighlightTaskComparator; highlightSubtaskComparator: HighlightTaskComparator; @@ -38,38 +40,103 @@ const GanttChart = ({ startDate, endDate, collections, editability }: Gant const today = new Date(new Date().setHours(0, 0, 0, 0)); const currentWeekCol = days.findIndex((day) => toDateString(day) === toDateString(getMonday(today))) + 1; - const daysIntoWeek = differenceInDays(today, getMonday(today)); const dailyOffset = daysIntoWeek * (parseFloat(GANTT_CHART_CELL_SIZE) / 7); + const scrollContainerRef = useRef(null); + + const validCollections = collections.filter((c) => c.tasks); + + const sectionDataRef = useRef< + { sectionEl: HTMLDivElement | null; placeholderEl: HTMLDivElement | null; height: number }[] + >([]); + + if (sectionDataRef.current.length !== validCollections.length) { + sectionDataRef.current = validCollections.map((_, i) => ({ + sectionEl: sectionDataRef.current[i]?.sectionEl ?? null, + placeholderEl: sectionDataRef.current[i]?.placeholderEl ?? null, + height: sectionDataRef.current[i]?.height ?? 0 + })); + } + + const updateVisibility = useCallback(() => { + const container = scrollContainerRef.current; + if (!container) return; + + const containerRect = container.getBoundingClientRect(); + const viewportBottom = containerRect.bottom; + + for (const data of sectionDataRef.current) { + const { sectionEl, placeholderEl } = data; + if (!sectionEl || !placeholderEl) continue; + + const el = sectionEl.style.display === 'none' ? placeholderEl : sectionEl; + const elTop = el.getBoundingClientRect().top; + const measuredHeight = sectionEl.offsetHeight || placeholderEl.offsetHeight; + + const isVisible = elTop <= viewportBottom; + + sectionEl.style.display = isVisible ? '' : 'none'; + placeholderEl.style.display = isVisible ? 'none' : ''; + placeholderEl.style.height = `${measuredHeight}px`; + } + }, []); + + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + container.addEventListener('scroll', updateVisibility, { passive: true }); + // Also re-check on container resize + const ro = new ResizeObserver(updateVisibility); + ro.observe(container); + updateVisibility(); // initial pass + + return () => { + container.removeEventListener('scroll', updateVisibility); + ro.disconnect(); + }; + }, [updateVisibility]); + return ( - {collections.map((collection) => { - return collection.tasks ? ( - ( + + {/* Real */} + { + if (sectionDataRef.current[idx]) sectionDataRef.current[idx].sectionEl = el; + }} + > + + + {/* Placeholder */} + { + if (sectionDataRef.current[idx]) sectionDataRef.current[idx].placeholderEl = el; + }} + style={{ display: 'none', height: 0 }} /> - ) : ( - <> - ); - })} + + ))} {currentWeekCol > 0 && ( { @@ -10,16 +10,19 @@ interface GanttChartCollectionSectionProps { endDate: Date; collection: GanttCollection; editability?: GanttEditability; + onHeightChange?: (height: number) => void; } const GanttChartCollectionSection = ({ startDate, endDate, collection, - editability + editability, + onHeightChange }: GanttChartCollectionSectionProps) => { const theme = useTheme(); const [isEditMode, setIsEditMode] = useState(false); + const sectionRef = useRef(null); const collectionSectionBackgroundStyle = { mt: 1, @@ -61,8 +64,20 @@ const GanttChartCollectionSection = ({ const ignoreBool = () => false; + useEffect(() => { + const el = sectionRef.current; + if (!el || !onHeightChange) return; + + const ro = new ResizeObserver(() => { + onHeightChange(el.getBoundingClientRect().height); + }); + ro.observe(el); + onHeightChange(el.getBoundingClientRect().height); + return () => ro.disconnect(); + }, [onHeightChange]); + return ( - + {collection.title} diff --git a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx index 05cba3506e..52cfa5f9ac 100644 --- a/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx +++ b/src/frontend/src/pages/GanttPage/GanttChart/GanttChartComponents/GanttTaskBar/GanttTaskBarView.tsx @@ -4,10 +4,10 @@ import { HighlightTaskComparator, OnMouseOverOptions } from '../../../../../utils/gantt.utils'; -import { Collapse } from '@mui/material'; +import { Box } from '@mui/material'; import GanttTaskBar from './GanttTaskBar'; import GanttTaskBarDisplay from './GanttTaskBarDisplay'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; interface GanttTaskBarViewProps { days: Date[]; @@ -37,11 +37,19 @@ const GanttTaskBarView = ({ onToggle }: GanttTaskBarViewProps) => { const [showChildren, setShowChildren] = useState(false); + const animationRef = useRef(null); const handleToggle = () => { setShowChildren((prev) => !prev); }; + // Fire onToggle after the grid animation completes (200ms matches transition below) + useEffect(() => { + if (animationRef.current) cancelAnimationFrame(animationRef.current); + const timeout = setTimeout(() => onToggle?.(), 200); + return () => clearTimeout(timeout); + }, [showChildren, onToggle]); + return ( <> ({ highlightSubtaskComparator={highlightSubtaskComparator} highlightTaskComparator={highlightTaskComparator} /> - - - {task.children.map((child) => ( - {}} - handleOnMouseOver={handleOnMouseOver} - handleOnMouseLeave={handleOnMouseLeave} - highlightedChange={highlightedChange} - onAddTaskPressed={onAddTaskPressed} - highlightSubtaskComparator={highlightSubtaskComparator} - highlightTaskComparator={highlightTaskComparator} - onToggle={onToggle} - /> - ))} - + + + {task.children.map((child) => ( + {}} + handleOnMouseOver={handleOnMouseOver} + handleOnMouseLeave={handleOnMouseLeave} + highlightedChange={highlightedChange} + onAddTaskPressed={onAddTaskPressed} + highlightSubtaskComparator={highlightSubtaskComparator} + highlightTaskComparator={highlightTaskComparator} + onToggle={onToggle} + /> + ))} + + ); };