mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-29 18:01:18 -06:00
refactor AZScroller component for improved overlay handling and gesture management
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { LayoutChangeEvent, View as RNView } from 'react-native'
|
||||
import { View as RNView } from 'react-native'
|
||||
import { getToken, useTheme, View, YStack } from 'tamagui'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
@@ -7,6 +7,7 @@ import Animated, {
|
||||
useAnimatedStyle,
|
||||
runOnJS,
|
||||
withTiming,
|
||||
withDelay,
|
||||
} from 'react-native-reanimated'
|
||||
import { Text } from '../helpers/text'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
@@ -38,48 +39,59 @@ export default function AZScroller({
|
||||
|
||||
const alphabetSelectorRef = useRef<RNView>(null)
|
||||
|
||||
const alphabetSelectorTopY = useRef(0)
|
||||
const letterHeight = useRef(0)
|
||||
// Shared values for use on the UI thread
|
||||
const alphabetSelectorTopY = useSharedValue(0)
|
||||
const letterHeight = useSharedValue(0)
|
||||
const selectedLetter = useSharedValue('')
|
||||
|
||||
const [overlayLetter, setOverlayLetter] = useState('')
|
||||
|
||||
const showOverlay = () => {
|
||||
'worklet'
|
||||
overlayOpacity.value = withTiming(1)
|
||||
if (overlayOpacity.value !== 1) {
|
||||
overlayOpacity.value = withTiming(1)
|
||||
}
|
||||
}
|
||||
|
||||
const hideOverlay = () => {
|
||||
const hideOverlay = (delayMs: number = 200) => {
|
||||
'worklet'
|
||||
overlayOpacity.value = withTiming(0)
|
||||
overlayOpacity.value = withDelay(delayMs, withTiming(0))
|
||||
}
|
||||
|
||||
const getLetterFromAbsoluteY = (absoluteY: number) => {
|
||||
'worklet'
|
||||
if (letterHeight.value <= 0) return ''
|
||||
const relativeY = absoluteY - alphabetSelectorTopY.value
|
||||
const rawIndex = Math.floor(relativeY / letterHeight.value)
|
||||
const clampedIndex = Math.max(0, Math.min(alphabet.length - 1, rawIndex))
|
||||
return alphabet[clampedIndex] ?? ''
|
||||
}
|
||||
|
||||
const updateSelectionFromY = (absoluteY: number) => {
|
||||
'worklet'
|
||||
const letter = getLetterFromAbsoluteY(absoluteY)
|
||||
if (letter && letter !== selectedLetter.value) {
|
||||
selectedLetter.value = letter
|
||||
runOnJS(setOverlayLetter)(letter)
|
||||
showOverlay()
|
||||
} else if (letter) {
|
||||
showOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
const panGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.runOnJS(true)
|
||||
.minDistance(10)
|
||||
.hitSlop(8)
|
||||
.onBegin((e) => {
|
||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||
const index = Math.floor(relativeY / letterHeight.current)
|
||||
if (alphabet[index]) {
|
||||
const letter = alphabet[index]
|
||||
selectedLetter.value = letter
|
||||
setOverlayLetter(letter)
|
||||
runOnJS(showOverlay)()
|
||||
}
|
||||
updateSelectionFromY(e.absoluteY)
|
||||
})
|
||||
.onUpdate((e) => {
|
||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||
const index = Math.floor(relativeY / letterHeight.current)
|
||||
if (alphabet[index]) {
|
||||
const letter = alphabet[index]
|
||||
selectedLetter.value = letter
|
||||
setOverlayLetter(letter)
|
||||
runOnJS(showOverlay)()
|
||||
}
|
||||
updateSelectionFromY(e.absoluteY)
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(hideOverlay)()
|
||||
hideOverlay(200)
|
||||
if (selectedLetter.value) {
|
||||
runOnJS(onLetterSelect)(selectedLetter.value.toLowerCase())
|
||||
}
|
||||
@@ -90,37 +102,29 @@ export default function AZScroller({
|
||||
const tapGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Tap()
|
||||
.runOnJS(true)
|
||||
.maxDistance(10)
|
||||
.hitSlop(8)
|
||||
.onBegin((e) => {
|
||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||
const index = Math.floor(relativeY / letterHeight.current)
|
||||
if (alphabet[index]) {
|
||||
const letter = alphabet[index]
|
||||
selectedLetter.value = letter
|
||||
setOverlayLetter(letter)
|
||||
runOnJS(showOverlay)()
|
||||
}
|
||||
updateSelectionFromY(e.absoluteY)
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(hideOverlay)()
|
||||
hideOverlay(200)
|
||||
if (selectedLetter.value)
|
||||
runOnJS(onLetterSelect)(selectedLetter.value.toLowerCase())
|
||||
}),
|
||||
[onLetterSelect],
|
||||
)
|
||||
|
||||
const gesture = Gesture.Simultaneous(panGesture, tapGesture)
|
||||
// Prefer pan exclusively when movement occurs to avoid duplicate taps
|
||||
const gesture = Gesture.Exclusive(panGesture, tapGesture)
|
||||
|
||||
const animatedOverlayStyle = useAnimatedStyle(() => ({
|
||||
opacity: overlayOpacity.value,
|
||||
transform: [{ scale: overlayOpacity.value }],
|
||||
}))
|
||||
|
||||
const handleLetterLayout = (event: LayoutChangeEvent) => {
|
||||
letterHeight.current = event.nativeEvent.layout.height
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Keep haptics behavior as-is per request
|
||||
trigger('impactLight')
|
||||
}, [overlayLetter])
|
||||
|
||||
@@ -135,37 +139,33 @@ export default function AZScroller({
|
||||
justifyContent='flex-start'
|
||||
alignItems='center'
|
||||
alignContent='center'
|
||||
accessible
|
||||
accessibilityRole='adjustable'
|
||||
accessibilityLabel='Alphabet scroller'
|
||||
onLayout={() => {
|
||||
requestAnimationFrame(() => {
|
||||
alphabetSelectorRef.current?.measureInWindow((x, y, width, height) => {
|
||||
alphabetSelectorTopY.current = y
|
||||
alphabetSelectorRef.current?.measureInWindow((x, y, w, h) => {
|
||||
alphabetSelectorTopY.value = y
|
||||
if (h && h > 0) {
|
||||
letterHeight.value = h / alphabet.length
|
||||
}
|
||||
})
|
||||
})
|
||||
}}
|
||||
ref={alphabetSelectorRef}
|
||||
>
|
||||
{alphabet.map((letter, index) => {
|
||||
const letterElement = (
|
||||
<Text
|
||||
key={letter}
|
||||
fontSize='$6'
|
||||
textAlign='center'
|
||||
color='$neutral'
|
||||
height={'$1'}
|
||||
userSelect='none'
|
||||
>
|
||||
{letter}
|
||||
</Text>
|
||||
)
|
||||
|
||||
return index === 0 ? (
|
||||
<View height={'$1'} key={letter} onLayout={handleLetterLayout}>
|
||||
{letterElement}
|
||||
</View>
|
||||
) : (
|
||||
letterElement
|
||||
)
|
||||
})}
|
||||
{alphabet.map((letter) => (
|
||||
<Text
|
||||
key={letter}
|
||||
fontSize='$6'
|
||||
textAlign='center'
|
||||
color='$neutral'
|
||||
height={'$1'}
|
||||
userSelect='none'
|
||||
>
|
||||
{letter}
|
||||
</Text>
|
||||
))}
|
||||
</YStack>
|
||||
</GestureDetector>
|
||||
|
||||
@@ -179,6 +179,7 @@ export default function AZScroller({
|
||||
width: getToken('$13'),
|
||||
height: getToken('$13'),
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.background.val,
|
||||
borderRadius: getToken('$4'),
|
||||
borderWidth: getToken('$1'),
|
||||
@@ -193,7 +194,6 @@ export default function AZScroller({
|
||||
textAlign: 'center',
|
||||
fontFamily: 'Figtree-Bold',
|
||||
color: theme.primary.val,
|
||||
marginHorizontal: 'auto',
|
||||
}}
|
||||
>
|
||||
{overlayLetter}
|
||||
@@ -206,15 +206,20 @@ export default function AZScroller({
|
||||
export const alphabeticalSelectorCallback = async (
|
||||
letter: string,
|
||||
pageParams: RefObject<Set<string>>,
|
||||
{
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isPending,
|
||||
}: UseInfiniteQueryResult<BaseItemDto[] | (string | number | BaseItemDto)[], Error>,
|
||||
infiniteQuery: UseInfiniteQueryResult<BaseItemDto[] | (string | number | BaseItemDto)[], Error>,
|
||||
) => {
|
||||
while (!pageParams.current.has(letter.toUpperCase()) && hasNextPage) {
|
||||
const target = letter.toUpperCase()
|
||||
let safety = 0
|
||||
const MAX_PAGES = 100
|
||||
while (!pageParams.current.has(target) && infiniteQuery.hasNextPage && safety < MAX_PAGES) {
|
||||
console.debug(`Fetching next page for alphabet selection`)
|
||||
await fetchNextPage()
|
||||
await infiniteQuery.fetchNextPage()
|
||||
safety += 1
|
||||
}
|
||||
if (!pageParams.current.has(target) && safety >= MAX_PAGES) {
|
||||
console.warn(
|
||||
`Alphabetical Selector: reached page fetch cap (${MAX_PAGES}) without finding ${target}`,
|
||||
)
|
||||
}
|
||||
console.debug(`Alphabetical Selector Callback: ${letter} complete`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user