diff options
Diffstat (limited to 'src/view/com/pager')
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 11 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 26 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 1 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 51 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 15 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.web.tsx | 194 |
6 files changed, 261 insertions, 37 deletions
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 57c83f17c..385da5544 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -117,7 +117,7 @@ function FeedsTabBarTablet( 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, headerMinimalShellTransform]} + style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]} onLayout={e => { headerHeight.value = e.nativeEvent.layout.height }}> @@ -134,13 +134,16 @@ function FeedsTabBarTablet( const styles = StyleSheet.create({ tabBar: { - position: 'absolute', + // @ts-ignore Web only + position: 'sticky', zIndex: 1, // @ts-ignore Web only -prf - left: 'calc(50% - 299px)', - width: 598, + left: 'calc(50% - 300px)', + width: 600, top: 0, flexDirection: 'row', alignItems: 'center', + borderLeftWidth: 1, + borderRightWidth: 1, }, }) diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 2c5ba5dfb..b9959a6d9 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -20,6 +20,11 @@ import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {Logo} from '#/view/icons/Logo' +import {IS_DEV} from '#/env' +import {atoms} from '#/alf' +import {Link as Link2} from '#/components/Link' +import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette' + export function FeedsTabBar( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { @@ -68,7 +73,7 @@ export function FeedsTabBar( headerHeight.value = e.nativeEvent.layout.height }}> <View style={[pal.view, styles.topBar]}> - <View style={[pal.view]}> + <View style={[pal.view, {width: 100}]}> <TouchableOpacity testID="viewHeaderDrawerBtn" onPress={onPressAvi} @@ -88,7 +93,21 @@ export function FeedsTabBar( <View> <Logo width={30} /> </View> - <View style={[pal.view, {width: 18}]}> + <View + style={[ + atoms.flex_row, + atoms.justify_end, + atoms.align_center, + atoms.gap_md, + pal.view, + {width: 100}, + ]}> + {IS_DEV && ( + <Link2 to="/sys/debug"> + <ColorPalette size="md" /> + </Link2> + )} + {hasSession && ( <Link testID="viewHeaderHomeFeedPrefsBtn" @@ -123,7 +142,8 @@ export function FeedsTabBar( const styles = StyleSheet.create({ tabBar: { - position: 'absolute', + // @ts-ignore web-only + position: isWeb ? 'fixed' : 'absolute', zIndex: 1, left: 0, right: 0, diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 61c3609f2..834b1c0d0 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -17,6 +17,7 @@ export interface PagerRef { export interface RenderTabBarFnProps { selectedPage: number onSelect?: (index: number) => void + tabBarAnchor?: JSX.Element | null | undefined // Ignored on native. } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index 3b5e9164a..dde799e42 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -1,10 +1,12 @@ import React from 'react' +import {flushSync} from 'react-dom' import {View} from 'react-native' import {s} from 'lib/styles' export interface RenderTabBarFnProps { selectedPage: number onSelect?: (index: number) => void + tabBarAnchor?: JSX.Element } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element @@ -27,6 +29,8 @@ export const Pager = React.forwardRef(function PagerImpl( ref, ) { const [selectedPage, setSelectedPage] = React.useState(initialPage) + const scrollYs = React.useRef<Array<number | null>>([]) + const anchorRef = React.useRef(null) React.useImperativeHandle(ref, () => ({ setPage: (index: number) => setSelectedPage(index), @@ -34,11 +38,36 @@ export const Pager = React.forwardRef(function PagerImpl( const onTabBarSelect = React.useCallback( (index: number) => { - setSelectedPage(index) - onPageSelected?.(index) - onPageSelecting?.(index) + const scrollY = window.scrollY + // We want to determine if the tabbar is already "sticking" at the top (in which + // case we should preserve and restore scroll), or if it is somewhere below in the + // viewport (in which case a scroll jump would be jarring). We determine this by + // measuring where the "anchor" element is (which we place just above the tabbar). + let anchorTop = anchorRef.current + ? (anchorRef.current as Element).getBoundingClientRect().top + : -scrollY // If there's no anchor, treat the top of the page as one. + const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable. + + if (isSticking) { + scrollYs.current[selectedPage] = window.scrollY + } else { + scrollYs.current[selectedPage] = null + } + flushSync(() => { + setSelectedPage(index) + onPageSelected?.(index) + onPageSelecting?.(index) + }) + if (isSticking) { + const restoredScrollY = scrollYs.current[index] + if (restoredScrollY != null) { + window.scrollTo(0, restoredScrollY) + } else { + window.scrollTo(0, scrollY + anchorTop) + } + } }, - [setSelectedPage, onPageSelected, onPageSelecting], + [selectedPage, setSelectedPage, onPageSelected, onPageSelecting], ) return ( @@ -46,21 +75,11 @@ export const Pager = React.forwardRef(function PagerImpl( {tabBarPosition === 'top' && renderTabBar({ selectedPage, + tabBarAnchor: <View ref={anchorRef} />, onSelect: onTabBarSelect, })} {React.Children.map(children, (child, i) => ( - <View - style={ - selectedPage === i - ? s.flex1 - : { - position: 'absolute', - pointerEvents: 'none', - // @ts-ignore web-only - visibility: 'hidden', - } - } - key={`page-${i}`}> + <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}> {child} </View> ))} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 158940d67..279b607ad 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -18,7 +18,6 @@ import Animated, { } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {ListMethods} from '../util/List' import {ScrollProvider} from '#/lib/ScrollContext' @@ -235,7 +234,6 @@ let PagerTabBar = ({ onCurrentPageSelected?: (index: number) => void onSelect?: (index: number) => void }): React.ReactNode => { - const {isMobile} = useWebMediaQueries() const headerTransform = useAnimatedStyle(() => ({ transform: [ { @@ -246,10 +244,7 @@ let PagerTabBar = ({ return ( <Animated.View pointerEvents="box-none" - style={[ - isMobile ? styles.tabBarMobile : styles.tabBarDesktop, - headerTransform, - ]}> + style={[styles.tabBarMobile, headerTransform]}> <View onLayout={onHeaderOnlyLayout} pointerEvents="box-none"> {renderHeader?.()} </View> @@ -325,14 +320,6 @@ const styles = StyleSheet.create({ left: 0, width: '100%', }, - tabBarDesktop: { - position: 'absolute', - zIndex: 1, - top: 0, - // @ts-ignore Web only -prf - left: 'calc(50% - 299px)', - width: 598, - }, }) function noop() { diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx new file mode 100644 index 000000000..0a18a9e7d --- /dev/null +++ b/src/view/com/pager/PagerWithHeader.web.tsx @@ -0,0 +1,194 @@ +import * as React from 'react' +import {FlatList, ScrollView, StyleSheet, View} from 'react-native' +import {useAnimatedRef} from 'react-native-reanimated' +import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' +import {TabBar} from './TabBar' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {ListMethods} from '../util/List' + +export interface PagerWithHeaderChildParams { + headerHeight: number + isFocused: boolean + scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null> +} + +export interface PagerWithHeaderProps { + testID?: string + children: + | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] + | ((props: PagerWithHeaderChildParams) => JSX.Element) + items: string[] + isHeaderReady: boolean + renderHeader?: () => JSX.Element + initialPage?: number + onPageSelected?: (index: number) => void + onCurrentPageSelected?: (index: number) => void +} +export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( + function PageWithHeaderImpl( + { + children, + testID, + items, + renderHeader, + initialPage, + onPageSelected, + onCurrentPageSelected, + }: PagerWithHeaderProps, + ref, + ) { + const [currentPage, setCurrentPage] = React.useState(0) + + const renderTabBar = React.useCallback( + (props: RenderTabBarFnProps) => { + return ( + <PagerTabBar + items={items} + renderHeader={renderHeader} + currentPage={currentPage} + onCurrentPageSelected={onCurrentPageSelected} + onSelect={props.onSelect} + tabBarAnchor={props.tabBarAnchor} + testID={testID} + /> + ) + }, + [items, renderHeader, currentPage, onCurrentPageSelected, testID], + ) + + const onPageSelectedInner = React.useCallback( + (index: number) => { + setCurrentPage(index) + onPageSelected?.(index) + }, + [onPageSelected, setCurrentPage], + ) + + const onPageSelecting = React.useCallback((index: number) => { + setCurrentPage(index) + }, []) + + return ( + <Pager + ref={ref} + testID={testID} + initialPage={initialPage} + onPageSelected={onPageSelectedInner} + onPageSelecting={onPageSelecting} + renderTabBar={renderTabBar} + tabBarPosition="top"> + {toArray(children) + .filter(Boolean) + .map((child, i) => { + return ( + <View key={i} collapsable={false}> + <PagerItem isFocused={i === currentPage} renderTab={child} /> + </View> + ) + })} + </Pager> + ) + }, +) + +let PagerTabBar = ({ + currentPage, + items, + testID, + renderHeader, + onCurrentPageSelected, + onSelect, + tabBarAnchor, +}: { + currentPage: number + items: string[] + testID?: string + renderHeader?: () => JSX.Element + onCurrentPageSelected?: (index: number) => void + onSelect?: (index: number) => void + tabBarAnchor?: JSX.Element | null | undefined +}): React.ReactNode => { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + return ( + <> + <View style={[!isMobile && styles.headerContainerDesktop, pal.border]}> + {renderHeader?.()} + </View> + {tabBarAnchor} + <View + style={[ + styles.tabBarContainer, + isMobile + ? styles.tabBarContainerMobile + : styles.tabBarContainerDesktop, + pal.border, + ]}> + <TabBar + testID={testID} + items={items} + selectedPage={currentPage} + onSelect={onSelect} + onPressSelected={onCurrentPageSelected} + /> + </View> + </> + ) +} +PagerTabBar = React.memo(PagerTabBar) + +function PagerItem({ + isFocused, + renderTab, +}: { + isFocused: boolean + renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null +}) { + const scrollElRef = useAnimatedRef() + if (renderTab == null) { + return null + } + return renderTab({ + headerHeight: 0, + isFocused, + scrollElRef: scrollElRef as React.MutableRefObject< + ListMethods | ScrollView | null + >, + }) +} + +const styles = StyleSheet.create({ + headerContainerDesktop: { + marginLeft: 'auto', + marginRight: 'auto', + width: 600, + borderLeftWidth: 1, + borderRightWidth: 1, + }, + tabBarContainer: { + // @ts-ignore web-only + position: 'sticky', + overflow: 'hidden', + top: 0, + zIndex: 1, + }, + tabBarContainerDesktop: { + marginLeft: 'auto', + marginRight: 'auto', + width: 600, + borderLeftWidth: 1, + borderRightWidth: 1, + }, + tabBarContainerMobile: { + paddingLeft: 14, + paddingRight: 14, + }, +}) + +function toArray<T>(v: T | T[]): T[] { + if (Array.isArray(v)) { + return v + } + return [v] +} |