From 396d9dd12a8a6570bc18c00746abe105977e4173 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 3 Mar 2026 11:27:46 +0100 Subject: [PATCH] Allow scrolling alt text popover with keyboard & improved media focus outlines (#38033) --- .../index.tsx} | 51 ++++++++++++++---- .../alt_text_badge/styles.module.scss | 17 ++++++ .../styles/mastodon/components.scss | 52 ++++++++++++++++++- 3 files changed, 109 insertions(+), 11 deletions(-) rename app/javascript/mastodon/components/{alt_text_badge.tsx => alt_text_badge/index.tsx} (54%) create mode 100644 app/javascript/mastodon/components/alt_text_badge/styles.module.scss diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge/index.tsx similarity index 54% rename from app/javascript/mastodon/components/alt_text_badge.tsx rename to app/javascript/mastodon/components/alt_text_badge/index.tsx index 1aa847b65f..6bb64254c6 100644 --- a/app/javascript/mastodon/components/alt_text_badge.tsx +++ b/app/javascript/mastodon/components/alt_text_badge/index.tsx @@ -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(null); + const intl = useIntl(); + const uniqueId = useId(); + const popoverId = `${uniqueId}-popover`; + const titleId = `${uniqueId}-title`; + const buttonRef = useRef(null); + const popoverRef = useRef(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 }> = ({ <> @@ -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 }> = ({
-

+

+ + +

{description}

diff --git a/app/javascript/mastodon/components/alt_text_badge/styles.module.scss b/app/javascript/mastodon/components/alt_text_badge/styles.module.scss new file mode 100644 index 0000000000..1b7d5ec788 --- /dev/null +++ b/app/javascript/mastodon/components/alt_text_badge/styles.module.scss @@ -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; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8e67274a62..aee074e453 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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 {