Files
bsky-social-app/src/view/com/pager/TabBar.tsx
T
Dan Abramov 82b227e64a Use regular ScrollView on native
The Draggable thing was needed for web-only behavior so we can drop it in the native fork.
2024-11-30 18:03:07 +00:00

138 lines
3.7 KiB
TypeScript

import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native'
import {usePalette} from '#/lib/hooks/usePalette'
import {PressableWithHover} from '../util/PressableWithHover'
import {Text} from '../util/text/Text'
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<ScrollView>(null)
const [itemXs, setItemXs] = useState<number[]>([])
const indicatorStyle = useMemo(
() => ({borderBottomColor: indicatorColor || pal.colors.link}),
[indicatorColor, pal],
)
useEffect(() => {
// 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})
}, [scrollElRef, itemXs, selectedPage])
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 (
<View
testID={testID}
style={[pal.view, styles.outer]}
accessibilityRole="tablist">
<ScrollView
testID={`${testID}-selector`}
horizontal={true}
showsHorizontalScrollIndicator={false}
ref={scrollElRef}
contentContainerStyle={styles.contentContainer}>
{items.map((item, i) => {
const selected = i === selectedPage
return (
<PressableWithHover
testID={`${testID}-selector-${i}`}
key={`${item}-${i}`}
onLayout={e => onItemLayout(e, i)}
style={styles.item}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}
accessibilityRole="tab">
<View style={[styles.itemInner, selected && indicatorStyle]}>
<Text
emoji
type="lg-bold"
testID={testID ? `${testID}-${item}` : undefined}
style={[
selected ? pal.text : pal.textLight,
{lineHeight: 20},
]}>
{item}
</Text>
</View>
</PressableWithHover>
)
})}
</ScrollView>
<View style={[pal.border, styles.outerBottomBorder]} />
</View>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
},
contentContainer: {
backgroundColor: 'transparent',
paddingHorizontal: 6,
},
item: {
paddingTop: 10,
paddingHorizontal: 10,
justifyContent: 'center',
},
itemInner: {
paddingBottom: 10,
borderBottomWidth: 3,
borderBottomColor: 'transparent',
},
outerBottomBorder: {
position: 'absolute',
left: 0,
right: 0,
top: '100%',
borderBottomWidth: StyleSheet.hairlineWidth,
},
})