From da4f3ac88bc975908709dd1dc13f65a517ad407d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 14 Apr 2026 13:42:16 -0500 Subject: [PATCH] Add keyboard handling --- src/components/images/Gallery/index.tsx | 70 ++++++++++++++++++- .../images/Gallery/useKeyboardHandlers.ts | 6 ++ .../images/Gallery/useKeyboardHandlers.web.ts | 43 ++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/components/images/Gallery/useKeyboardHandlers.ts create mode 100644 src/components/images/Gallery/useKeyboardHandlers.web.ts diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index 96ca6e9643..79c7af4a15 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -27,6 +27,7 @@ import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' +import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandlers' const CONTAINER_ASPECT_RATIO = 3 / 2 const ITEM_GAP = 8 // tokens.space.sm @@ -127,6 +128,30 @@ export function Gallery({ const width = bleedWidth || Math.min(600, window.width) /* End container overflow styles */ + const flatListRef = useRef(null) + const itemWidthsRef = useRef>(new Map()) + const itemRefsRef = useRef>(new Map()) + const currentIndexRef = useRef(0) + + const scrollToIndex = (index: number, animated = true) => { + let offset = 0 + for (let i = 0; i < index; i++) { + offset += (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP + } + flatListRef.current?.scrollToOffset({offset, animated}) + } + + useKeyboardHandlers({ + flatListRef, + currentIndexRef, + scrollToIndex(index: number) { + scrollToIndex(index) + const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null + el?.focus({preventScroll: true}) + }, + imageCount: images.length, + }) + return ( item.thumb} - renderItem={({item}) => { - return + renderItem={({item, index}) => { + return ( + { + itemWidthsRef.current.set(i, w) + }} + itemRef={node => { + if (node) { + itemRefsRef.current.set(index, node) + } else { + itemRefsRef.current.delete(index) + } + }} + /> + ) + }} + onScroll={e => { + const offsetX = e.nativeEvent.contentOffset.x + let accumulated = 0 + for (let i = 0; i < images.length; i++) { + const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP + if (offsetX < accumulated + w / 2) { + currentIndexRef.current = i + break + } + accumulated += w + if (i === images.length - 1) { + currentIndexRef.current = i + } + } }} style={[ { @@ -200,9 +257,15 @@ function computeDims({ function GalleryImage({ contentHeight: height, image, + index, + onWidthChange, + itemRef, }: { contentHeight: number image: AppBskyEmbedImages.ViewImage + index: number + onWidthChange: (index: number, width: number) => void + itemRef: (node: View | null) => void }) { const t = useTheme() const [aspectRatio, setAspectRatio] = useState(() => @@ -210,8 +273,11 @@ function GalleryImage({ ) const dims = computeDims({height, aspectRatio}) + onWidthChange(index, dims.width) + return ( + currentIndexRef: React.RefObject + scrollToIndex: (index: number, animated?: boolean) => void + imageCount: number +}) { + useEffect(() => { + if (imageCount <= 1) return + + const onKeyDown = (e: KeyboardEvent) => { + const el = flatListRef.current?.getScrollableNode() as unknown as HTMLElement | null + if (!el || !el.contains(document.activeElement)) return + + const current = currentIndexRef.current + if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) { + const next = current + 1 + if (next < imageCount) { + e.preventDefault() + scrollToIndex(next) + currentIndexRef.current = next + } + } else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) { + const prev = current - 1 + if (prev >= 0) { + e.preventDefault() + scrollToIndex(prev) + currentIndexRef.current = prev + } + } + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [flatListRef, currentIndexRef, scrollToIndex, imageCount]) +}