have back button dismiss lightbox on web (#9874)

This commit is contained in:
Spence Pope
2026-02-16 08:39:53 -05:00
committed by GitHub
parent 9b830fd425
commit 6be7751769
3 changed files with 56 additions and 15 deletions
+16
View File
@@ -108,6 +108,22 @@ export const atoms = {
animation: `zoomIn ${EXP_CURVE} 0.3s, fadeIn ${EXP_CURVE} 0.3s`,
}),
/**
* Visually hidden but available to screen readers (web).
* Use for live regions or off-screen labels (e.g. "Image 1 of 3").
*/
sr_only: web({
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
whiteSpace: 'nowrap',
borderWidth: 0,
}),
/**
* {@link Layout.SCROLLBAR_OFFSET}
*/
@@ -72,19 +72,7 @@ export function VideoEmbedInnerWeb({
loop={loop}
/>
{embed.alt && (
<figcaption
id={figId}
style={{
position: 'absolute',
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
borderWidth: 0,
}}>
<figcaption id={figId} style={a.sr_only}>
{embed.alt}
</figcaption>
)}
+39 -2
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useState} from 'react'
import {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
@@ -69,7 +69,13 @@ function LightboxContainer({
<Backdrop />
<RemoveScrollBar />
<FocusScope.FocusScope loop trapped asChild>
<div style={{position: 'absolute', inset: 0}}>{children}</div>
<div
role="dialog"
aria-modal="true"
aria-label={_(msg`Image viewer`)}
style={{position: 'absolute', inset: 0}}>
{children}
</div>
</FocusScope.FocusScope>
</Pressable>
)
@@ -125,6 +131,32 @@ function LightboxGallery({
return () => window.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
// Push a history entry so the browser back button closes the lightbox
// instead of navigating away from the page.
const closedByPopStateRef = useRef(false)
useEffect(() => {
history.pushState({lightbox: true}, '')
const handlePopState = () => {
closedByPopStateRef.current = true
onClose()
}
window.addEventListener('popstate', handlePopState)
return () => {
window.removeEventListener('popstate', handlePopState)
// Only pop our entry if it's still the current one. If navigation
// already pushed a new entry on top, leave the orphaned entry —
// it shares the same URL so traversing through it is harmless.
if (
!closedByPopStateRef.current &&
(history.state as {lightbox?: boolean})?.lightbox
) {
history.back()
}
}
}, [onClose])
const delayedFadeInAnim = !reduceMotionEnabled && [
a.fade_in,
{animationDelay: '0.2s', animationFillMode: 'both'},
@@ -205,6 +237,11 @@ function LightboxGallery({
</Pressable>
</View>
) : null}
{imgs.length > 1 && (
<div aria-live="polite" aria-atomic="true" style={a.sr_only}>
<Text>{_(msg`Image ${index + 1} of ${imgs.length}`)}</Text>
</div>
)}
<Button
onPress={onClose}
style={[