fix: Fix inaccessible "Clear search" button (#35152)
This commit is contained in:
		@@ -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}
 | 
			
		||||
    />
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
        )}
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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)}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -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}",
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user