Improve search menu keyboard navigation (#37255)

This commit is contained in:
diondiondion 2025-12-17 15:42:07 +01:00 committed by GitHub
parent dbc5af6641
commit c4a760d6ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,4 +1,11 @@
import { useCallback, useState, useRef, useEffect, useMemo } from 'react'; import {
useCallback,
useState,
useRef,
useEffect,
useMemo,
useId,
} from 'react';
import { import {
defineMessages, defineMessages,
@ -432,12 +439,17 @@ export const Search: React.FC<{
switch (e.key) { switch (e.key) {
case 'Escape': case 'Escape':
e.preventDefault(); e.preventDefault();
unfocus(); searchInputRef.current?.focus();
setExpanded(false);
break; break;
case 'ArrowDown': case 'ArrowDown':
e.preventDefault(); e.preventDefault();
if (!expanded) {
setExpanded(true);
}
if (navigableOptions.length > 0) { if (navigableOptions.length > 0) {
setSelectedOption( setSelectedOption(
Math.min(selectedOption + 1, navigableOptions.length - 1), Math.min(selectedOption + 1, navigableOptions.length - 1),
@ -476,10 +488,10 @@ export const Search: React.FC<{
break; break;
} }
}, },
[unfocus, navigableOptions, selectedOption, submit, value], [expanded, navigableOptions, selectedOption, submit, value],
); );
const handleFocus = useCallback(() => { const handleInputFocus = useCallback(() => {
setExpanded(true); setExpanded(true);
setSelectedOption(-1); setSelectedOption(-1);
@ -495,10 +507,16 @@ export const Search: React.FC<{
} }
}, [setExpanded, setSelectedOption, singleColumn]); }, [setExpanded, setSelectedOption, singleColumn]);
const handleBlur = useCallback(() => { const handleInputBlur = useCallback(() => {
setSelectedOption(-1); setSelectedOption(-1);
}, [setSelectedOption]); }, [setSelectedOption]);
const getOptionFocusHandler = useCallback((index: number) => {
return () => {
setSelectedOption(index);
};
}, []);
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
useEffect(() => { useEffect(() => {
@ -526,6 +544,8 @@ export const Search: React.FC<{
return () => null; return () => null;
}, [expanded]); }, [expanded]);
const searchOptionsHeading = useId();
return ( return (
<form ref={formRef} className={classNames('search', { active: expanded })}> <form ref={formRef} className={classNames('search', { active: expanded })}>
<input <input
@ -541,13 +561,20 @@ export const Search: React.FC<{
value={value} value={value}
onChange={handleChange} onChange={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onFocus={handleFocus} onFocus={handleInputFocus}
onBlur={handleBlur} onBlur={handleInputBlur}
/> />
<ClearButton hasValue={hasValue} onClick={handleClear} /> <ClearButton hasValue={hasValue} onClick={handleClear} />
<div className='search__popout' tabIndex={-1}> {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<div
className='search__popout'
role='dialog'
tabIndex={-1}
aria-labelledby={searchOptionsHeading}
onKeyDown={handleKeyDown}
>
{!hasValue && ( {!hasValue && (
<> <>
<h4> <h4>
@ -565,6 +592,7 @@ export const Search: React.FC<{
tabIndex={0} tabIndex={0}
role='button' role='button'
onMouseDown={action} onMouseDown={action}
onFocus={getOptionFocusHandler(i)}
className={classNames( className={classNames(
'search__popout__menu__item search__popout__menu__item--flex', 'search__popout__menu__item search__popout__menu__item--flex',
{ selected: selectedOption === i }, { selected: selectedOption === i },
@ -606,6 +634,7 @@ export const Search: React.FC<{
<button <button
key={key} key={key}
onMouseDown={action} onMouseDown={action}
onFocus={getOptionFocusHandler(i)}
className={classNames('search__popout__menu__item', { className={classNames('search__popout__menu__item', {
selected: selectedOption === i, selected: selectedOption === i,
})} })}
@ -618,7 +647,7 @@ export const Search: React.FC<{
</> </>
)} )}
<h4> <h4 id={searchOptionsHeading}>
<FormattedMessage <FormattedMessage
id='search_popout.options' id='search_popout.options'
defaultMessage='Search options' defaultMessage='Search options'
@ -627,20 +656,22 @@ export const Search: React.FC<{
{searchEnabled && signedIn ? ( {searchEnabled && signedIn ? (
<div className='search__popout__menu'> <div className='search__popout__menu'>
{searchOptions.map(({ key, label, action }, i) => ( {searchOptions.map(({ key, label, action }, i) => {
<button const currentIndex = (quickActions.length || recent.length) + i;
key={key} return (
onMouseDown={action} <button
className={classNames('search__popout__menu__item', { key={key}
selected: onMouseDown={action}
selectedOption === onFocus={getOptionFocusHandler(currentIndex)}
(quickActions.length || recent.length) + i, className={classNames('search__popout__menu__item', {
})} selected: selectedOption === currentIndex,
type='button' })}
> type='button'
{label} >
</button> {label}
))} </button>
);
})}
</div> </div>
) : ( ) : (
<div className='search__popout__menu__message'> <div className='search__popout__menu__message'>