my fork of the bluesky client
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

make tab bar scroll view draggable on web

+130 -3
+84
src/lib/hooks/useDraggableScrollView.ts
··· 1 + import {useEffect, useRef, useMemo, ForwardedRef} from 'react' 2 + import {Platform, findNodeHandle} from 'react-native' 3 + import type {ScrollView} from 'react-native' 4 + import {mergeRefs} from 'lib/merge-refs' 5 + 6 + type Props<Scrollable extends ScrollView = ScrollView> = { 7 + cursor?: string 8 + outerRef?: ForwardedRef<Scrollable> 9 + } 10 + 11 + export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({ 12 + outerRef, 13 + cursor = 'grab', 14 + }: Props<Scrollable> = {}) { 15 + const ref = useRef<Scrollable>(null) 16 + 17 + useEffect(() => { 18 + if (Platform.OS !== 'web' || !ref.current) { 19 + return 20 + } 21 + const slider = findNodeHandle(ref.current) as unknown as HTMLDivElement 22 + if (!slider) { 23 + return 24 + } 25 + let isDragging = false 26 + let isMouseDown = false 27 + let startX = 0 28 + let scrollLeft = 0 29 + 30 + const mouseDown = (e: MouseEvent) => { 31 + isMouseDown = true 32 + startX = e.pageX - slider.offsetLeft 33 + scrollLeft = slider.scrollLeft 34 + 35 + slider.style.cursor = cursor 36 + } 37 + 38 + const mouseUp = () => { 39 + if (isDragging) { 40 + slider.addEventListener('click', e => e.stopPropagation(), {once: true}) 41 + } 42 + 43 + isMouseDown = false 44 + isDragging = false 45 + slider.style.cursor = 'default' 46 + } 47 + 48 + const mouseMove = (e: MouseEvent) => { 49 + if (!isMouseDown) { 50 + return 51 + } 52 + 53 + // Require n pixels momement before start of drag (3 in this case ) 54 + const x = e.pageX - slider.offsetLeft 55 + if (Math.abs(x - startX) < 3) { 56 + return 57 + } 58 + 59 + isDragging = true 60 + e.preventDefault() 61 + const walk = x - startX 62 + slider.scrollLeft = scrollLeft - walk 63 + } 64 + 65 + slider.addEventListener('mousedown', mouseDown) 66 + window.addEventListener('mouseup', mouseUp) 67 + window.addEventListener('mousemove', mouseMove) 68 + 69 + return () => { 70 + slider.removeEventListener('mousedown', mouseDown) 71 + window.removeEventListener('mouseup', mouseUp) 72 + window.removeEventListener('mousemove', mouseMove) 73 + } 74 + }, [cursor]) 75 + 76 + const refs = useMemo( 77 + () => mergeRefs(outerRef ? [ref, outerRef] : [ref]), 78 + [ref, outerRef], 79 + ) 80 + 81 + return { 82 + refs, 83 + } 84 + }
+27
src/lib/merge-refs.ts
··· 1 + /** 2 + * This TypeScript function merges multiple React refs into a single ref callback. 3 + * When developing low level UI components, it is common to have to use a local ref 4 + * but also support an external one using React.forwardRef. 5 + * Natively, React does not offer a way to set two refs inside the ref property. This is the goal of this small utility. 6 + * Today a ref can be a function or an object, tomorrow it could be another thing, who knows. 7 + * This utility handles compatibility for you. 8 + * This function is inspired by https://github.com/gregberge/react-merge-refs 9 + * @param refs - An array of React refs, which can be either `React.MutableRefObject<T>` or 10 + * `React.LegacyRef<T>`. These refs are used to store references to DOM elements or React components. 11 + * The `mergeRefs` function takes in an array of these refs and returns a callback function that 12 + * @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and 13 + * returns a ref callback function that can be used to merge multiple refs into a single ref. 14 + */ 15 + export function mergeRefs<T = any>( 16 + refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>, 17 + ): React.RefCallback<T> { 18 + return value => { 19 + refs.forEach(ref => { 20 + if (typeof ref === 'function') { 21 + ref(value) 22 + } else if (ref != null) { 23 + ;(ref as React.MutableRefObject<T | null>).current = value 24 + } 25 + }) 26 + } 27 + }
+15
src/view/com/pager/DraggableScrollView.tsx
··· 1 + import {useDraggableScroll} from 'lib/hooks/useDraggableScrollView' 2 + import React, {ComponentProps} from 'react' 3 + import {ScrollView} from 'react-native' 4 + 5 + export const DraggableScrollView = React.forwardRef< 6 + ScrollView, 7 + ComponentProps<typeof ScrollView> 8 + >(function DraggableScrollView(props, ref) { 9 + const {refs} = useDraggableScroll<ScrollView>({ 10 + outerRef: ref, 11 + cursor: 'grab', // optional, default 12 + }) 13 + 14 + return <ScrollView ref={refs} horizontal {...props} /> 15 + })
+1 -1
src/view/com/pager/FeedsTabBar.web.tsx
··· 53 53 // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf 54 54 <Animated.View style={[pal.view, styles.tabBar, transform]}> 55 55 <TabBar 56 - {...props} 57 56 key={items.join(',')} 57 + {...props} 58 58 items={items} 59 59 indicatorColor={pal.colors.link} 60 60 />
+3 -2
src/view/com/pager/TabBar.tsx
··· 11 11 import {PressableWithHover} from '../util/PressableWithHover' 12 12 import {usePalette} from 'lib/hooks/usePalette' 13 13 import {isDesktopWeb} from 'platform/detection' 14 + import {DraggableScrollView} from './DraggableScrollView' 14 15 15 16 export interface TabBarProps { 16 17 testID?: string ··· 75 76 76 77 return ( 77 78 <View testID={testID} style={[pal.view, styles.outer]}> 78 - <ScrollView 79 + <DraggableScrollView 79 80 horizontal={true} 80 81 showsHorizontalScrollIndicator={false} 81 82 ref={scrollElRef} ··· 98 99 </PressableWithHover> 99 100 ) 100 101 })} 101 - </ScrollView> 102 + </DraggableScrollView> 102 103 </View> 103 104 ) 104 105 }