diff --git a/src/components/images/Gallery/index.tsx b/src/components/images/Gallery/index.tsx index f73733494a..cfc6cd537c 100644 --- a/src/components/images/Gallery/index.tsx +++ b/src/components/images/Gallery/index.tsx @@ -30,6 +30,7 @@ import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandlers' import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers' import {CONTAINER_ASPECT_RATIO, ITEM_GAP, MIN_PEEK} from '#/components/images/Gallery/const' +import {IS_WEB} from '#/env' interface GalleryProps { images: AppBskyEmbedImages.ViewImage[] @@ -55,6 +56,7 @@ export function GalleryBleed({children}: {children: React.ReactNode}) { const [bleedWidth, setBleedWidth] = useState(0) if (!isValidElement(children)) { + throw new Error('GalleryBleed children must be a single React element') } @@ -132,22 +134,22 @@ export function Gallery({ 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}) + const scrollTo = (offset: number) => { + flatListRef.current?.scrollToOffset({offset, animated: false}) + } + + const onSettle = (index: number) => { + if (!IS_WEB) return + const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null + el?.focus({preventScroll: true}) } useKeyboardHandlers({ flatListRef, + itemWidthsRef, currentIndexRef, - scrollToIndex(index: number) { - scrollToIndex(index) - const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null - el?.focus({preventScroll: true}) - }, + scrollTo, + onSettle, imageCount: images.length, }) @@ -155,10 +157,8 @@ export function Gallery({ flatListRef, itemWidthsRef, currentIndexRef, - onSettle(index: number) { - const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null - el?.focus({preventScroll: true}) - }, + scrollTo, + onSettle, imageCount: images.length, }) diff --git a/src/components/images/Gallery/useKeyboardHandlers.ts b/src/components/images/Gallery/useKeyboardHandlers.ts index afa29a5628..324ea28d2f 100644 --- a/src/components/images/Gallery/useKeyboardHandlers.ts +++ b/src/components/images/Gallery/useKeyboardHandlers.ts @@ -1,6 +1,8 @@ export function useKeyboardHandlers(_args: { flatListRef: any + itemWidthsRef: any currentIndexRef: any - scrollToIndex: any + scrollTo: any + onSettle: any imageCount: any }) {} diff --git a/src/components/images/Gallery/useKeyboardHandlers.web.ts b/src/components/images/Gallery/useKeyboardHandlers.web.ts index a893ea1107..490e32761e 100644 --- a/src/components/images/Gallery/useKeyboardHandlers.web.ts +++ b/src/components/images/Gallery/useKeyboardHandlers.web.ts @@ -1,43 +1,89 @@ import {useEffect} from 'react' import {type FlatList} from 'react-native' +import {ITEM_GAP} from '#/components/images/Gallery/const' +import {tween} from '#/components/images/Gallery/tween' + +const SETTLE_DURATION = 600 + +function getOffsetForIndex( + itemWidths: Map, + index: number, +): number { + let offset = 0 + for (let i = 0; i < index; i++) { + offset += (itemWidths.get(i) ?? 0) + ITEM_GAP + } + return offset +} + export function useKeyboardHandlers({ flatListRef, + itemWidthsRef, currentIndexRef, - scrollToIndex, + scrollTo, + onSettle, imageCount, }: { flatListRef: React.RefObject + itemWidthsRef: React.RefObject> currentIndexRef: React.RefObject - scrollToIndex: (index: number, animated?: boolean) => void + scrollTo: (offset: number) => void + onSettle: (index: number) => void imageCount: number }) { useEffect(() => { if (imageCount <= 1) return + let stopTween: (() => void) | null = null + const onKeyDown = (e: KeyboardEvent) => { - const el = flatListRef.current?.getScrollableNode() as unknown as HTMLElement | null + const el = + flatListRef.current?.getScrollableNode() as unknown as HTMLElement | null if (!el || !el.contains(document.activeElement)) return const current = currentIndexRef.current + let targetIndex: number | undefined + if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) { - const next = current + 1 - if (next < imageCount) { - e.preventDefault() - scrollToIndex(next) - currentIndexRef.current = next + if (current < imageCount - 1) { + targetIndex = current + 1 } } else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) { - const prev = current - 1 - if (prev >= 0) { - e.preventDefault() - scrollToIndex(prev) - currentIndexRef.current = prev + if (current > 0) { + targetIndex = current - 1 } } + + if (targetIndex != null) { + e.preventDefault() + + if (stopTween) { + stopTween() + stopTween = null + } + + const from = el.scrollLeft + const to = getOffsetForIndex(itemWidthsRef.current, targetIndex) + const idx = targetIndex + + stopTween = tween(from, to, SETTLE_DURATION)( + v => { + scrollTo(v) + }, + () => { + stopTween = null + currentIndexRef.current = idx + onSettle(idx) + }, + ) + } } window.addEventListener('keydown', onKeyDown) - return () => window.removeEventListener('keydown', onKeyDown) - }, [flatListRef, currentIndexRef, scrollToIndex, imageCount]) + return () => { + window.removeEventListener('keydown', onKeyDown) + if (stopTween) stopTween() + } + }, [flatListRef, itemWidthsRef, currentIndexRef, scrollTo, onSettle, imageCount]) } diff --git a/src/components/images/Gallery/usePointerHandlers.ts b/src/components/images/Gallery/usePointerHandlers.ts index d0cdb131e2..661c20b511 100644 --- a/src/components/images/Gallery/usePointerHandlers.ts +++ b/src/components/images/Gallery/usePointerHandlers.ts @@ -2,6 +2,7 @@ export function usePointerHandlers(_args: { flatListRef: any itemWidthsRef: any currentIndexRef: any + scrollTo: any onSettle: any imageCount: any }) {} diff --git a/src/components/images/Gallery/usePointerHandlers.web.ts b/src/components/images/Gallery/usePointerHandlers.web.ts index 69be0a3340..ee4d2372c8 100644 --- a/src/components/images/Gallery/usePointerHandlers.web.ts +++ b/src/components/images/Gallery/usePointerHandlers.web.ts @@ -51,12 +51,14 @@ export function usePointerHandlers({ flatListRef, itemWidthsRef, currentIndexRef, + scrollTo, onSettle, imageCount, }: { flatListRef: React.RefObject itemWidthsRef: React.RefObject> currentIndexRef: React.RefObject + scrollTo: (offset: number) => void onSettle: (index: number) => void imageCount: number }) { @@ -126,10 +128,10 @@ export function usePointerHandlers({ velo = (delta - prevDelta) / (elapsed * FRAME_MS) t = e.timeStamp - el.scrollLeft = dragScrollLeft - delta + scrollTo(dragScrollLeft - delta) // Update current index from scroll position - const offsetX = el.scrollLeft + const offsetX = dragScrollLeft - delta let accumulated = 0 for (let i = 0; i < imageCount; i++) { const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP @@ -184,7 +186,7 @@ export function usePointerHandlers({ stopTween = tween(from, to, SETTLE_DURATION)( v => { - el.scrollLeft = v + scrollTo(v) }, () => { stopTween = null @@ -207,5 +209,5 @@ export function usePointerHandlers({ el.style.cursor = '' el.style.userSelect = '' } - }, [flatListRef, itemWidthsRef, currentIndexRef, onSettle, imageCount]) + }, [flatListRef, itemWidthsRef, currentIndexRef, scrollTo, onSettle, imageCount]) }