import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native' import {isNative} from '#/platform/detection' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {PressableWithHover} from '../util/PressableWithHover' import {Text} from '../util/text/Text' import {DraggableScrollView} from './DraggableScrollView' export interface TabBarProps { testID?: string selectedPage: number items: string[] indicatorColor?: string onSelect?: (index: number) => void onPressSelected?: (index: number) => void } // How much of the previous/next item we're showing // to give the user a hint there's more to scroll. const OFFSCREEN_ITEM_WIDTH = 20 export function TabBar({ testID, selectedPage, items, indicatorColor, onSelect, onPressSelected, }: TabBarProps) { const pal = usePalette('default') const scrollElRef = useRef(null) const itemRefs = useRef>([]) const [itemXs, setItemXs] = useState([]) const indicatorStyle = useMemo( () => ({borderBottomColor: indicatorColor || pal.colors.link}), [indicatorColor, pal], ) const {isDesktop, isTablet} = useWebMediaQueries() const styles = isDesktop || isTablet ? desktopStyles : mobileStyles useEffect(() => { if (isNative) { // On native, the primary interaction is swiping. // We adjust the scroll little by little on every tab change. // Scroll into view but keep the end of the previous item visible. let x = itemXs[selectedPage] || 0 x = Math.max(0, x - OFFSCREEN_ITEM_WIDTH) scrollElRef.current?.scrollTo({x}) } else { // On the web, the primary interaction is tapping. // Scrolling under tap feels disorienting so only adjust the scroll offset // when tapping on an item out of view--and we adjust by almost an entire page. const parent = scrollElRef?.current?.getScrollableNode?.() if (!parent) { return } const parentRect = parent.getBoundingClientRect() if (!parentRect) { return } const { left: parentLeft, right: parentRight, width: parentWidth, } = parentRect const child = itemRefs.current[selectedPage] if (!child) { return } const childRect = child.getBoundingClientRect?.() if (!childRect) { return } const {left: childLeft, right: childRight, width: childWidth} = childRect let dx = 0 if (childRight >= parentRight) { dx += childRight - parentRight dx += parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH } else if (childLeft <= parentLeft) { dx -= parentLeft - childLeft dx -= parentWidth - childWidth - OFFSCREEN_ITEM_WIDTH } let x = parent.scrollLeft + dx x = Math.max(0, x) x = Math.min(x, parent.scrollWidth - parentWidth) if (dx !== 0) { parent.scroll({ left: x, behavior: 'smooth', }) } } }, [scrollElRef, itemXs, selectedPage, styles]) const onPressItem = useCallback( (index: number) => { onSelect?.(index) if (index === selectedPage) { onPressSelected?.(index) } }, [onSelect, selectedPage, onPressSelected], ) // calculates the x position of each item on mount and on layout change const onItemLayout = React.useCallback( (e: LayoutChangeEvent, index: number) => { const x = e.nativeEvent.layout.x setItemXs(prev => { const Xs = [...prev] Xs[index] = x return Xs }) }, [], ) return ( {items.map((item, i) => { const selected = i === selectedPage return ( (itemRefs.current[i] = node)} onLayout={e => onItemLayout(e, i)} style={styles.item} hoverStyle={pal.viewLight} onPress={() => onPressItem(i)}> {item} ) })} ) } const desktopStyles = StyleSheet.create({ outer: { flexDirection: 'row', width: 598, }, contentContainer: { paddingHorizontal: 0, backgroundColor: 'transparent', }, item: { paddingTop: 14, paddingHorizontal: 14, justifyContent: 'center', }, itemInner: { paddingBottom: 12, borderBottomWidth: 3, borderBottomColor: 'transparent', }, outerBottomBorder: { position: 'absolute', left: 0, right: 0, bottom: -1, borderBottomWidth: 1, }, }) const mobileStyles = StyleSheet.create({ outer: { flexDirection: 'row', }, contentContainer: { backgroundColor: 'transparent', paddingHorizontal: 8, }, item: { paddingTop: 10, paddingHorizontal: 10, justifyContent: 'center', }, itemInner: { paddingBottom: 10, borderBottomWidth: 3, borderBottomColor: 'transparent', }, outerBottomBorder: { position: 'absolute', left: 0, right: 0, bottom: -1, borderBottomWidth: 1, }, })