139 lines
3.5 KiB
TypeScript
139 lines
3.5 KiB
TypeScript
import { memo, useCallback, useEffect, useRef } from 'react';
|
|
import { Pressable, ScrollView, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
|
|
|
|
type Category = {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
type CategoryTabsProps = {
|
|
categories: Category[];
|
|
activeId: string;
|
|
onChange: (id: string) => void;
|
|
};
|
|
|
|
export function CategoryTabs({ categories, activeId, onChange }: CategoryTabsProps) {
|
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
const tabPositionsRef = useRef<Map<string, { x: number; width: number }>>(new Map());
|
|
const containerWidthRef = useRef<number>(0);
|
|
const { width: screenWidth } = useWindowDimensions();
|
|
|
|
const scrollToActiveTab = useCallback(() => {
|
|
if (!activeId) {
|
|
return;
|
|
}
|
|
|
|
const position = tabPositionsRef.current.get(activeId);
|
|
if (!position || !scrollViewRef.current) {
|
|
return;
|
|
}
|
|
|
|
const centerOffset = screenWidth / 2;
|
|
const tabCenter = position.x + position.width / 2;
|
|
const scrollX = tabCenter - centerOffset;
|
|
|
|
const maxScrollX = Math.max(0, containerWidthRef.current - screenWidth);
|
|
const targetX = Math.max(0, Math.min(scrollX, maxScrollX));
|
|
|
|
scrollViewRef.current.scrollTo({
|
|
x: targetX,
|
|
animated: true,
|
|
});
|
|
}, [activeId, screenWidth]);
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(scrollToActiveTab, 150);
|
|
return () => clearTimeout(timer);
|
|
}, [scrollToActiveTab]);
|
|
|
|
const handleContentSizeChange = useCallback((width: number) => {
|
|
containerWidthRef.current = width;
|
|
}, []);
|
|
|
|
return (
|
|
<ScrollView
|
|
ref={scrollViewRef}
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
contentContainerStyle={styles.container}
|
|
style={styles.scrollView}
|
|
onContentSizeChange={handleContentSizeChange}
|
|
>
|
|
{categories.map((category, index) => (
|
|
<CategoryTabItem
|
|
key={category.id}
|
|
category={category}
|
|
isActive={category.id === activeId}
|
|
onPress={onChange}
|
|
onLayout={(layout) => {
|
|
tabPositionsRef.current.set(category.id, {
|
|
x: layout.x,
|
|
width: layout.width,
|
|
});
|
|
}}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
type CategoryTabItemProps = {
|
|
category: Category;
|
|
isActive: boolean;
|
|
onPress: (id: string) => void;
|
|
onLayout: (layout: { x: number; width: number }) => void;
|
|
};
|
|
|
|
const CategoryTabItem = memo(({ category, isActive, onPress, onLayout }: CategoryTabItemProps) => {
|
|
return (
|
|
<Pressable
|
|
onPress={() => onPress(category.id)}
|
|
style={styles.tabButton}
|
|
onLayout={(event) => {
|
|
const { x, width } = event.nativeEvent.layout;
|
|
onLayout({ x, width });
|
|
}}
|
|
>
|
|
<Text style={[styles.label, isActive && styles.labelActive]}>{category.label}</Text>
|
|
<View style={[styles.indicator, isActive && styles.indicatorActive]} />
|
|
</Pressable>
|
|
);
|
|
});
|
|
CategoryTabItem.displayName = 'CategoryTabItem';
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollView: {
|
|
flexGrow: 0,
|
|
flexShrink: 0,
|
|
},
|
|
container: {
|
|
paddingVertical: 0,
|
|
paddingHorizontal: 6,
|
|
paddingTop: 10
|
|
},
|
|
tabButton: {
|
|
alignItems: 'center',
|
|
marginRight: 20,
|
|
paddingBottom: 0,
|
|
},
|
|
label: {
|
|
color: '#7F8794',
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
letterSpacing: 0.3,
|
|
},
|
|
labelActive: {
|
|
color: '#C7FF00',
|
|
},
|
|
indicator: {
|
|
alignSelf: 'stretch',
|
|
height: 3,
|
|
marginTop: 8,
|
|
backgroundColor: 'transparent',
|
|
borderRadius: 999,
|
|
},
|
|
indicatorActive: {
|
|
backgroundColor: '#C7FF00',
|
|
},
|
|
});
|