make tab bar scroll view draggable on web

zio/stable
Ansh Nanda 2023-05-24 15:04:30 -07:00
parent 7e555ecc1b
commit 32c9dabb74
5 changed files with 130 additions and 3 deletions

View File

@ -0,0 +1,84 @@
import {useEffect, useRef, useMemo, ForwardedRef} from 'react'
import {Platform, findNodeHandle} from 'react-native'
import type {ScrollView} from 'react-native'
import {mergeRefs} from 'lib/merge-refs'
type Props<Scrollable extends ScrollView = ScrollView> = {
cursor?: string
outerRef?: ForwardedRef<Scrollable>
}
export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({
outerRef,
cursor = 'grab',
}: Props<Scrollable> = {}) {
const ref = useRef<Scrollable>(null)
useEffect(() => {
if (Platform.OS !== 'web' || !ref.current) {
return
}
const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement
if (!slider) {
return
}
let isDragging = false
let isMouseDown = false
let startX = 0
let scrollLeft = 0
const mouseDown = (e: MouseEvent) => {
isMouseDown = true
startX = e.pageX - slider.offsetLeft
scrollLeft = slider.scrollLeft
slider.style.cursor = cursor
}
const mouseUp = () => {
if (isDragging) {
slider.addEventListener('click', e => e.stopPropagation(), {once: true})
}
isMouseDown = false
isDragging = false
slider.style.cursor = 'default'
}
const mouseMove = (e: MouseEvent) => {
if (!isMouseDown) {
return
}
// Require n pixels momement before start of drag (3 in this case )
const x = e.pageX - slider.offsetLeft
if (Math.abs(x - startX) < 3) {
return
}
isDragging = true
e.preventDefault()
const walk = x - startX
slider.scrollLeft = scrollLeft - walk
}
slider.addEventListener('mousedown', mouseDown)
window.addEventListener('mouseup', mouseUp)
window.addEventListener('mousemove', mouseMove)
return () => {
slider.removeEventListener('mousedown', mouseDown)
window.removeEventListener('mouseup', mouseUp)
window.removeEventListener('mousemove', mouseMove)
}
}, [cursor])
const refs = useMemo(
() => mergeRefs(outerRef ? [ref, outerRef] : [ref]),
[ref, outerRef],
)
return {
refs,
}
}

View File

@ -0,0 +1,27 @@
/**
* This TypeScript function merges multiple React refs into a single ref callback.
* When developing low level UI components, it is common to have to use a local ref
* but also support an external one using React.forwardRef.
* Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility.
* Today a ref can be a function or an object, tomorrow it could be another thing, who knows.
* This utility handles compatibility for you.
* This function is inspired by https://github.com/gregberge/react-merge-refs
* @param refs - An array of React refs, which can be either `React.MutableRefObject<T>` or
* `React.LegacyRef<T>`. These refs are used to store references to DOM elements or React components.
* The `mergeRefs` function takes in an array of these refs and returns a callback function that
* @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and
* returns a ref callback function that can be used to merge multiple refs into a single ref.
*/
export function mergeRefs<T = any>(
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>,
): React.RefCallback<T> {
return value => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
;(ref as React.MutableRefObject<T | null>).current = value
}
})
}
}

View File

@ -0,0 +1,15 @@
import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView'
import React, {ComponentProps} from 'react'
import {ScrollView} from 'react-native'
export const DraggableScrollView = React.forwardRef<
ScrollView,
ComponentProps<typeof ScrollView>
>(function DraggableScrollView(props, ref) {
const {refs} = useDraggableScroll<ScrollView>({
outerRef: ref,
cursor: 'grab', // optional, default
})
return <ScrollView ref={refs} horizontal {...props} />
})

View File

@ -53,8 +53,8 @@ const FeedsTabBarDesktop = observer(
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
<Animated.View style={[pal.view, styles.tabBar, transform]}> <Animated.View style={[pal.view, styles.tabBar, transform]}>
<TabBar <TabBar
{...props}
key={items.join(',')} key={items.join(',')}
{...props}
items={items} items={items}
indicatorColor={pal.colors.link} indicatorColor={pal.colors.link}
/> />

View File

@ -11,6 +11,7 @@ import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover' import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection' import {isDesktopWeb} from 'platform/detection'
import {DraggableScrollView} from './DraggableScrollView'
export interface TabBarProps { export interface TabBarProps {
testID?: string testID?: string
@ -75,7 +76,7 @@ export function TabBar({
return ( return (
<View testID={testID} style={[pal.view, styles.outer]}> <View testID={testID} style={[pal.view, styles.outer]}>
<ScrollView <DraggableScrollView
horizontal={true} horizontal={true}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
ref={scrollElRef} ref={scrollElRef}
@ -98,7 +99,7 @@ export function TabBar({
</PressableWithHover> </PressableWithHover>
) )
})} })}
</ScrollView> </DraggableScrollView>
</View> </View>
) )
} }