make tab bar scroll view draggable on web
parent
7e555ecc1b
commit
32c9dabb74
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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} />
|
||||
})
|
|
@ -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
|
||||
<Animated.View style={[pal.view, styles.tabBar, transform]}>
|
||||
<TabBar
|
||||
{...props}
|
||||
key={items.join(',')}
|
||||
{...props}
|
||||
items={items}
|
||||
indicatorColor={pal.colors.link}
|
||||
/>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {Text} from '../util/text/Text'
|
|||
import {PressableWithHover} from '../util/PressableWithHover'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isDesktopWeb} from 'platform/detection'
|
||||
import {DraggableScrollView} from './DraggableScrollView'
|
||||
|
||||
export interface TabBarProps {
|
||||
testID?: string
|
||||
|
@ -75,7 +76,7 @@ export function TabBar({
|
|||
|
||||
return (
|
||||
<View testID={testID} style={[pal.view, styles.outer]}>
|
||||
<ScrollView
|
||||
<DraggableScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
ref={scrollElRef}
|
||||
|
@ -98,7 +99,7 @@ export function TabBar({
|
|||
</PressableWithHover>
|
||||
)
|
||||
})}
|
||||
</ScrollView>
|
||||
</DraggableScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue