expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
257 lines • 13.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeTabsView = NativeTabsView;
const react_1 = __importStar(require("react"));
const react_native_screens_1 = require("react-native-screens");
const package_json_1 = __importDefault(require("react-native-screens/package.json"));
const appearance_1 = require("./appearance");
const types_1 = require("./types");
const utils_1 = require("./utils");
// We let native tabs to control the changes. This requires freeze to be disabled for tab bar.
// Otherwise user may see glitches when switching between tabs.
react_native_screens_1.featureFlags.experiment.controlledBottomTabs = false;
const supportedBlurEffectsSet = new Set(types_1.SUPPORTED_BLUR_EFFECTS);
// TODO(ubax): refactor this component, so that testing options passed to screen is easier
function NativeTabsView(props) {
const { builder, minimizeBehavior, disableIndicator, focusedIndex } = props;
const { state, descriptors, navigation } = builder;
const { routes } = state;
let blurEffect = props.blurEffect;
if (blurEffect && !supportedBlurEffectsSet.has(blurEffect)) {
console.warn(`Unsupported blurEffect: ${blurEffect}. Supported values are: ${types_1.SUPPORTED_BLUR_EFFECTS.map((effect) => `"${effect}"`).join(', ')}`);
blurEffect = undefined;
}
const defaultIconColor = (0, utils_1.convertIconColorPropToObject)(props.iconColor).default;
const defaultLabelStyle = (0, utils_1.convertLabelStylePropToObject)(props.labelStyle).default;
const deferredFocusedIndex = (0, react_1.useDeferredValue)(focusedIndex);
// We need to check if the deferred index is not out of bounds
// This can happen when the focused index is the last tab, and user removes that tab
// In that case the deferred index will still point to the last tab, but after re-render
// it will be out of bounds
const inBoundsDeferredFocusedIndex = deferredFocusedIndex < routes.length ? deferredFocusedIndex : focusedIndex;
let standardAppearance = (0, appearance_1.convertStyleToAppearance)({
...defaultLabelStyle,
iconColor: defaultIconColor,
blurEffect,
backgroundColor: props.backgroundColor,
badgeBackgroundColor: props.badgeBackgroundColor,
});
if (props.tintColor) {
standardAppearance = (0, appearance_1.appendSelectedStyleToAppearance)({ iconColor: props.tintColor, color: props.tintColor }, standardAppearance);
}
const scrollEdgeAppearance = (0, appearance_1.convertStyleToAppearance)({
...defaultLabelStyle,
iconColor: defaultIconColor,
blurEffect,
backgroundColor: props.backgroundColor,
badgeBackgroundColor: props.badgeBackgroundColor,
});
const appearances = routes.map((route) => ({
standardAppearance: (0, appearance_1.createStandardAppearanceFromOptions)(descriptors[route.key].options, standardAppearance),
scrollEdgeAppearance: (0, appearance_1.createScrollEdgeAppearanceFromOptions)(descriptors[route.key].options, scrollEdgeAppearance),
}));
const options = routes.map((route) => descriptors[route.key].options);
const children = routes
.map((route, index) => ({ route, index }))
.filter(({ route: { key } }) => (0, utils_1.shouldTabBeVisible)(descriptors[key].options))
.map(({ route, index }) => {
const descriptor = descriptors[route.key];
const isFocused = index === inBoundsDeferredFocusedIndex;
return (<Screen key={route.key} routeKey={route.key} name={route.name} descriptor={descriptor} isFocused={isFocused} standardAppearance={appearances[index].standardAppearance} scrollEdgeAppearance={appearances[index].scrollEdgeAppearance} badgeTextColor={props.badgeTextColor}/>);
});
const currentTabAppearance = appearances[inBoundsDeferredFocusedIndex]?.standardAppearance;
return (<BottomTabsWrapper
// #region android props
tabBarItemTitleFontColor={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontColor} tabBarItemTitleFontFamily={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontFamily} tabBarItemTitleFontSize={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontSize} tabBarItemTitleFontSizeActive={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontSize} tabBarItemTitleFontWeight={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontWeight} tabBarItemTitleFontStyle={currentTabAppearance?.stacked?.normal?.tabBarItemTitleFontStyle} tabBarItemIconColor={currentTabAppearance?.stacked?.normal?.tabBarItemIconColor} tabBarBackgroundColor={currentTabAppearance?.tabBarBackgroundColor ?? props.backgroundColor ?? undefined} tabBarItemRippleColor={props.rippleColor} tabBarItemLabelVisibilityMode={props.labelVisibilityMode} tabBarItemIconColorActive={currentTabAppearance?.stacked?.selected?.tabBarItemIconColor ?? props?.tintColor} tabBarItemTitleFontColorActive={currentTabAppearance?.stacked?.selected?.tabBarItemTitleFontColor ?? props?.tintColor}
// tabBarItemTitleFontSizeActive={activeStyle?.fontSize}
tabBarItemActiveIndicatorColor={options[inBoundsDeferredFocusedIndex]?.indicatorColor ?? props?.indicatorColor} tabBarItemActiveIndicatorEnabled={!disableIndicator}
// #endregion
// #region iOS props
tabBarTintColor={props?.tintColor} tabBarMinimizeBehavior={minimizeBehavior}
// #endregion
onNativeFocusChange={({ nativeEvent: { tabKey } }) => {
const descriptor = descriptors[tabKey];
const route = descriptor.route;
navigation.dispatch({
type: 'JUMP_TO',
target: state.key,
payload: {
name: route.name,
},
});
}}>
{children}
</BottomTabsWrapper>);
}
function Screen(props) {
const { routeKey, name, descriptor, isFocused, standardAppearance, scrollEdgeAppearance, badgeTextColor, } = props;
const title = descriptor.options.title ?? name;
const icon = useAwaitedScreensIcon(descriptor.options.icon);
const selectedIcon = useAwaitedScreensIcon(descriptor.options.selectedIcon);
return (<react_native_screens_1.BottomTabsScreen {...descriptor.options} tabBarItemBadgeBackgroundColor={standardAppearance.stacked?.normal?.tabBarItemBadgeBackgroundColor} tabBarItemBadgeTextColor={badgeTextColor} standardAppearance={standardAppearance} scrollEdgeAppearance={scrollEdgeAppearance} iconResourceName={getAndroidIconResourceName(icon)} iconResource={getAndroidIconResource(icon)} icon={convertOptionsIconToPropsIcon(icon)} selectedIcon={convertOptionsIconToPropsIcon(selectedIcon)} title={title} freezeContents={false} tabKey={routeKey} systemItem={descriptor.options.role} isFocused={isFocused}>
{descriptor.render()}
</react_native_screens_1.BottomTabsScreen>);
}
function useAwaitedScreensIcon(icon) {
const src = icon && typeof icon === 'object' && 'src' in icon ? icon.src : undefined;
const [awaitedIcon, setAwaitedIcon] = (0, react_1.useState)(undefined);
(0, react_1.useEffect)(() => {
const loadIcon = async () => {
if (src && src instanceof Promise) {
const awaitedSrc = await src;
if (awaitedSrc) {
const currentAwaitedIcon = { src: awaitedSrc };
setAwaitedIcon(currentAwaitedIcon);
}
}
};
loadIcon();
// Checking `src` rather then icon here, to avoid unnecessary re-renders
// The icon object can be recreated, while src should stay the same
// In this case as we control `VectorIcon`, it will only change if `family` or `name` props change
// So we should be safe with promise resolving
}, [src]);
return (0, react_1.useMemo)(() => (isAwaitedIcon(icon) ? icon : awaitedIcon), [awaitedIcon, icon]);
}
function isAwaitedIcon(icon) {
return !icon || !('src' in icon && icon.src instanceof Promise);
}
function convertOptionsIconToPropsIcon_4_16(icon) {
if (!icon) {
return undefined;
}
if ('sf' in icon && icon.sf) {
return { sfSymbolName: icon.sf };
}
else if ('src' in icon && icon.src) {
return { templateSource: icon.src };
}
return undefined;
}
// Function added in https://github.com/expo/expo/pull/41631
// to support native tabs icons in both 4.16 and 4.18+ versions of react-native-screens
function convertOptionsIconToPropsIcon_4_18(icon) {
if (!icon) {
return undefined;
}
if (process.env.EXPO_OS === 'ios') {
if ('sf' in icon && icon.sf) {
return {
// selectedIcon
type: 'sfSymbol',
name: icon.sf,
// icon
ios: {
type: 'sfSymbol',
name: icon.sf,
},
};
}
if ('src' in icon && icon.src) {
return {
// selectedIcon
type: 'templateSource',
templateSource: icon.src,
// icon
ios: {
type: 'templateSource',
templateSource: icon.src,
},
};
}
}
else if (process.env.EXPO_OS === 'android') {
if ('drawable' in icon && icon.drawable) {
return {
android: {
type: 'drawableResource',
name: icon.drawable,
},
};
}
if ('src' in icon && icon.src) {
return {
android: {
type: 'imageSource',
imageSource: icon.src,
},
};
}
}
return undefined;
}
function convertOptionsIconToPropsIcon(icon) {
// Code added in https://github.com/expo/expo/pull/41631
// to support native tabs icons in both 4.16 and 4.18+ versions of react-native-screens
const [_, minor] = package_json_1.default.version.split('.');
const is4_18rNewer = minor && parseInt(minor, 10) >= 18;
if (is4_18rNewer) {
return convertOptionsIconToPropsIcon_4_18(icon);
}
return convertOptionsIconToPropsIcon_4_16(icon);
}
function getAndroidIconResource(icon) {
if (icon && 'src' in icon && icon.src) {
return icon.src;
}
return undefined;
}
function getAndroidIconResourceName(icon) {
if (icon && 'drawable' in icon && icon.drawable) {
return icon.drawable;
}
return undefined;
}
const supportedTabBarMinimizeBehaviorsSet = new Set(types_1.SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS);
const supportedTabBarItemLabelVisibilityModesSet = new Set(types_1.SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES);
function BottomTabsWrapper(props) {
let { tabBarMinimizeBehavior, tabBarItemLabelVisibilityMode, ...rest } = props;
if (tabBarMinimizeBehavior && !supportedTabBarMinimizeBehaviorsSet.has(tabBarMinimizeBehavior)) {
console.warn(`Unsupported minimizeBehavior: ${tabBarMinimizeBehavior}. Supported values are: ${types_1.SUPPORTED_TAB_BAR_MINIMIZE_BEHAVIORS.map((behavior) => `"${behavior}"`).join(', ')}`);
tabBarMinimizeBehavior = undefined;
}
if (tabBarItemLabelVisibilityMode &&
!supportedTabBarItemLabelVisibilityModesSet.has(tabBarItemLabelVisibilityMode)) {
console.warn(`Unsupported labelVisibilityMode: ${tabBarItemLabelVisibilityMode}. Supported values are: ${types_1.SUPPORTED_TAB_BAR_ITEM_LABEL_VISIBILITY_MODES.map((mode) => `"${mode}"`).join(', ')}`);
tabBarItemLabelVisibilityMode = undefined;
}
return (<react_native_screens_1.BottomTabs tabBarItemLabelVisibilityMode={tabBarItemLabelVisibilityMode} tabBarMinimizeBehavior={tabBarMinimizeBehavior} {...rest}/>);
}
// #endregion
//# sourceMappingURL=NativeTabsView.js.map