diff options
Diffstat (limited to 'src/view/com/pager')
-rw-r--r-- | src/view/com/pager/DraggableScrollView.tsx | 15 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 11 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 74 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 125 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 99 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 202 |
6 files changed, 271 insertions, 255 deletions
diff --git a/src/view/com/pager/DraggableScrollView.tsx b/src/view/com/pager/DraggableScrollView.tsx new file mode 100644 index 000000000..4b7396eaa --- /dev/null +++ b/src/view/com/pager/DraggableScrollView.tsx @@ -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} /> +}) diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 0fc1b7310..0df915950 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useMemo} from 'react' import {Animated, StyleSheet} from 'react-native' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' @@ -27,6 +27,10 @@ const FeedsTabBarDesktop = observer( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) => { const store = useStores() + const items = useMemo( + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], + [store.me.savedFeeds.pinnedFeedNames], + ) const pal = usePalette('default') const interp = useAnimatedValue(0) @@ -44,13 +48,14 @@ const FeedsTabBarDesktop = observer( {translateY: Animated.multiply(interp, -100)}, ], } + return ( // @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 + key={items.join(',')} {...props} - items={['Following', "What's hot"]} - indicatorPosition="bottom" + items={items} indicatorColor={pal.colors.link} /> </Animated.View> diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index b42ffe726..9c7138815 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -1,12 +1,17 @@ -import React from 'react' -import {Animated, StyleSheet, TouchableOpacity} from 'react-native' +import React, {useMemo} from 'react' +import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' -import {UserAvatar} from '../util/UserAvatar' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {Link} from '../util/Link' +import {Text} from '../util/text/Text' +import {CogIcon} from 'lib/icons' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {s} from 'lib/styles' export const FeedsTabBar = observer( ( @@ -28,25 +33,51 @@ export const FeedsTabBar = observer( transform: [{translateY: Animated.multiply(interp, -100)}], } + const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) + const onPressAvi = React.useCallback(() => { store.shell.openDrawer() }, [store]) + const items = useMemo( + () => ['Following', ...store.me.savedFeeds.pinnedFeedNames], + [store.me.savedFeeds.pinnedFeedNames], + ) + return ( <Animated.View style={[pal.view, pal.border, styles.tabBar, transform]}> - <TouchableOpacity - testID="viewHeaderDrawerBtn" - style={styles.tabBarAvi} - onPress={onPressAvi} - accessibilityRole="button" - accessibilityLabel="Menu" - accessibilityHint="Access navigation links and settings"> - <UserAvatar avatar={store.me.avatar} size={30} /> - </TouchableOpacity> + <View style={[pal.view, styles.topBar]}> + <View style={[pal.view]}> + <TouchableOpacity + testID="viewHeaderDrawerBtn" + onPress={onPressAvi} + accessibilityRole="button" + accessibilityLabel="Open navigation" + accessibilityHint="Access profile and other navigation links" + hitSlop={10}> + <FontAwesomeIcon + icon="bars" + size={18} + color={pal.colors.textLight} + /> + </TouchableOpacity> + </View> + <Text style={[brandBlue, s.bold, styles.title]}>Bluesky</Text> + <View style={[pal.view]}> + <Link + href="/settings/saved-feeds" + hitSlop={10} + accessibilityRole="button" + accessibilityLabel="Edit Saved Feeds" + accessibilityHint="Opens screen to edit Saved Feeds"> + <CogIcon size={21} strokeWidth={2} style={pal.textLight} /> + </Link> + </View> + </View> <TabBar + key={items.join(',')} {...props} - items={['Following', "What's hot"]} - indicatorPosition="bottom" + items={items} indicatorColor={pal.colors.link} /> </Animated.View> @@ -61,13 +92,20 @@ const styles = StyleSheet.create({ left: 0, right: 0, top: 0, + flexDirection: 'column', + alignItems: 'center', + borderBottomWidth: 1, + }, + topBar: { flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 18, - borderBottomWidth: 1, + paddingTop: 8, + paddingBottom: 2, + width: '100%', }, - tabBarAvi: { - marginTop: 1, - marginRight: 18, + title: { + fontSize: 21, }, }) diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 34747db6d..e2c8bf6d2 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -1,16 +1,17 @@ -import React from 'react' +import React, {forwardRef} from 'react' import {Animated, View} from 'react-native' import PagerView, {PagerViewOnPageSelectedEvent} from 'react-native-pager-view' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {s} from 'lib/styles' export type PageSelectedEvent = PagerViewOnPageSelectedEvent const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) +export interface PagerRef { + setPage: (index: number) => void +} + export interface RenderTabBarFnProps { selectedPage: number - position: Animated.Value - offset: Animated.Value onSelect?: (index: number) => void } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element @@ -22,68 +23,60 @@ interface Props { onPageSelected?: (index: number) => void testID?: string } -export const Pager = ({ - children, - tabBarPosition = 'top', - initialPage = 0, - renderTabBar, - onPageSelected, - testID, -}: React.PropsWithChildren<Props>) => { - const [selectedPage, setSelectedPage] = React.useState(0) - const position = useAnimatedValue(0) - const offset = useAnimatedValue(0) - const pagerView = React.useRef<PagerView>() +export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( + ( + { + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, + testID, + }: React.PropsWithChildren<Props>, + ref, + ) => { + const [selectedPage, setSelectedPage] = React.useState(0) + const pagerView = React.useRef<PagerView>() - const onPageSelectedInner = React.useCallback( - (e: PageSelectedEvent) => { - setSelectedPage(e.nativeEvent.position) - onPageSelected?.(e.nativeEvent.position) - }, - [setSelectedPage, onPageSelected], - ) + React.useImperativeHandle(ref, () => ({ + setPage: (index: number) => pagerView.current?.setPage(index), + })) - const onTabBarSelect = React.useCallback( - (index: number) => { - pagerView.current?.setPage(index) - }, - [pagerView], - ) + const onPageSelectedInner = React.useCallback( + (e: PageSelectedEvent) => { + setSelectedPage(e.nativeEvent.position) + onPageSelected?.(e.nativeEvent.position) + }, + [setSelectedPage, onPageSelected], + ) - return ( - <View testID={testID}> - {tabBarPosition === 'top' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - <AnimatedPagerView - ref={pagerView} - style={s.h100pct} - initialPage={initialPage} - onPageSelected={onPageSelectedInner} - onPageScroll={Animated.event( - [ - { - nativeEvent: { - position: position, - offset: offset, - }, - }, - ], - {useNativeDriver: true}, - )}> - {children} - </AnimatedPagerView> - {tabBarPosition === 'bottom' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - </View> - ) -} + const onTabBarSelect = React.useCallback( + (index: number) => { + pagerView.current?.setPage(index) + }, + [pagerView], + ) + + return ( + <View testID={testID}> + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + <AnimatedPagerView + ref={pagerView} + style={s.h100pct} + initialPage={initialPage} + onPageSelected={onPageSelectedInner}> + {children} + </AnimatedPagerView> + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + </View> + ) + }, +) diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index 107497f6f..7be2b11ec 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -1,12 +1,9 @@ import React from 'react' -import {Animated, View} from 'react-native' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' +import {View} from 'react-native' import {s} from 'lib/styles' export interface RenderTabBarFnProps { selectedPage: number - position: Animated.Value - offset: Animated.Value onSelect?: (index: number) => void } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element @@ -17,53 +14,51 @@ interface Props { renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void } -export const Pager = ({ - children, - tabBarPosition = 'top', - initialPage = 0, - renderTabBar, - onPageSelected, -}: React.PropsWithChildren<Props>) => { - const [selectedPage, setSelectedPage] = React.useState(initialPage) - const position = useAnimatedValue(0) - const offset = useAnimatedValue(0) +export const Pager = React.forwardRef( + ( + { + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, + }: React.PropsWithChildren<Props>, + ref, + ) => { + const [selectedPage, setSelectedPage] = React.useState(initialPage) - const onTabBarSelect = React.useCallback( - (index: number) => { - setSelectedPage(index) - onPageSelected?.(index) - Animated.timing(position, { - toValue: index, - duration: 200, - useNativeDriver: true, - }).start() - }, - [setSelectedPage, onPageSelected, position], - ) + React.useImperativeHandle(ref, () => ({ + setPage: (index: number) => setSelectedPage(index), + })) - return ( - <View> - {tabBarPosition === 'top' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - {React.Children.map(children, (child, i) => ( - <View - style={selectedPage === i ? undefined : s.hidden} - key={`page-${i}`}> - {child} - </View> - ))} - {tabBarPosition === 'bottom' && - renderTabBar({ - selectedPage, - position, - offset, - onSelect: onTabBarSelect, - })} - </View> - ) -} + const onTabBarSelect = React.useCallback( + (index: number) => { + setSelectedPage(index) + onPageSelected?.(index) + }, + [setSelectedPage, onPageSelected], + ) + + return ( + <View> + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + {React.Children.map(children, (child, i) => ( + <View + style={selectedPage === i ? undefined : s.hidden} + key={`page-${i}`}> + {child} + </View> + ))} + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + </View> + ) + }, +) diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index a0b72a93f..d7121fde9 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -1,22 +1,22 @@ -import React, {createRef, useState, useMemo, useRef} from 'react' -import {Animated, StyleSheet, View} from 'react-native' +import React, { + useRef, + createRef, + useMemo, + useEffect, + useState, + useCallback, +} from 'react' +import {StyleSheet, View, ScrollView} from 'react-native' import {Text} from '../util/text/Text' import {PressableWithHover} from '../util/PressableWithHover' import {usePalette} from 'lib/hooks/usePalette' -import {isDesktopWeb} from 'platform/detection' - -interface Layout { - x: number - width: number -} +import {isDesktopWeb, isMobileWeb} from 'platform/detection' +import {DraggableScrollView} from './DraggableScrollView' export interface TabBarProps { testID?: string selectedPage: number items: string[] - position: Animated.Value - offset: Animated.Value - indicatorPosition?: 'top' | 'bottom' indicatorColor?: string onSelect?: (index: number) => void onPressSelected?: () => void @@ -26,105 +26,81 @@ export function TabBar({ testID, selectedPage, items, - position, - offset, - indicatorPosition = 'bottom', indicatorColor, onSelect, onPressSelected, }: TabBarProps) { const pal = usePalette('default') - const [itemLayouts, setItemLayouts] = useState<Layout[]>( - items.map(() => ({x: 0, width: 0})), - ) + const scrollElRef = useRef<ScrollView>(null) + const [itemXs, setItemXs] = useState<number[]>([]) const itemRefs = useMemo( () => Array.from({length: items.length}).map(() => createRef<View>()), [items.length], ) - const panX = Animated.add(position, offset) - const containerRef = useRef<View>(null) + const indicatorStyle = useMemo( + () => ({borderBottomColor: indicatorColor || pal.colors.link}), + [indicatorColor, pal], + ) - const indicatorStyle = { - backgroundColor: indicatorColor || pal.colors.link, - bottom: - indicatorPosition === 'bottom' ? (isDesktopWeb ? 0 : -1) : undefined, - top: indicatorPosition === 'top' ? (isDesktopWeb ? 0 : -1) : undefined, - transform: [ - { - translateX: panX.interpolate({ - inputRange: items.map((_item, i) => i), - outputRange: itemLayouts.map(l => l.x + l.width / 2), - }), - }, - { - scaleX: panX.interpolate({ - inputRange: items.map((_item, i) => i), - outputRange: itemLayouts.map(l => l.width), - }), - }, - ], - } + useEffect(() => { + scrollElRef.current?.scrollTo({x: itemXs[selectedPage] || 0}) + }, [scrollElRef, itemXs, selectedPage]) + + const onPressItem = useCallback( + (index: number) => { + onSelect?.(index) + if (index === selectedPage) { + onPressSelected?.() + } + }, + [onSelect, onPressSelected, selectedPage], + ) const onLayout = React.useCallback(() => { const promises = [] for (let i = 0; i < items.length; i++) { promises.push( - new Promise<Layout>(resolve => { - if (!containerRef.current || !itemRefs[i].current) { - return resolve({x: 0, width: 0}) + new Promise<number>(resolve => { + if (!itemRefs[i].current) { + return resolve(0) } - itemRefs[i].current?.measureLayout( - containerRef.current, - (x: number, _y: number, width: number) => { - resolve({x, width}) - }, - ) + itemRefs[i].current?.measure((x: number) => resolve(x)) }), ) } - Promise.all(promises).then((layouts: Layout[]) => { - setItemLayouts(layouts) + Promise.all(promises).then((Xs: number[]) => { + setItemXs(Xs) }) - }, [containerRef, itemRefs, setItemLayouts, items.length]) - - const onPressItem = React.useCallback( - (index: number) => { - onSelect?.(index) - if (index === selectedPage) { - onPressSelected?.() - } - }, - [onSelect, onPressSelected, selectedPage], - ) + }, [itemRefs, setItemXs, items.length]) return ( - <View - testID={testID} - style={[pal.view, styles.outer]} - onLayout={onLayout} - ref={containerRef}> - <Animated.View style={[styles.indicator, indicatorStyle]} /> - {items.map((item, i) => { - const selected = i === selectedPage - return ( - <PressableWithHover - ref={itemRefs[i]} - key={item} - style={ - indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom - } - hoverStyle={pal.viewLight} - onPress={() => onPressItem(i)}> - <Text - type="xl-bold" - testID={testID ? `${testID}-${item}` : undefined} - style={selected ? pal.text : pal.textLight}> - {item} - </Text> - </PressableWithHover> - ) - })} + <View testID={testID} style={[pal.view, styles.outer]}> + <DraggableScrollView + horizontal={true} + showsHorizontalScrollIndicator={false} + ref={scrollElRef} + contentContainerStyle={styles.contentContainer} + onLayout={onLayout}> + {items.map((item, i) => { + const selected = i === selectedPage + return ( + <PressableWithHover + ref={itemRefs[i]} + key={item} + style={[styles.item, selected && indicatorStyle]} + hoverStyle={pal.viewLight} + onPress={() => onPressItem(i)}> + <Text + type={isDesktopWeb ? 'xl-bold' : 'lg-bold'} + testID={testID ? `${testID}-${item}` : undefined} + style={selected ? pal.text : pal.textLight}> + {item} + </Text> + </PressableWithHover> + ) + })} + </DraggableScrollView> </View> ) } @@ -133,45 +109,39 @@ const styles = isDesktopWeb ? StyleSheet.create({ outer: { flexDirection: 'row', - paddingHorizontal: 18, + width: 598, }, - itemTop: { - paddingTop: 16, - paddingBottom: 14, - paddingHorizontal: 12, + contentContainer: { + columnGap: 8, + marginLeft: 14, + paddingRight: 14, + backgroundColor: 'transparent', }, - itemBottom: { + item: { paddingTop: 14, - paddingBottom: 16, - paddingHorizontal: 12, - }, - indicator: { - position: 'absolute', - left: 0, - width: 1, - height: 3, - zIndex: 1, + paddingBottom: 12, + paddingHorizontal: 10, + borderBottomWidth: 3, + borderBottomColor: 'transparent', }, }) : StyleSheet.create({ outer: { + flex: 1, flexDirection: 'row', - paddingHorizontal: 14, + backgroundColor: 'transparent', }, - itemTop: { + contentContainer: { + columnGap: isMobileWeb ? 0 : 20, + marginLeft: isMobileWeb ? 0 : 18, + paddingRight: isMobileWeb ? 0 : 36, + backgroundColor: 'transparent', + }, + item: { paddingTop: 10, paddingBottom: 10, - marginRight: 24, - }, - itemBottom: { - paddingTop: 8, - paddingBottom: 12, - marginRight: 24, - }, - indicator: { - position: 'absolute', - left: 0, - width: 1, - height: 3, + paddingHorizontal: isMobileWeb ? 8 : 0, + borderBottomWidth: 3, + borderBottomColor: 'transparent', }, }) |