From 37d8eedd641bb04c086797010292bcb25240d56d Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 6 Nov 2025 23:36:52 +0100 Subject: Refactor styles and add alert color variables; implement scroll management for timetable --- src/frontend/app/routes/timetable-$id.tsx | 235 +++++++++++++++++------------- 1 file changed, 131 insertions(+), 104 deletions(-) (limited to 'src/frontend/app/routes/timetable-$id.tsx') diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx index c8f0125..df77372 100644 --- a/src/frontend/app/routes/timetable-$id.tsx +++ b/src/frontend/app/routes/timetable-$id.tsx @@ -153,9 +153,6 @@ export default function Timetable() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showPastEntries, setShowPastEntries] = useState(false); - const [showScrollTop, setShowScrollTop] = useState(false); - const [showScrollBottom, setShowScrollBottom] = useState(false); - const [showGoToNow, setShowGoToNow] = useState(false); const nextEntryRef = useRef(null); const containerRef = useRef(null); const regionConfig = getRegionConfig(region); @@ -242,73 +239,7 @@ export default function Timetable() { setCustomName(StopDataProvider.getCustomName(region, stopIdNum)); }, [params.id, region]); - // Handle scroll events to update FAB visibility - useEffect(() => { - const handleScroll = () => { - if ( - !containerRef.current || - loading || - error || - timetableData.length === 0 - ) { - return; - } - - const container = containerRef.current; - const scrollTop = container.scrollTop; - const scrollHeight = container.scrollHeight; - const clientHeight = container.clientHeight; - const scrollBottom = scrollHeight - scrollTop - clientHeight; - - // Threshold for showing scroll buttons (in pixels) - const threshold = 100; - - // Show scroll top button when scrolled down - setShowScrollTop(scrollTop > threshold); - - // Show scroll bottom button when not at bottom - setShowScrollBottom(scrollBottom > threshold); - - // Check if next entry (current time) is visible - if (nextEntryRef.current) { - const rect = nextEntryRef.current.getBoundingClientRect(); - const containerRect = container.getBoundingClientRect(); - const isNextVisible = - rect.top >= containerRect.top && rect.bottom <= containerRect.bottom; - - setShowGoToNow(!isNextVisible); - } - }; - - const container = containerRef.current; - if (container) { - container.addEventListener("scroll", handleScroll); - // Initial check - handleScroll(); - - return () => { - container.removeEventListener("scroll", handleScroll); - }; - } - }, [loading, error, timetableData]); - - const scrollToTop = () => { - containerRef.current?.scrollTo({ top: 0, behavior: "smooth" }); - }; - - const scrollToBottom = () => { - containerRef.current?.scrollTo({ - top: containerRef.current.scrollHeight, - behavior: "smooth", - }); - }; - - const scrollToNow = () => { - nextEntryRef.current?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - }; + // Scroll FABs moved to ScrollFabManager component if (loading) { return ( @@ -401,40 +332,13 @@ export default function Timetable() { /> {/* Floating Action Button */} - {(showGoToNow || showScrollTop || showScrollBottom) && ( -
- {showGoToNow && !showScrollTop && !showScrollBottom && ( - - )} - {showScrollTop && ( - - )} - {showScrollBottom && !showScrollTop && ( - - )} -
- )} + )} @@ -525,3 +429,126 @@ const TimetableTableWithScroll: React.FC<{ ); }; + +// Component to manage scroll-based FAB visibility globally within timetable +const ScrollFabManager: React.FC<{ + containerRef: React.RefObject; + nextEntryRef: React.RefObject; + currentTime: string; + data: ScheduledTable[]; + disabled?: boolean; +}> = ({ containerRef, nextEntryRef, currentTime, data, disabled = false }) => { + const { t } = useTranslation(); + const [showScrollTop, setShowScrollTop] = useState(false); + const [showScrollBottom, setShowScrollBottom] = useState(false); + const [showGoToNow, setShowGoToNow] = useState(false); + + // Find the actual scrollable ancestor (.main-content) if our container isn't scrollable + const getScrollContainer = () => { + let el: HTMLElement | null = containerRef.current; + while (el) { + const style = getComputedStyle(el); + const hasScroll = el.scrollHeight > el.clientHeight + 8; + const overflowY = style.overflowY; + if (hasScroll && (overflowY === 'auto' || overflowY === 'scroll')) { + return el; + } + el = el.parentElement; + } + return null; + }; + + useEffect(() => { + if (disabled) return; + const scrollEl = getScrollContainer(); + const useWindowScroll = !scrollEl; + + const handleScroll = () => { + const scrollTop = useWindowScroll + ? (window.scrollY || document.documentElement.scrollTop || 0) + : scrollEl!.scrollTop; + const scrollHeight = useWindowScroll + ? document.documentElement.scrollHeight + : scrollEl!.scrollHeight; + const clientHeight = useWindowScroll ? window.innerHeight : scrollEl!.clientHeight; + + const scrollBottom = scrollHeight - scrollTop - clientHeight; + const threshold = 80; // slightly smaller threshold for responsiveness + setShowScrollTop(scrollTop > threshold); + setShowScrollBottom(scrollBottom > threshold); + + if (nextEntryRef.current) { + const rect = nextEntryRef.current.getBoundingClientRect(); + const isNextVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; + setShowGoToNow(!isNextVisible); + } + }; + + const target: any = useWindowScroll ? window : scrollEl!; + target.addEventListener('scroll', handleScroll, { passive: true }); + window.addEventListener('resize', handleScroll); + handleScroll(); + return () => { + target.removeEventListener('scroll', handleScroll); + window.removeEventListener('resize', handleScroll); + }; + }, [containerRef, nextEntryRef, disabled, data, currentTime]); + + const scrollToTop = () => { + const scrollEl = getScrollContainer(); + if (!scrollEl) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + scrollEl.scrollTo({ top: 0, behavior: 'smooth' }); + } + }; + const scrollToBottom = () => { + const scrollEl = getScrollContainer(); + if (!scrollEl) { + window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); + } else { + scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'smooth' }); + } + }; + const scrollToNow = () => { + nextEntryRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + }; + + if (disabled) return null; + if (!(showGoToNow || showScrollTop || showScrollBottom)) return null; + + return ( +
+ {showGoToNow && !showScrollTop && !showScrollBottom && ( + + )} + {showScrollTop && ( + + )} + {showScrollBottom && !showScrollTop && ( + + )} +
+ ); +}; -- cgit v1.3