Use same scroll handling for both web hooks

This commit is contained in:
Eric Bailey
2026-04-14 15:30:37 -05:00
parent d4185da5b9
commit 23e3cf9b75
5 changed files with 86 additions and 35 deletions
+15 -15
View File
@@ -30,6 +30,7 @@ import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandlers' import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandlers'
import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers' import {usePointerHandlers} from '#/components/images/Gallery/usePointerHandlers'
import {CONTAINER_ASPECT_RATIO, ITEM_GAP, MIN_PEEK} from '#/components/images/Gallery/const' import {CONTAINER_ASPECT_RATIO, ITEM_GAP, MIN_PEEK} from '#/components/images/Gallery/const'
import {IS_WEB} from '#/env'
interface GalleryProps { interface GalleryProps {
images: AppBskyEmbedImages.ViewImage[] images: AppBskyEmbedImages.ViewImage[]
@@ -55,6 +56,7 @@ export function GalleryBleed({children}: {children: React.ReactNode}) {
const [bleedWidth, setBleedWidth] = useState(0) const [bleedWidth, setBleedWidth] = useState(0)
if (!isValidElement(children)) { if (!isValidElement(children)) {
throw new Error('GalleryBleed children must be a single React element') throw new Error('GalleryBleed children must be a single React element')
} }
@@ -132,22 +134,22 @@ export function Gallery({
const itemRefsRef = useRef<Map<number, View>>(new Map()) const itemRefsRef = useRef<Map<number, View>>(new Map())
const currentIndexRef = useRef(0) const currentIndexRef = useRef(0)
const scrollToIndex = (index: number, animated = true) => { const scrollTo = (offset: number) => {
let offset = 0 flatListRef.current?.scrollToOffset({offset, animated: false})
for (let i = 0; i < index; i++) { }
offset += (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP
} const onSettle = (index: number) => {
flatListRef.current?.scrollToOffset({offset, animated}) if (!IS_WEB) return
const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null
el?.focus({preventScroll: true})
} }
useKeyboardHandlers({ useKeyboardHandlers({
flatListRef, flatListRef,
itemWidthsRef,
currentIndexRef, currentIndexRef,
scrollToIndex(index: number) { scrollTo,
scrollToIndex(index) onSettle,
const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null
el?.focus({preventScroll: true})
},
imageCount: images.length, imageCount: images.length,
}) })
@@ -155,10 +157,8 @@ export function Gallery({
flatListRef, flatListRef,
itemWidthsRef, itemWidthsRef,
currentIndexRef, currentIndexRef,
onSettle(index: number) { scrollTo,
const el = itemRefsRef.current.get(index) as unknown as HTMLElement | null onSettle,
el?.focus({preventScroll: true})
},
imageCount: images.length, imageCount: images.length,
}) })
@@ -1,6 +1,8 @@
export function useKeyboardHandlers(_args: { export function useKeyboardHandlers(_args: {
flatListRef: any flatListRef: any
itemWidthsRef: any
currentIndexRef: any currentIndexRef: any
scrollToIndex: any scrollTo: any
onSettle: any
imageCount: any imageCount: any
}) {} }) {}
@@ -1,43 +1,89 @@
import {useEffect} from 'react' import {useEffect} from 'react'
import {type FlatList} from 'react-native' 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<number, number>,
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({ export function useKeyboardHandlers({
flatListRef, flatListRef,
itemWidthsRef,
currentIndexRef, currentIndexRef,
scrollToIndex, scrollTo,
onSettle,
imageCount, imageCount,
}: { }: {
flatListRef: React.RefObject<FlatList | null> flatListRef: React.RefObject<FlatList | null>
itemWidthsRef: React.RefObject<Map<number, number>>
currentIndexRef: React.RefObject<number> currentIndexRef: React.RefObject<number>
scrollToIndex: (index: number, animated?: boolean) => void scrollTo: (offset: number) => void
onSettle: (index: number) => void
imageCount: number imageCount: number
}) { }) {
useEffect(() => { useEffect(() => {
if (imageCount <= 1) return if (imageCount <= 1) return
let stopTween: (() => void) | null = null
const onKeyDown = (e: KeyboardEvent) => { 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 if (!el || !el.contains(document.activeElement)) return
const current = currentIndexRef.current const current = currentIndexRef.current
let targetIndex: number | undefined
if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) { if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) {
const next = current + 1 if (current < imageCount - 1) {
if (next < imageCount) { targetIndex = current + 1
e.preventDefault()
scrollToIndex(next)
currentIndexRef.current = next
} }
} else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) { } else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) {
const prev = current - 1 if (current > 0) {
if (prev >= 0) { targetIndex = current - 1
e.preventDefault()
scrollToIndex(prev)
currentIndexRef.current = prev
} }
} }
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) window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown) return () => {
}, [flatListRef, currentIndexRef, scrollToIndex, imageCount]) window.removeEventListener('keydown', onKeyDown)
if (stopTween) stopTween()
}
}, [flatListRef, itemWidthsRef, currentIndexRef, scrollTo, onSettle, imageCount])
} }
@@ -2,6 +2,7 @@ export function usePointerHandlers(_args: {
flatListRef: any flatListRef: any
itemWidthsRef: any itemWidthsRef: any
currentIndexRef: any currentIndexRef: any
scrollTo: any
onSettle: any onSettle: any
imageCount: any imageCount: any
}) {} }) {}
@@ -51,12 +51,14 @@ export function usePointerHandlers({
flatListRef, flatListRef,
itemWidthsRef, itemWidthsRef,
currentIndexRef, currentIndexRef,
scrollTo,
onSettle, onSettle,
imageCount, imageCount,
}: { }: {
flatListRef: React.RefObject<FlatList | null> flatListRef: React.RefObject<FlatList | null>
itemWidthsRef: React.RefObject<Map<number, number>> itemWidthsRef: React.RefObject<Map<number, number>>
currentIndexRef: React.RefObject<number> currentIndexRef: React.RefObject<number>
scrollTo: (offset: number) => void
onSettle: (index: number) => void onSettle: (index: number) => void
imageCount: number imageCount: number
}) { }) {
@@ -126,10 +128,10 @@ export function usePointerHandlers({
velo = (delta - prevDelta) / (elapsed * FRAME_MS) velo = (delta - prevDelta) / (elapsed * FRAME_MS)
t = e.timeStamp t = e.timeStamp
el.scrollLeft = dragScrollLeft - delta scrollTo(dragScrollLeft - delta)
// Update current index from scroll position // Update current index from scroll position
const offsetX = el.scrollLeft const offsetX = dragScrollLeft - delta
let accumulated = 0 let accumulated = 0
for (let i = 0; i < imageCount; i++) { for (let i = 0; i < imageCount; i++) {
const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP const w = (itemWidthsRef.current.get(i) ?? 0) + ITEM_GAP
@@ -184,7 +186,7 @@ export function usePointerHandlers({
stopTween = tween(from, to, SETTLE_DURATION)( stopTween = tween(from, to, SETTLE_DURATION)(
v => { v => {
el.scrollLeft = v scrollTo(v)
}, },
() => { () => {
stopTween = null stopTween = null
@@ -207,5 +209,5 @@ export function usePointerHandlers({
el.style.cursor = '' el.style.cursor = ''
el.style.userSelect = '' el.style.userSelect = ''
} }
}, [flatListRef, itemWidthsRef, currentIndexRef, onSettle, imageCount]) }, [flatListRef, itemWidthsRef, currentIndexRef, scrollTo, onSettle, imageCount])
} }