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
|
// @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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue