diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 9ae677c2d2..53c5b331fe 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -165,6 +165,13 @@ export function Gallery({ const containerRefsRef = useRef>>(new Map()) const thumbDimsRef = useRef>(new Map()) const currentIndexRef = useRef(0) + /* + * Reactive mirror of currentIndexRef for the numbering badge. The ref drives + * scroll/keyboard logic without re-rendering; this state only updates when + * the index actually changes (once per image, not per scroll frame), so the + * badge stays live and cheap. + */ + const [currentIndex, setCurrentIndexState] = useState(0) const emitSwipeMetric = useMemo( () => @@ -182,6 +189,7 @@ export function Gallery({ const prev = currentIndexRef.current if (prev !== index) { currentIndexRef.current = index + setCurrentIndexState(index) emitSwipeMetric(prev, index) } } @@ -348,10 +356,69 @@ export function Gallery({ }} /> + + {!hideBadges && images.length > 1 ? ( + + ) : null} ) } +/* + * Top-right "{n}/{total}" badge for the in-feed carousel. The visible pill is + * decorative (accessible={false}) since the digits are redundant for screen + * reader users. + * + * On native, screen reader users never reach this carousel branch - the Gallery + * renders a separate per-image stack early when screenReaderEnabled is true, + * and each slide there is labelled "Image N of M". So the live region only + * needs to exist on web, where screenReaderEnabled is always false and the + * carousel is the only branch. There we announce position changes via a + * visually-hidden aria-live region. + */ +function GalleryCounter({ + currentIndex, + imageCount, +}: { + currentIndex: number + imageCount: number +}) { + const {t: l} = useLingui() + + return ( + <> + + + {currentIndex + 1}/{imageCount} + + + + {IS_WEB ? ( + + {l`Image ${currentIndex + 1} of ${imageCount}`} + + ) : null} + + ) +} + function computeDims({ height, aspectRatio,