From 35091bd8a8c6ea2988dcb0ce03f6feb365f44baf Mon Sep 17 00:00:00 2001 From: Jonathan Amobi Date: Tue, 28 Apr 2026 10:05:28 +0100 Subject: [PATCH] fix(frontend): implement infinite scrolling for the font selection dropdown menu --- .../src/components/text-format-toolbar.tsx | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/text-format-toolbar.tsx b/frontend/src/components/text-format-toolbar.tsx index afa1d47..75fef64 100644 --- a/frontend/src/components/text-format-toolbar.tsx +++ b/frontend/src/components/text-format-toolbar.tsx @@ -58,9 +58,11 @@ export default function TextFormatToolbar({ const [fontQuery, setFontQuery] = useState('') const [fontMenuOpenUpward, setFontMenuOpenUpward] = useState(false) const [fontListMaxHeightPx, setFontListMaxHeightPx] = useState(224) + const [displayCount, setDisplayCount] = useState(LIST_LIMIT) const rootRef = useRef(null) const fontTriggerWrapRef = useRef(null) const fontMenuRef = useRef(null) + const sentinelRef = useRef(null) useEffect(() => { if (!fontOpen) return @@ -82,22 +84,43 @@ export default function TextFormatToolbar({ if (!fontOpen) { setFontMenuOpenUpward(false) setFontListMaxHeightPx(224) + setDisplayCount(LIST_LIMIT) } }, [fontOpen]) - const filteredFonts = useMemo(() => { + useEffect(() => { + setDisplayCount(LIST_LIMIT) + }, [fontQuery]) + + + const allFilteredFonts = useMemo(() => { const q = fontQuery.trim().toLowerCase() - if (!q) return GOOGLE_FONT_FAMILIES.slice(0, LIST_LIMIT) - const out: string[] = [] - for (const f of GOOGLE_FONT_FAMILIES) { - if (f.toLowerCase().includes(q)) { - out.push(f) - if (out.length >= LIST_LIMIT) break - } - } - return out + if (!q) return GOOGLE_FONT_FAMILIES + return GOOGLE_FONT_FAMILIES.filter((f) => f.toLowerCase().includes(q)) }, [fontQuery]) + useEffect(() => { + if (!fontOpen) return + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setDisplayCount((prev) => + Math.min(allFilteredFonts.length, prev + LIST_LIMIT), + ) + } + }, + { rootMargin: '200px' }, + ) + if (sentinelRef.current) observer.observe(sentinelRef.current) + return () => observer.disconnect() + }, [fontOpen, allFilteredFonts.length]) + + + + const displayedFonts = useMemo(() => { + return allFilteredFonts.slice(0, displayCount) + }, [allFilteredFonts, displayCount]) + useLayoutEffect(() => { if (!fontOpen) return if (!fontTriggerWrapRef.current) return @@ -130,7 +153,7 @@ export default function TextFormatToolbar({ window.removeEventListener('resize', syncPlacement) window.removeEventListener('scroll', syncPlacement, true) } - }, [fontOpen, fontQuery, filteredFonts.length]) + }, [fontOpen, fontQuery, displayedFonts.length]) return ( - {filteredFonts.map((name) => ( + {displayedFonts.map((name) => (
  • ))} + {displayCount < allFilteredFonts.length && ( +
    + )} - {filteredFonts.length === 0 ? ( + {allFilteredFonts.length === 0 ? (

    No matches