fix: Fix inaccessible "Clear search" button (#35152)

This commit is contained in:
diondiondion 2025-06-24 16:36:05 +02:00 committed by GitHub
parent 644da36336
commit 8ba1487f30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 82 additions and 59 deletions

View File

@ -13,14 +13,13 @@ interface Props extends React.SVGProps<SVGSVGElement> {
children?: never; children?: never;
id: string; id: string;
icon: IconProp; icon: IconProp;
title?: string;
} }
export const Icon: React.FC<Props> = ({ export const Icon: React.FC<Props> = ({
id, id,
icon: IconComponent, icon: IconComponent,
className, className,
title: titleProp, 'aria-label': ariaLabel,
...other ...other
}) => { }) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -34,18 +33,19 @@ export const Icon: React.FC<Props> = ({
IconComponent = CheckBoxOutlineBlankIcon; IconComponent = CheckBoxOutlineBlankIcon;
} }
const ariaHidden = titleProp ? undefined : true; const ariaHidden = ariaLabel ? undefined : true;
const role = !ariaHidden ? 'img' : undefined; const role = !ariaHidden ? 'img' : undefined;
// Set the title to an empty string to remove the built-in SVG one if any // Set the title to an empty string to remove the built-in SVG one if any
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const title = titleProp || ''; const title = ariaLabel || '';
return ( return (
<IconComponent <IconComponent
className={classNames('icon', `icon-${id}`, className)} className={classNames('icon', `icon-${id}`, className)}
title={title} title={title}
aria-hidden={ariaHidden} aria-hidden={ariaHidden}
aria-label={ariaLabel}
role={role} role={role}
{...other} {...other}
/> />

View File

@ -318,7 +318,7 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
id='check' id='check'
icon={CheckIcon} icon={CheckIcon}
className='poll__voted__mark' className='poll__voted__mark'
title={intl.formatMessage(messages.voted)} aria-label={intl.formatMessage(messages.voted)}
/> />
</span> </span>
)} )}

View File

@ -58,7 +58,7 @@ export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
<Icon <Icon
id={visibilityIcon.icon} id={visibilityIcon.icon}
icon={visibilityIcon.iconComponent} icon={visibilityIcon.iconComponent}
title={visibilityIcon.text} aria-label={visibilityIcon.text}
/> />
); );
}; };

View File

@ -768,7 +768,7 @@ export const AccountHeader: React.FC<{
<Icon <Icon
id='lock' id='lock'
icon={LockIcon} icon={LockIcon}
title={intl.formatMessage(messages.account_locked)} aria-label={intl.formatMessage(messages.account_locked)}
/> />
); );
} }

View File

@ -29,6 +29,7 @@ import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
clearSearch: { id: 'search.clear', defaultMessage: 'Clear search' },
placeholderSignedIn: { placeholderSignedIn: {
id: 'search.search_or_paste', id: 'search.search_or_paste',
defaultMessage: 'Search or paste URL', defaultMessage: 'Search or paste URL',
@ -50,6 +51,34 @@ const unfocus = () => {
document.querySelector('.ui')?.parentElement?.focus(); document.querySelector('.ui')?.parentElement?.focus();
}; };
const ClearButton: React.FC<{
onClick: () => void;
hasValue: boolean;
}> = ({ onClick, hasValue }) => {
const intl = useIntl();
return (
<div
className={classNames('search__icon-wrapper', { 'has-value': hasValue })}
>
<Icon id='search' icon={SearchIcon} className='search__icon' />
<button
type='button'
onClick={onClick}
className='search__icon search__icon--clear-button'
tabIndex={hasValue ? undefined : -1}
aria-hidden={!hasValue}
>
<Icon
id='times-circle'
icon={CancelIcon}
aria-label={intl.formatMessage(messages.clearSearch)}
/>
</button>
</div>
);
};
interface SearchOption { interface SearchOption {
key: string; key: string;
label: React.ReactNode; label: React.ReactNode;
@ -380,6 +409,7 @@ export const Search: React.FC<{
setValue(''); setValue('');
setQuickActions([]); setQuickActions([]);
setSelectedOption(-1); setSelectedOption(-1);
unfocus();
}, [setValue, setQuickActions, setSelectedOption]); }, [setValue, setQuickActions, setSelectedOption]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
@ -474,19 +504,7 @@ export const Search: React.FC<{
onBlur={handleBlur} onBlur={handleBlur}
/> />
<button type='button' className='search__icon' onClick={handleClear}> <ClearButton hasValue={hasValue} onClick={handleClear} />
<Icon
id='search'
icon={SearchIcon}
className={hasValue ? '' : 'active'}
/>
<Icon
id='times-circle'
icon={CancelIcon}
className={hasValue ? 'active' : ''}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</button>
<div className='search__popout'> <div className='search__popout'>
{!hasValue && ( {!hasValue && (

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Rule violation", "report_notification.categories.violation": "Rule violation",
"report_notification.categories.violation_sentence": "rule violation", "report_notification.categories.violation_sentence": "rule violation",
"report_notification.open": "Open report", "report_notification.open": "Open report",
"search.clear": "Clear search",
"search.no_recent_searches": "No recent searches", "search.no_recent_searches": "No recent searches",
"search.placeholder": "Search", "search.placeholder": "Search",
"search.quick_action.account_search": "Profiles matching {x}", "search.quick_action.account_search": "Profiles matching {x}",

View File

@ -5670,18 +5670,47 @@ a.status-card {
} }
} }
.search__icon { .search__icon-wrapper {
background: transparent;
border: 0;
padding: 0;
position: absolute; position: absolute;
top: 12px + 2px; top: 14px;
cursor: default; display: grid;
pointer-events: none;
margin-inline-start: 16px - 2px; margin-inline-start: 16px - 2px;
width: 20px; width: 20px;
height: 20px; height: 20px;
.icon {
width: 100%;
height: 100%;
}
&:not(.has-value) {
pointer-events: none;
}
}
.search__icon {
grid-area: 1 / 1;
transition: all 100ms linear;
transition-property: transform, opacity;
color: $darker-text-color;
}
.search__icon.icon-search {
.has-value & {
pointer-events: none;
opacity: 0;
transform: rotate(90deg);
}
}
.search__icon--clear-button {
background: transparent;
border: 0;
padding: 0;
width: 20px;
height: 20px;
border-radius: 100%;
&::-moz-focus-inner { &::-moz-focus-inner {
border: 0; border: 0;
} }
@ -5691,39 +5720,14 @@ a.status-card {
outline: 0 !important; outline: 0 !important;
} }
.icon { &:focus-visible {
position: absolute; box-shadow: 0 0 0 2px $ui-button-focus-outline-color;
top: 0; }
inset-inline-start: 0;
&[aria-hidden='true'] {
pointer-events: none;
opacity: 0; opacity: 0;
transition: all 100ms linear; transform: rotate(-90deg);
transition-property: transform, opacity;
width: 20px;
height: 20px;
color: $darker-text-color;
&.active {
pointer-events: auto;
opacity: 1;
}
}
.icon-search {
transform: rotate(90deg);
&.active {
pointer-events: none;
transform: rotate(0deg);
}
}
.icon-times-circle {
transform: rotate(0deg);
cursor: pointer;
&.active {
transform: rotate(90deg);
}
} }
} }