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 {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<Map<number, View>>(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,
})
@@ -1,6 +1,8 @@
export function useKeyboardHandlers(_args: {
flatListRef: any
itemWidthsRef: any
currentIndexRef: any
scrollToIndex: any
scrollTo: any
onSettle: any
imageCount: any
}) {}
@@ -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<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({
flatListRef,
itemWidthsRef,
currentIndexRef,
scrollToIndex,
scrollTo,
onSettle,
imageCount,
}: {
flatListRef: React.RefObject<FlatList | null>
itemWidthsRef: React.RefObject<Map<number, number>>
currentIndexRef: React.RefObject<number>
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])
}
@@ -2,6 +2,7 @@ export function usePointerHandlers(_args: {
flatListRef: any
itemWidthsRef: any
currentIndexRef: any
scrollTo: any
onSettle: any
imageCount: any
}) {}
@@ -51,12 +51,14 @@ export function usePointerHandlers({
flatListRef,
itemWidthsRef,
currentIndexRef,
scrollTo,
onSettle,
imageCount,
}: {
flatListRef: React.RefObject<FlatList | null>
itemWidthsRef: React.RefObject<Map<number, number>>
currentIndexRef: React.RefObject<number>
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])
}