Allow scrolling alt text popover with keyboard & improved media focus outlines (#38033)

This commit is contained in:
diondiondion authored and GitHub committed 2026-03-03 10:27:46 +00:00
1 parent 74b3b6c798
commit 396d9dd12a
3 files changed
+109 -11

No files matched your search

@@ -1,6 +1,6 @@
import { useState, useCallback, useRef, useId } from 'react';
import { useState, useCallback, useRef, useId, Fragment } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import type {
OffsetValue,
@@ -8,24 +8,37 @@ import type {
} from 'react-overlays/esm/usePopper';
import Overlay from 'react-overlays/Overlay';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
import { IconButton } from '../icon_button';
import classes from './styles.module.scss';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const AltTextBadge: React.FC<{ description: string }> = ({
description,
}) => {
const accessibilityId = useId();
const anchorRef = useRef<HTMLButtonElement>(null);
const intl = useIntl();
const uniqueId = useId();
const popoverId = `${uniqueId}-popover`;
const titleId = `${uniqueId}-title`;
const buttonRef = useRef<HTMLButtonElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
setOpen((v) => !v);
setTimeout(() => {
popoverRef.current?.focus();
}, 0);
}, [setOpen]);
const handleClose = useCallback(() => {
setOpen(false);
buttonRef.current?.focus();
}, [setOpen]);
const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClose);
@@ -34,11 +47,12 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
<>
<button
type='button'
ref={anchorRef}
ref={buttonRef}
className='media-gallery__alt__label'
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
aria-controls={popoverId}
aria-haspopup='dialog'
>
ALT
</button>
@@ -47,7 +61,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
rootClose
onHide={handleClose}
show={open}
target={anchorRef}
target={buttonRef}
placement='top-end'
flip
offset={offset}
@@ -57,17 +71,34 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
<div {...props} className='hover-card-controller'>
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
className='info-tooltip dropdown-animation'
role='region'
id={accessibilityId}
role='dialog'
aria-labelledby={titleId}
ref={popoverRef}
id={popoverId}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
>
<h4>
<h4 id={titleId}>
<FormattedMessage
id='alt_text_badge.title'
defaultMessage='Alt text'
tagName={Fragment}
/>
</h4>
<IconButton
title={intl.formatMessage({
id: 'lightbox.close',
defaultMessage: 'Close',
})}
icon='close'
iconComponent={CloseIcon}
onClick={handleClose}
className={classes.closeButton}
/>
<p>{description}</p>
</div>
</div>
@@ -0,0 +1,17 @@
.closeButton {
position: absolute;
top: 5px;
inset-inline-end: 2px;
padding: 10px;
--default-icon-color: var(--color-text-on-media);
--default-bg-color: transparent;
--hover-icon-color: var(--color-text-on-media);
--hover-bg-color: rgb(from var(--color-text-on-media) r g b / 10%);
--focus-outline-color: var(--color-text-on-media);
svg {
width: 20px;
height: 20px;
}
}