153 lines
4.5 KiB
TypeScript

import {
createNavigatorFactory,
TabRouter,
useNavigationBuilder,
type DefaultNavigatorOptions,
type NavigationProp,
type ParamListBase,
type TabActionHelpers,
type TabNavigationState,
type TabRouterOptions,
} from '@react-navigation/native';
import { withLayoutContext } from 'expo-router';
import type { ImageSourcePropType } from 'react-native';
import TabView, { type AppleIcon } from 'react-native-bottom-tabs';
// Pro-Screen Optionen (kompatibel mit Expo Router's Tabs.Screen API)
export type NativeTabsScreenOptions = {
title?: string;
tabBarIcon?: (props: { focused: boolean }) => AppleIcon | ImageSourcePropType;
tabBarBadge?: string;
// Expo-Router-Konvention: href === null → Screen NICHT in TabBar zeigen
href?: string | null;
};
type NativeTabNavigationEventMap = {
tabPress: { data: undefined; canPreventDefault: true };
tabLongPress: { data: undefined };
};
// Native-spezifische Tab-Layer-Optionen (iOS 26 Glass-Pill, Haptik, etc.)
type NativeOnlyOptions = {
sidebarAdaptable?: boolean;
hapticFeedbackEnabled?: boolean;
disablePageAnimations?: boolean;
scrollEdgeAppearance?: 'default' | 'opaque' | 'transparent';
minimizeBehavior?: 'automatic' | 'onScrollDown' | 'onScrollUp' | 'never';
tabBarActiveTintColor?: string;
tabBarInactiveTintColor?: string;
labeled?: boolean;
tabLabelStyle?: {
fontFamily?: string;
fontWeight?: string;
fontSize?: number;
};
};
type Props = DefaultNavigatorOptions<
ParamListBase,
string | undefined,
TabNavigationState<ParamListBase>,
NativeTabsScreenOptions,
NativeTabNavigationEventMap,
NavigationProp<ParamListBase>
> &
TabRouterOptions &
NativeOnlyOptions;
function NativeTabsNavigator({
id,
initialRouteName,
children,
screenOptions,
layout,
sidebarAdaptable = true,
hapticFeedbackEnabled = true,
disablePageAnimations,
scrollEdgeAppearance,
minimizeBehavior,
tabBarActiveTintColor,
tabBarInactiveTintColor,
labeled = true,
tabLabelStyle,
}: Props) {
const { state, descriptors, navigation, NavigationContent } =
useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
NativeTabsScreenOptions,
NativeTabNavigationEventMap
>(TabRouter, {
id,
initialRouteName,
children,
screenOptions,
layout,
});
return (
<NavigationContent>
<TabView
labeled={labeled}
sidebarAdaptable={sidebarAdaptable}
hapticFeedbackEnabled={hapticFeedbackEnabled}
disablePageAnimations={disablePageAnimations}
scrollEdgeAppearance={scrollEdgeAppearance}
minimizeBehavior={minimizeBehavior}
tabBarActiveTintColor={tabBarActiveTintColor}
tabBarInactiveTintColor={tabBarInactiveTintColor}
tabLabelStyle={tabLabelStyle}
navigationState={{
index: state.index,
routes: state.routes.map((route) => {
const options = descriptors[route.key].options;
return {
key: route.key,
title: options.title ?? route.name,
focusedIcon: options.tabBarIcon
? options.tabBarIcon({ focused: true })
: undefined,
unfocusedIcon: options.tabBarIcon
? options.tabBarIcon({ focused: false })
: undefined,
badge: options.tabBarBadge,
hidden: options.href === null,
};
}),
}}
onIndexChange={(index) => {
const route = state.routes[index];
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.dispatch({
type: 'NAVIGATE',
payload: { name: route.name, merge: true },
target: state.key,
});
}
}}
onTabLongPress={(index) => {
const route = state.routes[index];
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
}}
renderScene={({ route }) => descriptors[route.key].render()}
/>
</NavigationContent>
);
}
export const createNativeTabNavigator = createNavigatorFactory(NativeTabsNavigator);
const NativeTabNav = createNativeTabNavigator();
// withLayoutContext-wrapped Navigator für Expo-Router-Kompatibilität
export const NativeTabs = withLayoutContext(NativeTabNav.Navigator);