Add keyboard handling
This commit is contained in:
@@ -27,6 +27,7 @@ import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||||
|
import {useKeyboardHandlers} from '#/components/images/Gallery/useKeyboardHandlers'
|
||||||
|
|
||||||
const CONTAINER_ASPECT_RATIO = 3 / 2
|
const CONTAINER_ASPECT_RATIO = 3 / 2
|
||||||
const ITEM_GAP = 8 // tokens.space.sm
|
const ITEM_GAP = 8 // tokens.space.sm
|
||||||
@@ -127,6 +128,30 @@ export function Gallery({
|
|||||||
const width = bleedWidth || Math.min(600, window.width)
|
const width = bleedWidth || Math.min(600, window.width)
|
||||||
/* End container overflow styles */
|
/* End container overflow styles */
|
||||||
|
|
||||||
|
const flatListRef = useRef<FlatList>(null)
|
||||||
|
const itemWidthsRef = useRef<Map<number, number>>(new Map())
|
||||||
|
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})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
@@ -140,6 +165,7 @@ export function Gallery({
|
|||||||
onLayout={measure}>
|
onLayout={measure}>
|
||||||
<BlockDrawerGesture>
|
<BlockDrawerGesture>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={flatListRef}
|
||||||
horizontal
|
horizontal
|
||||||
pagingEnabled={false}
|
pagingEnabled={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -150,8 +176,39 @@ export function Gallery({
|
|||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
data={images}
|
data={images}
|
||||||
keyExtractor={item => item.thumb}
|
keyExtractor={item => item.thumb}
|
||||||
renderItem={({item}) => {
|
renderItem={({item, index}) => {
|
||||||
return <GalleryImage image={item} contentHeight={contentHeight} />
|
return (
|
||||||
|
<GalleryImage
|
||||||
|
image={item}
|
||||||
|
contentHeight={contentHeight}
|
||||||
|
index={index}
|
||||||
|
onWidthChange={(i, w) => {
|
||||||
|
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={[
|
style={[
|
||||||
{
|
{
|
||||||
@@ -200,9 +257,15 @@ function computeDims({
|
|||||||
function GalleryImage({
|
function GalleryImage({
|
||||||
contentHeight: height,
|
contentHeight: height,
|
||||||
image,
|
image,
|
||||||
|
index,
|
||||||
|
onWidthChange,
|
||||||
|
itemRef,
|
||||||
}: {
|
}: {
|
||||||
contentHeight: number
|
contentHeight: number
|
||||||
image: AppBskyEmbedImages.ViewImage
|
image: AppBskyEmbedImages.ViewImage
|
||||||
|
index: number
|
||||||
|
onWidthChange: (index: number, width: number) => void
|
||||||
|
itemRef: (node: View | null) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [aspectRatio, setAspectRatio] = useState(() =>
|
const [aspectRatio, setAspectRatio] = useState(() =>
|
||||||
@@ -210,8 +273,11 @@ function GalleryImage({
|
|||||||
)
|
)
|
||||||
const dims = computeDims({height, aspectRatio})
|
const dims = computeDims({height, aspectRatio})
|
||||||
|
|
||||||
|
onWidthChange(index, dims.width)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
|
ref={itemRef}
|
||||||
style={[a.rounded_md, a.overflow_hidden, t.atoms.bg_contrast_25]}>
|
style={[a.rounded_md, a.overflow_hidden, t.atoms.bg_contrast_25]}>
|
||||||
<Image
|
<Image
|
||||||
source={{uri: image.thumb}}
|
source={{uri: image.thumb}}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function useKeyboardHandlers(_args: {
|
||||||
|
flatListRef: any
|
||||||
|
currentIndexRef: any
|
||||||
|
scrollToIndex: any
|
||||||
|
imageCount: any
|
||||||
|
}) {}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {useEffect} from 'react'
|
||||||
|
import {type FlatList} from 'react-native'
|
||||||
|
|
||||||
|
export function useKeyboardHandlers({
|
||||||
|
flatListRef,
|
||||||
|
currentIndexRef,
|
||||||
|
scrollToIndex,
|
||||||
|
imageCount,
|
||||||
|
}: {
|
||||||
|
flatListRef: React.RefObject<FlatList | null>
|
||||||
|
currentIndexRef: React.RefObject<number>
|
||||||
|
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])
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user