From 8ba1487f30685fff4555a7537d3e6c765c73a07c Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 24 Jun 2025 16:36:05 +0200 Subject: [PATCH] fix: Fix inaccessible "Clear search" button (#35152) --- app/javascript/mastodon/components/icon.tsx | 8 +- app/javascript/mastodon/components/poll.tsx | 2 +- .../mastodon/components/visibility_icon.tsx | 2 +- .../components/account_header.tsx | 2 +- .../features/compose/components/search.tsx | 44 +++++++--- app/javascript/mastodon/locales/en.json | 1 + .../styles/mastodon/components.scss | 82 ++++++++++--------- 7 files changed, 82 insertions(+), 59 deletions(-) diff --git a/app/javascript/mastodon/components/icon.tsx b/app/javascript/mastodon/components/icon.tsx index f388380c4..165c214c1 100644 --- a/app/javascript/mastodon/components/icon.tsx +++ b/app/javascript/mastodon/components/icon.tsx @@ -13,14 +13,13 @@ interface Props extends React.SVGProps { children?: never; id: string; icon: IconProp; - title?: string; } export const Icon: React.FC = ({ 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 = ({ 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 ( diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index e9b3b2b67..d9e76617d 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -318,7 +318,7 @@ const PollOption: React.FC = (props) => { id='check' icon={CheckIcon} className='poll__voted__mark' - title={intl.formatMessage(messages.voted)} + aria-label={intl.formatMessage(messages.voted)} /> )} diff --git a/app/javascript/mastodon/components/visibility_icon.tsx b/app/javascript/mastodon/components/visibility_icon.tsx index 3a310cbae..293b5253c 100644 --- a/app/javascript/mastodon/components/visibility_icon.tsx +++ b/app/javascript/mastodon/components/visibility_icon.tsx @@ -58,7 +58,7 @@ export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({ ); }; diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 18807ecf8..30433151c 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -768,7 +768,7 @@ export const AccountHeader: React.FC<{ ); } diff --git a/app/javascript/mastodon/features/compose/components/search.tsx b/app/javascript/mastodon/features/compose/components/search.tsx index 30a7a84db..ae242190e 100644 --- a/app/javascript/mastodon/features/compose/components/search.tsx +++ b/app/javascript/mastodon/features/compose/components/search.tsx @@ -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 ( +
+ + +
+ ); +}; + 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} /> - +
{!hasValue && ( diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 0f53dbe57..6a2a7a885 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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}", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c17a4f158..c32110be8 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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); } }