From d5bd2d39a3272069a915a8780f496c5ed4f556a5 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 22 Sep 2026 18:01:04 +0000 Subject: [PATCH] Redesign: Fix search dropdown opening on navigation-triggered autofocus (#40656) --- .../navigation_focus_target/index.tsx | 20 ++++++++++++----- .../features/compose/components/search.tsx | 22 ++++++++++++++----- .../mastodon/features/search/index.tsx | 10 ++++++++- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/components/navigation_focus_target/index.tsx b/app/javascript/mastodon/components/navigation_focus_target/index.tsx index c23dff79c5b..9ffe0f3c354 100644 --- a/app/javascript/mastodon/components/navigation_focus_target/index.tsx +++ b/app/javascript/mastodon/components/navigation_focus_target/index.tsx @@ -99,15 +99,20 @@ export const FocusTargetProvider: React.FC<{ ); }; -export function useFocusOnNavigation(targetName?: NamedFocusTarget) { +export function useFocusAfterNavigation( + targetName?: NamedFocusTarget, + onAfterFocus?: (hasSetFocus: boolean) => void, +) { const focusTargetRef = useContext(FocusTargetContext); + const hasFocusedOnceRef = useRef(false); return useCallback( (element: HTMLElement | null) => { const focusTarget = focusTargetRef?.current; // Bail out if focusTarget was set to `false` - if (!element || !focusTarget) { + if (!element || !focusTarget || hasFocusedOnceRef.current) { + onAfterFocus?.(false); return; } @@ -118,10 +123,15 @@ export function useFocusOnNavigation(targetName?: NamedFocusTarget) { if (shouldSetFocus) { setTimeout(() => { element.focus({ preventScroll: true }); + onAfterFocus?.(true); + // Prevent this focus handler from setting focus again + hasFocusedOnceRef.current = true; }, 0); + } else { + onAfterFocus?.(false); } }, - [focusTargetRef, targetName], + [focusTargetRef, onAfterFocus, targetName], ); } @@ -136,10 +146,10 @@ export const NavigationFocusTarget = ({ ...otherProps }: PolymorphicProps) => { const Component = asComp ?? 'h1'; - const focusOnNavigation = useFocusOnNavigation(focusTargetName); + const focusAfterNavigation = useFocusAfterNavigation(focusTargetName); return ( - + {children} ); diff --git a/app/javascript/mastodon/features/compose/components/search.tsx b/app/javascript/mastodon/features/compose/components/search.tsx index 8bdae5f91c9..6730e111645 100644 --- a/app/javascript/mastodon/features/compose/components/search.tsx +++ b/app/javascript/mastodon/features/compose/components/search.tsx @@ -21,7 +21,7 @@ import { isFulfilled } from '@reduxjs/toolkit'; import { FOCUS_TARGET, - useFocusOnNavigation, + useFocusAfterNavigation, } from '@/mastodon/components/navigation_focus_target'; import { getCollectionPath } from '@/mastodon/features/collections/utils'; import { useMergedRefs } from '@/mastodon/hooks/useMergedRefs'; @@ -111,7 +111,13 @@ export const Search: React.FC<{ const [expanded, setExpanded] = useState(false); const [selectedOption, setSelectedOption] = useState(-1); const [quickActions, setQuickActions] = useState([]); - const focusOnNavigation = useFocusOnNavigation(FOCUS_TARGET.SEARCH); + const [shouldOpenOnFocus, setShouldOpenOnFocus] = useState(false); + const focusAfterNavigation = useFocusAfterNavigation( + FOCUS_TARGET.SEARCH, + () => { + setShouldOpenOnFocus(true); + }, + ); const unfocus = useCallback(() => { document.querySelector('.ui')?.parentElement?.focus(); @@ -318,6 +324,8 @@ export const Search: React.FC<{ const handleChange = useCallback( ({ target: { value } }: React.ChangeEvent) => { setValue(value); + setExpanded(true); + setSelectedOption(-1); const trimmedValue = value.trim(); const newQuickActions = []; @@ -504,8 +512,10 @@ export const Search: React.FC<{ ); const handleInputFocus = useCallback(() => { - setExpanded(true); - setSelectedOption(-1); + if (shouldOpenOnFocus) { + setExpanded(true); + setSelectedOption(-1); + } if (searchInputRef.current && !singleColumn) { const { left, right } = searchInputRef.current.getBoundingClientRect(); @@ -517,7 +527,7 @@ export const Search: React.FC<{ searchInputRef.current.scrollIntoView(); } } - }, [setExpanded, setSelectedOption, singleColumn]); + }, [shouldOpenOnFocus, singleColumn]); const handleInputBlur = useCallback(() => { setSelectedOption(-1); @@ -567,7 +577,7 @@ export const Search: React.FC<{ = ({ const extraStickyHeaderContent = ( <> -
+