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

This commit is contained in:
diondiondion
2026-03-03 11:27:46 +01:00
committed by GitHub
parent 74b3b6c798
commit 396d9dd12a
3 changed files with 109 additions and 11 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -285,6 +285,7 @@
--default-bg-color: transparent;
--hover-icon-color: var(--color-text-primary);
--hover-bg-color: var(--color-bg-brand-softer);
--focus-outline-color: var(--color-text-brand);
display: inline-flex;
color: var(--default-icon-color);
@@ -313,7 +314,7 @@
}
&:focus-visible {
outline: 2px solid var(--color-text-brand);
outline: 2px solid var(--focus-outline-color);
}
&.disabled {
@@ -5016,6 +5017,13 @@ a.status-card {
background-color: rgb(from var(--color-bg-media-base) r g b / 90%);
}
}
&:focus-visible {
.spoiler-button__overlay__label {
outline: 2px solid var(--color-text-on-media);
outline-offset: -4px;
}
}
}
}
@@ -7077,6 +7085,13 @@ a.status-card {
font-size: 14px;
font-weight: 700;
line-height: 20px;
&:focus-visible {
outline: none;
box-shadow:
inset 0 0 0 2px var(--color-text-on-media),
0 0 0 2px var(--color-bg-media);
}
}
}
@@ -7106,6 +7121,13 @@ a.status-card {
cursor: pointer;
pointer-events: auto;
&:focus-visible {
outline: none;
box-shadow:
inset 0 0 0 2px var(--color-text-on-media),
0 0 0 2px var(--color-bg-media);
}
&--non-interactive {
pointer-events: none;
}
@@ -7130,6 +7152,16 @@ a.status-card {
overflow-y: auto;
z-index: 10;
&:focus-visible {
box-shadow:
var(--dropdown-shadow),
inset 0 0 0 2px var(--color-text-on-media);
// Extend background color for better visibility of the
// inset box-shadow "outline"
outline: 2px solid var(--color-bg-media);
}
&--solid {
color: var(--color-text-primary);
background: var(--color-bg-primary);
@@ -7370,6 +7402,7 @@ a.status-card {
color: var(--color-text-primary);
position: relative;
z-index: -1;
border-radius: inherit;
&,
img {
@@ -7380,6 +7413,23 @@ a.status-card {
img {
object-fit: cover;
}
&:focus {
outline: none;
border-radius: inherit;
}
// Double focus outline for better visibility on photos
&:focus-visible::after {
content: '';
position: absolute;
inset: 2px;
z-index: 1;
border-radius: inherit;
border: 2px solid var(--color-text-on-inverted);
outline: 2px solid var(--color-bg-inverted);
pointer-events: none;
}
}
.media-gallery__preview {