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;
id: string;
icon: IconProp;
title?: string;
}
export const Icon: React.FC<Props> = ({
id,
icon: IconComponent,
className,
title: titleProp,
'aria-label': ariaLabel,
...other
}) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -34,18 +33,19 @@ export const Icon: React.FC<Props> = ({
IconComponent = CheckBoxOutlineBlankIcon;
}
const ariaHidden = titleProp ? undefined : true;
const ariaHidden = ariaLabel ? undefined : true;
const role = !ariaHidden ? 'img' : undefined;
// 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
const title = titleProp || '';
const title = ariaLabel || '';
return (
<IconComponent
className={classNames('icon', `icon-${id}`, className)}
title={title}
aria-hidden={ariaHidden}
aria-label={ariaLabel}
role={role}
{...other}
/>

View File

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

View File

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

View File

@ -768,7 +768,7 @@ export const AccountHeader: React.FC<{
<Icon
id='lock'
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({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
clearSearch: { id: 'search.clear', defaultMessage: 'Clear search' },
placeholderSignedIn: {
id: 'search.search_or_paste',
defaultMessage: 'Search or paste URL',
@ -50,6 +51,34 @@ const unfocus = () => {
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 {
key: string;
label: React.ReactNode;
@ -380,6 +409,7 @@ export const Search: React.FC<{
setValue('');
setQuickActions([]);
setSelectedOption(-1);
unfocus();
}, [setValue, setQuickActions, setSelectedOption]);
const handleKeyDown = useCallback(
@ -474,19 +504,7 @@ export const Search: React.FC<{
onBlur={handleBlur}
/>
<button type='button' className='search__icon' 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>
<ClearButton hasValue={hasValue} onClick={handleClear} />
<div className='search__popout'>
{!hasValue && (

View File

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

View File

@ -5670,18 +5670,47 @@ a.status-card {
}
}
.search__icon {
background: transparent;
border: 0;
padding: 0;
.search__icon-wrapper {
position: absolute;
top: 12px + 2px;
cursor: default;
pointer-events: none;
top: 14px;
display: grid;
margin-inline-start: 16px - 2px;
width: 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 {
border: 0;
}
@ -5691,39 +5720,14 @@ a.status-card {
outline: 0 !important;
}
.icon {
position: absolute;
top: 0;
inset-inline-start: 0;
&:focus-visible {
box-shadow: 0 0 0 2px $ui-button-focus-outline-color;
}
&[aria-hidden='true'] {
pointer-events: none;
opacity: 0;
transition: all 100ms linear;
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);
}
transform: rotate(-90deg);
}
}