Redesign: Fix search dropdown opening on navigation-triggered autofocus (#40656)

This commit is contained in:
diondiondion
2026-09-22 18:01:04 +00:00
committed by GitHub
parent f0c4e511da
commit d5bd2d39a3
3 changed files with 40 additions and 12 deletions

View File

@@ -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 = <As extends React.ElementType = 'h1'>({
...otherProps
}: PolymorphicProps<FocusTargetElementProps, As>) => {
const Component = asComp ?? 'h1';
const focusOnNavigation = useFocusOnNavigation(focusTargetName);
const focusAfterNavigation = useFocusAfterNavigation(focusTargetName);
return (
<Component ref={focusOnNavigation} tabIndex={-1} {...otherProps}>
<Component ref={focusAfterNavigation} tabIndex={-1} {...otherProps}>
{children}
</Component>
);

View File

@@ -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<SearchOption[]>([]);
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<HTMLInputElement>) => {
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<{
<input
ref={useMergedRefs(
searchInputRef,
isRedesignEnabled() ? focusOnNavigation : null,
isRedesignEnabled() ? focusAfterNavigation : null,
)}
className='search__input'
type='text'

View File

@@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from '@unhead/react/helmet';
import { Column } from '@/mastodon/components/column';
@@ -26,6 +28,7 @@ import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { CollectionListItem } from '../collections/components/collection_list_item';
import exploreRedesignClasses from '../explore/redesign.module.scss';
import { SearchSection } from './components/search_section';
@@ -240,7 +243,12 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
const extraStickyHeaderContent = (
<>
<div className='explore__search-header'>
<div
className={classNames(
'explore__search-header',
isRedesignEnabled() && exploreRedesignClasses.searchHeader,
)}
>
<Search singleColumn initialValue={trimmedValue} key={trimmedValue} />
</div>