fix(app): keep focus and its ring on a spoiler after it is revealed (#1961)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:53:47 +02:00
1 parent 38e86acffe
commit b51562d0e3
2 files changed
+35 -28

No files matched your search

@@ -6,6 +6,7 @@ import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro'; import {useLingui} from '@lingui/react/macro';
import {clsx} from 'clsx'; import {clsx} from 'clsx';
import type {FC, ReactNode} from 'react'; import type {FC, ReactNode} from 'react';
import {useCallback, useLayoutEffect, useRef} from 'react';
const REVEAL_SPOILER_DESCRIPTOR = msg({ const REVEAL_SPOILER_DESCRIPTOR = msg({
message: 'Reveal spoiler', message: 'Reveal spoiler',
@@ -27,9 +28,26 @@ interface SpoilerOverlayProps {
export const SpoilerOverlay: FC<SpoilerOverlayProps> = ({hidden, onReveal, children, label, inline, className}) => { export const SpoilerOverlay: FC<SpoilerOverlayProps> = ({hidden, onReveal, children, label, inline, className}) => {
const {i18n} = useLingui(); const {i18n} = useLingui();
const wrapperElementRef = useRef<HTMLDivElement | null>(null);
const pendingFocusHandoffRef = useRef(false);
const ariaLabel = label ?? i18n._(REVEAL_SPOILER_DESCRIPTOR); const ariaLabel = label ?? i18n._(REVEAL_SPOILER_DESCRIPTOR);
const handleRevealRequest = useCallback(() => {
pendingFocusHandoffRef.current = true;
onReveal();
}, [onReveal]);
useLayoutEffect(() => {
if (hidden) {
pendingFocusHandoffRef.current = false;
return;
}
if (!pendingFocusHandoffRef.current) return;
pendingFocusHandoffRef.current = false;
wrapperElementRef.current?.focus({preventScroll: true});
}, [hidden]);
return ( return (
<div <div
ref={wrapperElementRef}
tabIndex={-1}
className={clsx(styles.container, inline && styles.inline, hidden && styles.hidden, className)} className={clsx(styles.container, inline && styles.inline, hidden && styles.hidden, className)}
data-flx="app.spoiler-overlay.container" data-flx="app.spoiler-overlay.container"
> >
@@ -41,7 +59,7 @@ export const SpoilerOverlay: FC<SpoilerOverlayProps> = ({hidden, onReveal, child
<button <button
type="button" type="button"
className={styles.overlayButton} className={styles.overlayButton}
onClick={onReveal} onClick={handleRevealRequest}
aria-label={ariaLabel} aria-label={ariaLabel}
data-flx="app.spoiler-overlay.overlay-button.reveal" data-flx="app.spoiler-overlay.overlay-button.reveal"
> >
@@ -93,17 +93,30 @@ export const SpoilerRenderer = observer(function SpoilerRenderer({
const wrapperClass = isBlock ? markupStyles.blockSpoilerWrapper : markupStyles.spoilerWrapper; const wrapperClass = isBlock ? markupStyles.blockSpoilerWrapper : markupStyles.spoilerWrapper;
const spoilerClass = isBlock ? markupStyles.blockSpoiler : markupStyles.spoiler; const spoilerClass = isBlock ? markupStyles.blockSpoiler : markupStyles.spoiler;
const shouldReveal = !hidden || autoRevealed; const shouldReveal = !hidden || autoRevealed;
const revealTriggerProps = shouldReveal
? {}
: {
onClick: handleClick,
onKeyDown: handleKeyDown,
role: 'button',
'aria-label': i18n._(CLICK_TO_REVEAL_SPOILER_DESCRIPTOR),
};
return ( return (
<span <span
key={id} key={id}
className={wrapperClass} className={wrapperClass}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.span" data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.span"
> >
{shouldReveal ? ( <FocusRing
offset={-2}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.focus-ring"
>
<span <span
className={spoilerClass} className={spoilerClass}
data-revealed={shouldReveal} data-revealed={shouldReveal}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.span--2" tabIndex={shouldReveal ? -1 : 0}
{...revealTriggerProps}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.button.click"
> >
<span <span
className={markupStyles.spoilerContent} className={markupStyles.spoilerContent}
@@ -113,31 +126,7 @@ export const SpoilerRenderer = observer(function SpoilerRenderer({
{renderChildren(node.children)} {renderChildren(node.children)}
</span> </span>
</span> </span>
) : ( </FocusRing>
<FocusRing
offset={-2}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.focus-ring"
>
<span
className={spoilerClass}
data-revealed={shouldReveal}
onClick={handleClick}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label={i18n._(CLICK_TO_REVEAL_SPOILER_DESCRIPTOR)}
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.button.click"
>
<span
className={markupStyles.spoilerContent}
aria-hidden
data-flx="messaging.markdown.renderers.common.formatting-elements.spoiler-renderer.span--4"
>
{renderChildren(node.children)}
</span>
</span>
</FocusRing>
)}
</span> </span>
); );
}); });