From 5f5d01047fa3d1576ca0504415f7d1c44da63a48 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Fri, 21 Nov 2025 23:28:16 +0800 Subject: [PATCH] feat: Dynamically show/hide expand button for badge lists based on content overflow --- components/search/SourceBadgeList.tsx | 75 +++++++++++++++++---------- components/search/TypeBadgeList.tsx | 72 +++++++++++++++---------- 2 files changed, 94 insertions(+), 53 deletions(-) diff --git a/components/search/SourceBadgeList.tsx b/components/search/SourceBadgeList.tsx index 117bae8..df8e2db 100644 --- a/components/search/SourceBadgeList.tsx +++ b/components/search/SourceBadgeList.tsx @@ -6,7 +6,7 @@ 'use client'; -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import { Icons } from '@/components/ui/Icon'; import { SourceBadgeItem } from './SourceBadgeItem'; import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation'; @@ -26,7 +26,9 @@ interface SourceBadgeListProps { export function SourceBadgeList({ sources, selectedSources, onToggleSource }: SourceBadgeListProps) { const [isExpanded, setIsExpanded] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); + const [hasOverflow, setHasOverflow] = useState(false); const containerRef = useRef(null); + const badgeContainerRef = useRef(null); const badgeRefs = useRef<(HTMLButtonElement | null)[]>([]); // Keyboard navigation @@ -40,8 +42,8 @@ export function SourceBadgeList({ sources, selectedSources, onToggleSource }: So setFocusedIndex(index); badgeRefs.current[index]?.focus(); // Scroll into view for mobile - badgeRefs.current[index]?.scrollIntoView({ - behavior: 'smooth', + badgeRefs.current[index]?.scrollIntoView({ + behavior: 'smooth', block: 'nearest', inline: 'center', }); @@ -51,42 +53,61 @@ export function SourceBadgeList({ sources, selectedSources, onToggleSource }: So }, [sources, onToggleSource]), }); + // Check if content has overflow on mount and when sources change + useEffect(() => { + const checkOverflow = () => { + if (badgeContainerRef.current) { + const maxHeight = 50; // 50px to fit one row (44px) + padding but hide second row (starts at 52px) + setHasOverflow(badgeContainerRef.current.scrollHeight > maxHeight); + } + }; + + checkOverflow(); + // Recheck after a short delay to account for animations + const timeout = setTimeout(checkOverflow, 100); + return () => clearTimeout(timeout); + }, [sources]); + return ( <> {/* Desktop: Expandable Grid */} -
-
- {sources.map((source, index) => ( - onToggleSource(source.id)} - isFocused={focusedIndex === index} - onFocus={() => setFocusedIndex(index)} - innerRef={(el: HTMLButtonElement | null) => { badgeRefs.current[index] = el; }} - /> - ))} +
+
+ {sources.map((source, index) => ( + onToggleSource(source.id)} + isFocused={focusedIndex === index} + onFocus={() => setFocusedIndex(index)} + innerRef={(el: HTMLButtonElement | null) => { badgeRefs.current[index] = el; }} + /> + ))} +
- - {sources.length > 5 && ( + + {hasOverflow && ( @@ -94,12 +115,12 @@ export function SourceBadgeList({ sources, selectedSources, onToggleSource }: So
{/* Mobile & Tablet: Horizontal Scroll */} -
-
diff --git a/components/search/TypeBadgeList.tsx b/components/search/TypeBadgeList.tsx index ed176d3..003d485 100644 --- a/components/search/TypeBadgeList.tsx +++ b/components/search/TypeBadgeList.tsx @@ -6,7 +6,7 @@ 'use client'; -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import { Icons } from '@/components/ui/Icon'; import { TypeBadgeItem } from './TypeBadgeItem'; import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation'; @@ -25,7 +25,9 @@ interface TypeBadgeListProps { export function TypeBadgeList({ badges, selectedTypes, onToggleType }: TypeBadgeListProps) { const [isExpanded, setIsExpanded] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); + const [hasOverflow, setHasOverflow] = useState(false); const containerRef = useRef(null); + const badgeContainerRef = useRef(null); const badgeRefs = useRef<(HTMLButtonElement | null)[]>([]); // Keyboard navigation @@ -39,8 +41,8 @@ export function TypeBadgeList({ badges, selectedTypes, onToggleType }: TypeBadge setFocusedIndex(index); badgeRefs.current[index]?.focus(); // Scroll into view for mobile - badgeRefs.current[index]?.scrollIntoView({ - behavior: 'smooth', + badgeRefs.current[index]?.scrollIntoView({ + behavior: 'smooth', block: 'nearest', inline: 'center', }); @@ -50,41 +52,59 @@ export function TypeBadgeList({ badges, selectedTypes, onToggleType }: TypeBadge }, [badges, onToggleType]), }); + // Check if content has overflow on mount and when badges change + useEffect(() => { + const checkOverflow = () => { + if (badgeContainerRef.current) { + const maxHeight = 50; // 50px to fit one row (44px) + padding but hide second row (starts at 52px) + setHasOverflow(badgeContainerRef.current.scrollHeight > maxHeight); + } + }; + + checkOverflow(); + // Recheck after a short delay to account for animations + const timeout = setTimeout(checkOverflow, 100); + return () => clearTimeout(timeout); + }, [badges]); + return ( <> {/* Desktop: Expandable Grid */} -
-
- {badges.map((badge, index) => ( - onToggleType(badge.type)} - isFocused={focusedIndex === index} - onFocus={() => setFocusedIndex(index)} - innerRef={(el) => { badgeRefs.current[index] = el; }} - /> - ))} +
+
+ {badges.map((badge, index) => ( + onToggleType(badge.type)} + isFocused={focusedIndex === index} + onFocus={() => setFocusedIndex(index)} + innerRef={(el) => { badgeRefs.current[index] = el; }} + /> + ))} +
- - {badges.length > 5 && ( + {hasOverflow && ( @@ -92,12 +112,12 @@ export function TypeBadgeList({ badges, selectedTypes, onToggleType }: TypeBadge
{/* Mobile & Tablet: Horizontal Scroll */} -
-