first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

21
node_modules/@react-navigation/native-stack/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 React Navigation Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,5 @@
# `@react-navigation/native-stack`
Stack navigator for React Native using native primitives for navigation. Uses [`react-native-screens`](https://github.com/software-mansion/react-native-screens) under the hood.
Installation instructions and documentation can be found on the [React Navigation website](https://reactnavigation.org/docs/native-stack-navigator.html).

View File

@@ -0,0 +1,21 @@
"use strict";
/**
* Navigators
*/
export { createNativeStackNavigator } from "./navigators/createNativeStackNavigator.js";
/**
* Views
*/
export { NativeStackView } from './views/NativeStackView';
/**
* Hooks
*/
export { useAnimatedHeaderHeight } from "./utils/useAnimatedHeaderHeight.js";
/**
* Types
*/
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["createNativeStackNavigator","NativeStackView","useAnimatedHeaderHeight"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA;AACA;AACA;AACA,SAASA,0BAA0B,QAAQ,4CAAyC;;AAEpF;AACA;AACA;AACA,SAASC,eAAe,QAAQ,yBAAyB;;AAEzD;AACA;AACA;AACA,SAASC,uBAAuB,QAAQ,oCAAiC;;AAEzE;AACA;AACA","ignoreList":[]}

View File

@@ -0,0 +1,75 @@
"use strict";
import { createNavigatorFactory, NavigationMetaContext, StackActions, StackRouter, useNavigationBuilder } from '@react-navigation/native';
import * as React from 'react';
import { NativeStackView } from '../views/NativeStackView';
import { jsx as _jsx } from "react/jsx-runtime";
function NativeStackNavigator({
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router,
...rest
}) {
const {
state,
describe,
descriptors,
navigation,
NavigationContent
} = useNavigationBuilder(StackRouter, {
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router
});
const meta = React.useContext(NavigationMetaContext);
React.useEffect(() => {
if (meta && 'type' in meta && meta.type === 'native-tabs') {
// If we're inside native tabs, we don't need to handle popToTop
// It's handled natively by native tabs
return;
}
// @ts-expect-error: there may not be a tab navigator in parent
return navigation?.addListener?.('tabPress', e => {
const isFocused = navigation.isFocused();
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (state.index > 0 && isFocused && !e.defaultPrevented) {
// When user taps on already focused tab and we're inside the tab,
// reset the stack to replicate native behaviour
navigation.dispatch({
...StackActions.popToTop(),
target: state.key
});
}
});
});
}, [meta, navigation, state.index, state.key]);
return /*#__PURE__*/_jsx(NavigationContent, {
children: /*#__PURE__*/_jsx(NativeStackView, {
...rest,
state: state,
navigation: navigation,
descriptors: descriptors,
describe: describe
})
});
}
export function createNativeStackNavigator(config) {
return createNavigatorFactory(NativeStackNavigator)(config);
}
//# sourceMappingURL=createNativeStackNavigator.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["createNavigatorFactory","NavigationMetaContext","StackActions","StackRouter","useNavigationBuilder","React","NativeStackView","jsx","_jsx","NativeStackNavigator","id","initialRouteName","UNSTABLE_routeNamesChangeBehavior","children","layout","screenListeners","screenOptions","screenLayout","UNSTABLE_router","rest","state","describe","descriptors","navigation","NavigationContent","meta","useContext","useEffect","type","addListener","e","isFocused","requestAnimationFrame","index","defaultPrevented","dispatch","popToTop","target","key","createNativeStackNavigator","config"],"sourceRoot":"../../../src","sources":["navigators/createNativeStackNavigator.tsx"],"mappings":";;AAAA,SACEA,sBAAsB,EAEtBC,qBAAqB,EAIrBC,YAAY,EAEZC,WAAW,EAIXC,oBAAoB,QACf,0BAA0B;AACjC,OAAO,KAAKC,KAAK,MAAM,OAAO;AAQ9B,SAASC,eAAe,QAAQ,0BAA0B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAE3D,SAASC,oBAAoBA,CAAC;EAC5BC,EAAE;EACFC,gBAAgB;EAChBC,iCAAiC;EACjCC,QAAQ;EACRC,MAAM;EACNC,eAAe;EACfC,aAAa;EACbC,YAAY;EACZC,eAAe;EACf,GAAGC;AACsB,CAAC,EAAE;EAC5B,MAAM;IAAEC,KAAK;IAAEC,QAAQ;IAAEC,WAAW;IAAEC,UAAU;IAAEC;EAAkB,CAAC,GACnEpB,oBAAoB,CAMlBD,WAAW,EAAE;IACbO,EAAE;IACFC,gBAAgB;IAChBC,iCAAiC;IACjCC,QAAQ;IACRC,MAAM;IACNC,eAAe;IACfC,aAAa;IACbC,YAAY;IACZC;EACF,CAAC,CAAC;EAEJ,MAAMO,IAAI,GAAGpB,KAAK,CAACqB,UAAU,CAACzB,qBAAqB,CAAC;EAEpDI,KAAK,CAACsB,SAAS,CAAC,MAAM;IACpB,IAAIF,IAAI,IAAI,MAAM,IAAIA,IAAI,IAAIA,IAAI,CAACG,IAAI,KAAK,aAAa,EAAE;MACzD;MACA;MACA;IACF;;IAEA;IACA,OAAOL,UAAU,EAAEM,WAAW,GAAG,UAAU,EAAGC,CAAM,IAAK;MACvD,MAAMC,SAAS,GAAGR,UAAU,CAACQ,SAAS,CAAC,CAAC;;MAExC;MACA;MACAC,qBAAqB,CAAC,MAAM;QAC1B,IACEZ,KAAK,CAACa,KAAK,GAAG,CAAC,IACfF,SAAS,IACT,CAAED,CAAC,CAAgCI,gBAAgB,EACnD;UACA;UACA;UACAX,UAAU,CAACY,QAAQ,CAAC;YAClB,GAAGjC,YAAY,CAACkC,QAAQ,CAAC,CAAC;YAC1BC,MAAM,EAAEjB,KAAK,CAACkB;UAChB,CAAC,CAAC;QACJ;MACF,CAAC,CAAC;IACJ,CAAC,CAAC;EACJ,CAAC,EAAE,CAACb,IAAI,EAAEF,UAAU,EAAEH,KAAK,CAACa,KAAK,EAAEb,KAAK,CAACkB,GAAG,CAAC,CAAC;EAE9C,oBACE9B,IAAA,CAACgB,iBAAiB;IAAAX,QAAA,eAChBL,IAAA,CAACF,eAAe;MAAA,GACVa,IAAI;MACRC,KAAK,EAAEA,KAAM;MACbG,UAAU,EAAEA,UAAW;MACvBD,WAAW,EAAEA,WAAY;MACzBD,QAAQ,EAAEA;IAAS,CACpB;EAAC,CACe,CAAC;AAExB;AAEA,OAAO,SAASkB,0BAA0BA,CAmBxCC,MAAe,EAAmC;EAClD,OAAOxC,sBAAsB,CAACS,oBAAoB,CAAC,CAAC+B,MAAM,CAAC;AAC7D","ignoreList":[]}

View File

@@ -0,0 +1 @@
{"type":"module"}

View File

@@ -0,0 +1,4 @@
"use strict";
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":[],"sourceRoot":"../../src","sources":["types.tsx"],"mappings":"","ignoreList":[]}

View File

@@ -0,0 +1,12 @@
"use strict";
export function debounce(func, duration) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, duration);
};
}
//# sourceMappingURL=debounce.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["debounce","func","duration","timeout","args","clearTimeout","setTimeout","apply"],"sourceRoot":"../../../src","sources":["utils/debounce.tsx"],"mappings":";;AAAA,OAAO,SAASA,QAAQA,CACtBC,IAAO,EACPC,QAAgB,EACb;EACH,IAAIC,OAAsC;EAE1C,OAAO,UAAyB,GAAGC,IAAI,EAAE;IACvCC,YAAY,CAACF,OAAO,CAAC;IAErBA,OAAO,GAAGG,UAAU,CAAC,MAAM;MACzBL,IAAI,CAACM,KAAK,CAAC,IAAI,EAAEH,IAAI,CAAC;IACxB,CAAC,EAAEF,QAAQ,CAAC;EACd,CAAC;AACH","ignoreList":[]}

View File

@@ -0,0 +1,12 @@
"use strict";
export const getModalRouteKeys = (routes, descriptors) => routes.reduce((acc, route) => {
const {
presentation
} = descriptors[route.key]?.options ?? {};
if (acc.length && !presentation || presentation === 'modal' || presentation === 'transparentModal' || presentation === 'containedModal' || presentation === 'containedTransparentModal' || presentation === 'fullScreenModal' || presentation === 'formSheet' || presentation === 'pageSheet') {
acc.push(route.key);
}
return acc;
}, []);
//# sourceMappingURL=getModalRoutesKeys.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["getModalRouteKeys","routes","descriptors","reduce","acc","route","presentation","key","options","length","push"],"sourceRoot":"../../../src","sources":["utils/getModalRoutesKeys.ts"],"mappings":";;AAIA,OAAO,MAAMA,iBAAiB,GAAGA,CAC/BC,MAAuB,EACvBC,WAAqC,KAErCD,MAAM,CAACE,MAAM,CAAW,CAACC,GAAG,EAAEC,KAAK,KAAK;EACtC,MAAM;IAAEC;EAAa,CAAC,GAAGJ,WAAW,CAACG,KAAK,CAACE,GAAG,CAAC,EAAEC,OAAO,IAAI,CAAC,CAAC;EAE9D,IACGJ,GAAG,CAACK,MAAM,IAAI,CAACH,YAAY,IAC5BA,YAAY,KAAK,OAAO,IACxBA,YAAY,KAAK,kBAAkB,IACnCA,YAAY,KAAK,gBAAgB,IACjCA,YAAY,KAAK,2BAA2B,IAC5CA,YAAY,KAAK,iBAAiB,IAClCA,YAAY,KAAK,WAAW,IAC5BA,YAAY,KAAK,WAAW,EAC5B;IACAF,GAAG,CAACM,IAAI,CAACL,KAAK,CAACE,GAAG,CAAC;EACrB;EAEA,OAAOH,GAAG;AACZ,CAAC,EAAE,EAAE,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,12 @@
"use strict";
import * as React from 'react';
export const AnimatedHeaderHeightContext = /*#__PURE__*/React.createContext(undefined);
export function useAnimatedHeaderHeight() {
const animatedValue = React.useContext(AnimatedHeaderHeightContext);
if (animatedValue === undefined) {
throw new Error("Couldn't find the header height. Are you inside a screen in a native stack navigator?");
}
return animatedValue;
}
//# sourceMappingURL=useAnimatedHeaderHeight.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["React","AnimatedHeaderHeightContext","createContext","undefined","useAnimatedHeaderHeight","animatedValue","useContext","Error"],"sourceRoot":"../../../src","sources":["utils/useAnimatedHeaderHeight.tsx"],"mappings":";;AAAA,OAAO,KAAKA,KAAK,MAAM,OAAO;AAG9B,OAAO,MAAMC,2BAA2B,gBAAGD,KAAK,CAACE,aAAa,CAE5DC,SAAS,CAAC;AAEZ,OAAO,SAASC,uBAAuBA,CAAA,EAAG;EACxC,MAAMC,aAAa,GAAGL,KAAK,CAACM,UAAU,CAACL,2BAA2B,CAAC;EAEnE,IAAII,aAAa,KAAKF,SAAS,EAAE;IAC/B,MAAM,IAAII,KAAK,CACb,uFACF,CAAC;EACH;EAEA,OAAOF,aAAa;AACtB","ignoreList":[]}

View File

@@ -0,0 +1,17 @@
"use strict";
import * as React from 'react';
export function useDismissedRouteError(state) {
const [nextDismissedKey, setNextDismissedKey] = React.useState(null);
const dismissedRouteName = nextDismissedKey ? state.routes.find(route => route.key === nextDismissedKey)?.name : null;
React.useEffect(() => {
if (dismissedRouteName) {
const message = `The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` + `This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` + `Consider using a 'usePreventRemove' hook with 'headerBackButtonMenuEnabled: false' to prevent users from natively going back multiple screens.`;
console.error(message);
}
}, [dismissedRouteName]);
return {
setNextDismissedKey
};
}
//# sourceMappingURL=useDismissedRouteError.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["React","useDismissedRouteError","state","nextDismissedKey","setNextDismissedKey","useState","dismissedRouteName","routes","find","route","key","name","useEffect","message","console","error"],"sourceRoot":"../../../src","sources":["utils/useDismissedRouteError.tsx"],"mappings":";;AAIA,OAAO,KAAKA,KAAK,MAAM,OAAO;AAE9B,OAAO,SAASC,sBAAsBA,CACpCC,KAA0C,EAC1C;EACA,MAAM,CAACC,gBAAgB,EAAEC,mBAAmB,CAAC,GAAGJ,KAAK,CAACK,QAAQ,CAC5D,IACF,CAAC;EAED,MAAMC,kBAAkB,GAAGH,gBAAgB,GACvCD,KAAK,CAACK,MAAM,CAACC,IAAI,CAAEC,KAAK,IAAKA,KAAK,CAACC,GAAG,KAAKP,gBAAgB,CAAC,EAAEQ,IAAI,GAClE,IAAI;EAERX,KAAK,CAACY,SAAS,CAAC,MAAM;IACpB,IAAIN,kBAAkB,EAAE;MACtB,MAAMO,OAAO,GACX,eAAeP,kBAAkB,+DAA+D,GAChG,6HAA6H,GAC7H,gJAAgJ;MAElJQ,OAAO,CAACC,KAAK,CAACF,OAAO,CAAC;IACxB;EACF,CAAC,EAAE,CAACP,kBAAkB,CAAC,CAAC;EAExB,OAAO;IAAEF;EAAoB,CAAC;AAChC","ignoreList":[]}

View File

@@ -0,0 +1,20 @@
"use strict";
import { usePreventRemoveContext } from '@react-navigation/native';
import * as React from 'react';
export function useInvalidPreventRemoveError(descriptors) {
const {
preventedRoutes
} = usePreventRemoveContext();
const preventedRouteKey = Object.keys(preventedRoutes)[0];
const preventedDescriptor = descriptors[preventedRouteKey];
const isHeaderBackButtonMenuEnabledOnPreventedScreen = preventedDescriptor?.options?.headerBackButtonMenuEnabled;
const preventedRouteName = preventedDescriptor?.route?.name;
React.useEffect(() => {
if (preventedRouteKey != null && isHeaderBackButtonMenuEnabledOnPreventedScreen) {
const message = `The screen ${preventedRouteName} uses 'usePreventRemove' hook alongside 'headerBackButtonMenuEnabled: true', which is not supported. \n\n` + `Consider removing 'headerBackButtonMenuEnabled: true' from ${preventedRouteName} screen to get rid of this error.`;
console.error(message);
}
}, [preventedRouteKey, isHeaderBackButtonMenuEnabledOnPreventedScreen, preventedRouteName]);
}
//# sourceMappingURL=useInvalidPreventRemoveError.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["usePreventRemoveContext","React","useInvalidPreventRemoveError","descriptors","preventedRoutes","preventedRouteKey","Object","keys","preventedDescriptor","isHeaderBackButtonMenuEnabledOnPreventedScreen","options","headerBackButtonMenuEnabled","preventedRouteName","route","name","useEffect","message","console","error"],"sourceRoot":"../../../src","sources":["utils/useInvalidPreventRemoveError.tsx"],"mappings":";;AAAA,SAASA,uBAAuB,QAAQ,0BAA0B;AAClE,OAAO,KAAKC,KAAK,MAAM,OAAO;AAI9B,OAAO,SAASC,4BAA4BA,CAC1CC,WAAqC,EACrC;EACA,MAAM;IAAEC;EAAgB,CAAC,GAAGJ,uBAAuB,CAAC,CAAC;EACrD,MAAMK,iBAAiB,GAAGC,MAAM,CAACC,IAAI,CAACH,eAAe,CAAC,CAAC,CAAC,CAAC;EACzD,MAAMI,mBAAmB,GAAGL,WAAW,CAACE,iBAAiB,CAAC;EAC1D,MAAMI,8CAA8C,GAClDD,mBAAmB,EAAEE,OAAO,EAAEC,2BAA2B;EAC3D,MAAMC,kBAAkB,GAAGJ,mBAAmB,EAAEK,KAAK,EAAEC,IAAI;EAE3Db,KAAK,CAACc,SAAS,CAAC,MAAM;IACpB,IACEV,iBAAiB,IAAI,IAAI,IACzBI,8CAA8C,EAC9C;MACA,MAAMO,OAAO,GACX,cAAcJ,kBAAkB,2GAA2G,GAC3I,8DAA8DA,kBAAkB,mCAAmC;MACrHK,OAAO,CAACC,KAAK,CAACF,OAAO,CAAC;IACxB;EACF,CAAC,EAAE,CACDX,iBAAiB,EACjBI,8CAA8C,EAC9CG,kBAAkB,CACnB,CAAC;AACJ","ignoreList":[]}

View File

@@ -0,0 +1,6 @@
"use strict";
export function processFonts(_) {
throw new Error('Not supported on Web');
}
//# sourceMappingURL=FontProcessor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["processFonts","_","Error"],"sourceRoot":"../../../src","sources":["views/FontProcessor.tsx"],"mappings":";;AAAA,OAAO,SAASA,YAAYA,CAC1BC,CAAyB,EACD;EACxB,MAAM,IAAIC,KAAK,CAAC,sBAAsB,CAAC;AACzC","ignoreList":[]}

View File

@@ -0,0 +1,12 @@
"use strict";
// @ts-expect-error importing private module
import ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
export function processFonts(fontFamilies) {
const fontFamilyProcessor = ReactNativeStyleAttributes.fontFamily?.process;
if (typeof fontFamilyProcessor === 'function') {
return fontFamilies.map(fontFamilyProcessor);
}
return fontFamilies;
}
//# sourceMappingURL=FontProcessor.native.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["ReactNativeStyleAttributes","processFonts","fontFamilies","fontFamilyProcessor","fontFamily","process","map"],"sourceRoot":"../../../src","sources":["views/FontProcessor.native.tsx"],"mappings":";;AAAA;AACA,OAAOA,0BAA0B,MAAM,mEAAmE;AAE1G,OAAO,SAASC,YAAYA,CAC1BC,YAAoC,EACZ;EACxB,MAAMC,mBAAmB,GAAGH,0BAA0B,CAACI,UAAU,EAAEC,OAAO;EAC1E,IAAI,OAAOF,mBAAmB,KAAK,UAAU,EAAE;IAC7C,OAAOD,YAAY,CAACI,GAAG,CAACH,mBAAmB,CAAC;EAC9C;EACA,OAAOD,YAAY;AACrB","ignoreList":[]}

View File

@@ -0,0 +1,14 @@
"use strict";
import React from 'react';
import { ScreenFooter } from 'react-native-screens';
import { jsx as _jsx } from "react/jsx-runtime";
export function FooterComponent({
children
}) {
return /*#__PURE__*/_jsx(ScreenFooter, {
collapsable: false,
children: children
});
}
//# sourceMappingURL=FooterComponent.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["React","ScreenFooter","jsx","_jsx","FooterComponent","children","collapsable"],"sourceRoot":"../../../src","sources":["views/FooterComponent.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,YAAY,QAAQ,sBAAsB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAMpD,OAAO,SAASC,eAAeA,CAAC;EAAEC;AAAsB,CAAC,EAAE;EACzD,oBAAOF,IAAA,CAACF,YAAY;IAACK,WAAW,EAAE,KAAM;IAAAD,QAAA,EAAEA;EAAQ,CAAe,CAAC;AACpE","ignoreList":[]}

View File

@@ -0,0 +1,134 @@
"use strict";
import { getHeaderTitle, Header, HeaderBackButton, HeaderBackContext, SafeAreaProviderCompat, Screen, useHeaderHeight } from '@react-navigation/elements';
import { useLinkBuilder } from '@react-navigation/native';
import * as React from 'react';
import { Animated, Image, StyleSheet, View } from 'react-native';
import { AnimatedHeaderHeightContext } from "../utils/useAnimatedHeaderHeight.js";
import { jsx as _jsx } from "react/jsx-runtime";
const TRANSPARENT_PRESENTATIONS = ['transparentModal', 'containedTransparentModal'];
export function NativeStackView({
state,
descriptors,
describe
}) {
const parentHeaderBack = React.useContext(HeaderBackContext);
const {
buildHref
} = useLinkBuilder();
const preloadedDescriptors = state.preloadedRoutes.reduce((acc, route) => {
acc[route.key] = acc[route.key] || describe(route, true);
return acc;
}, {});
return /*#__PURE__*/_jsx(SafeAreaProviderCompat, {
children: state.routes.concat(state.preloadedRoutes).map((route, i) => {
const isFocused = state.index === i;
const previousKey = state.routes[i - 1]?.key;
const nextKey = state.routes[i + 1]?.key;
const previousDescriptor = previousKey ? descriptors[previousKey] : undefined;
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
const {
options,
navigation,
render
} = descriptors[route.key] ?? preloadedDescriptors[route.key];
const headerBack = previousDescriptor ? {
title: getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name),
href: buildHref(previousDescriptor.route.name, previousDescriptor.route.params)
} : parentHeaderBack;
const canGoBack = headerBack != null;
const {
header,
headerShown,
headerBackIcon,
headerBackImageSource,
headerLeft,
headerTransparent,
headerBackTitle,
presentation,
contentStyle,
...rest
} = options;
const nextPresentation = nextDescriptor?.options.presentation;
const isPreloaded = preloadedDescriptors[route.key] !== undefined && descriptors[route.key] === undefined;
return /*#__PURE__*/_jsx(Screen, {
focused: isFocused,
route: route,
navigation: navigation,
headerShown: headerShown,
headerTransparent: headerTransparent,
header: header !== undefined ? header({
back: headerBack,
options,
route,
navigation
}) : /*#__PURE__*/_jsx(Header, {
...rest,
back: headerBack,
title: getHeaderTitle(options, route.name),
headerLeft: typeof headerLeft === 'function' ? ({
label,
...rest
}) => headerLeft({
...rest,
label: headerBackTitle ?? label
}) : headerLeft === undefined && canGoBack ? ({
tintColor,
label,
...rest
}) => /*#__PURE__*/_jsx(HeaderBackButton, {
...rest,
label: headerBackTitle ?? label,
tintColor: tintColor,
backImage: headerBackIcon !== undefined || headerBackImageSource !== undefined ? () => /*#__PURE__*/_jsx(Image, {
source: headerBackIcon?.source ?? headerBackImageSource,
resizeMode: "contain",
tintColor: tintColor,
style: styles.backImage
}) : undefined,
onPress: navigation.goBack
}) : headerLeft,
headerTransparent: headerTransparent
}),
style: [StyleSheet.absoluteFill, {
display: (isFocused || nextPresentation != null && TRANSPARENT_PRESENTATIONS.includes(nextPresentation)) && !isPreloaded ? 'flex' : 'none'
}, presentation != null && TRANSPARENT_PRESENTATIONS.includes(presentation) ? {
backgroundColor: 'transparent'
} : null],
children: /*#__PURE__*/_jsx(HeaderBackContext.Provider, {
value: headerBack,
children: /*#__PURE__*/_jsx(AnimatedHeaderHeightProvider, {
children: /*#__PURE__*/_jsx(View, {
style: [styles.contentContainer, contentStyle],
children: render()
})
})
})
}, route.key);
})
});
}
const AnimatedHeaderHeightProvider = ({
children
}) => {
const headerHeight = useHeaderHeight();
const [animatedHeaderHeight] = React.useState(() => new Animated.Value(headerHeight));
React.useEffect(() => {
animatedHeaderHeight.setValue(headerHeight);
}, [animatedHeaderHeight, headerHeight]);
return /*#__PURE__*/_jsx(AnimatedHeaderHeightContext.Provider, {
value: animatedHeaderHeight,
children: children
});
};
const styles = StyleSheet.create({
contentContainer: {
flex: 1
},
backImage: {
height: 24,
width: 24,
margin: 3
}
});
//# sourceMappingURL=NativeStackView.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,490 @@
"use strict";
import { getDefaultHeaderHeight, getHeaderTitle, HeaderBackContext, HeaderHeightContext, HeaderShownContext, SafeAreaProviderCompat, useFrameSize } from '@react-navigation/elements';
import { NavigationProvider, StackActions, usePreventRemoveContext, useTheme } from '@react-navigation/native';
import * as React from 'react';
import { Animated, Platform, StatusBar, StyleSheet, useAnimatedValue, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { compatibilityFlags, ScreenStack, ScreenStackItem } from 'react-native-screens';
import { debounce } from "../utils/debounce.js";
import { getModalRouteKeys } from "../utils/getModalRoutesKeys.js";
import { AnimatedHeaderHeightContext } from "../utils/useAnimatedHeaderHeight.js";
import { useDismissedRouteError } from "../utils/useDismissedRouteError.js";
import { useInvalidPreventRemoveError } from "../utils/useInvalidPreventRemoveError.js";
import { useHeaderConfigProps } from "./useHeaderConfigProps.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
function isFabric() {
return 'nativeFabricUIManager' in global;
}
const useNativeDriver = Platform.OS !== 'web';
const SceneView = ({
index,
focused,
shouldFreeze,
descriptor,
previousDescriptor,
nextDescriptor,
isPresentationModal,
isPreloaded,
onWillDisappear,
onWillAppear,
onAppear,
onDisappear,
onDismissed,
onHeaderBackButtonClicked,
onNativeDismissCancelled,
onGestureCancel,
onSheetDetentChanged
}) => {
const {
route,
navigation,
options,
render
} = descriptor;
let {
animation,
animationMatchesGesture,
presentation = isPresentationModal ? 'modal' : 'card',
fullScreenGestureEnabled
} = options;
const {
animationDuration,
animationTypeForReplace = 'push',
fullScreenGestureShadowEnabled = true,
gestureEnabled,
gestureDirection = presentation === 'card' ? 'horizontal' : 'vertical',
gestureResponseDistance,
header,
headerBackButtonMenuEnabled,
headerShown,
headerBackground,
headerTransparent,
autoHideHomeIndicator,
keyboardHandlingEnabled,
navigationBarColor,
navigationBarTranslucent,
navigationBarHidden,
orientation,
sheetAllowedDetents = [1.0],
sheetLargestUndimmedDetentIndex = -1,
sheetGrabberVisible = false,
sheetCornerRadius = -1.0,
sheetElevation = 24,
sheetExpandsWhenScrolledToEdge = true,
sheetInitialDetentIndex = 0,
sheetShouldOverflowTopInset = false,
sheetResizeAnimationEnabled = true,
statusBarAnimation,
statusBarHidden,
statusBarStyle,
statusBarTranslucent,
statusBarBackgroundColor,
unstable_sheetFooter,
scrollEdgeEffects,
freezeOnBlur,
contentStyle
} = options;
if (gestureDirection === 'vertical' && Platform.OS === 'ios') {
// for `vertical` direction to work, we need to set `fullScreenGestureEnabled` to `true`
// so the screen can be dismissed from any point on screen.
// `animationMatchesGesture` needs to be set to `true` so the `animation` set by user can be used,
// otherwise `simple_push` will be used.
// Also, the default animation for this direction seems to be `slide_from_bottom`.
if (fullScreenGestureEnabled === undefined) {
fullScreenGestureEnabled = true;
}
if (animationMatchesGesture === undefined) {
animationMatchesGesture = true;
}
if (animation === undefined) {
animation = 'slide_from_bottom';
}
}
// workaround for rn-screens where gestureDirection has to be set on both
// current and previous screen - software-mansion/react-native-screens/pull/1509
const nextGestureDirection = nextDescriptor?.options.gestureDirection;
const gestureDirectionOverride = nextGestureDirection != null ? nextGestureDirection : gestureDirection;
if (index === 0) {
// first screen should always be treated as `card`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `card`
presentation = 'card';
}
const {
colors
} = useTheme();
const insets = useSafeAreaInsets();
// `modal`, `formSheet` and `pageSheet` presentations do not take whole screen, so should not take the inset.
const isModal = presentation === 'modal' || presentation === 'formSheet' || presentation === 'pageSheet';
// Modals are fullscreen in landscape only on iPhone
const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const parentHeaderBack = React.useContext(HeaderBackContext);
const isLandscape = useFrameSize(frame => frame.width > frame.height);
const topInset = isParentHeaderShown || Platform.OS === 'ios' && isModal || isIPhone && isLandscape ? 0 : insets.top;
const defaultHeaderHeight = useFrameSize(frame => Platform.select({
// FIXME: Currently screens isn't using Material 3
// So our `getDefaultHeaderHeight` doesn't return the correct value
// So we hardcode the value here for now until screens is updated
android: ANDROID_DEFAULT_HEADER_HEIGHT + topInset,
default: getDefaultHeaderHeight(frame, isModal, topInset)
}));
const {
preventedRoutes
} = usePreventRemoveContext();
const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight);
// eslint-disable-next-line react-hooks/exhaustive-deps
const setHeaderHeightDebounced = React.useCallback(
// Debounce the header height updates to avoid excessive re-renders
debounce(setHeaderHeight, 100), []);
const hasCustomHeader = header != null;
const usesNewAndroidHeaderHeightImplementation = 'usesNewAndroidHeaderHeightImplementation' in compatibilityFlags && compatibilityFlags['usesNewAndroidHeaderHeightImplementation'] === true;
let headerHeightCorrectionOffset = 0;
if (Platform.OS === 'android' && !hasCustomHeader && !usesNewAndroidHeaderHeightImplementation) {
const statusBarHeight = StatusBar.currentHeight ?? 0;
// On Android, the native header height is not correctly calculated
// It includes status bar height even if statusbar is not translucent
// And the statusbar value itself doesn't match the actual status bar height
// So we subtract the bogus status bar height and add the actual top inset
headerHeightCorrectionOffset = -statusBarHeight + topInset;
}
const rawAnimatedHeaderHeight = useAnimatedValue(defaultHeaderHeight);
const animatedHeaderHeight = React.useMemo(() => Animated.add(rawAnimatedHeaderHeight, headerHeightCorrectionOffset), [headerHeightCorrectionOffset, rawAnimatedHeaderHeight]);
// During the very first render topInset is > 0 when running
// in non edge-to-edge mode on Android, while on every consecutive render
// topInset === 0, causing header content to jump, as we add padding on the first frame,
// just to remove it in next one. To prevent this, when statusBarTranslucent is set,
// we apply additional padding in header only if its true.
// For more details see: https://github.com/react-navigation/react-navigation/pull/12014
const headerTopInsetEnabled = typeof statusBarTranslucent === 'boolean' ? statusBarTranslucent : topInset !== 0;
const canGoBack = previousDescriptor != null || parentHeaderBack != null;
const backTitle = previousDescriptor ? getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name) : parentHeaderBack?.title;
const headerBack = React.useMemo(() => {
if (canGoBack) {
return {
href: undefined,
// No href needed for native
title: backTitle
};
}
return undefined;
}, [canGoBack, backTitle]);
const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
const headerConfig = useHeaderConfigProps({
...options,
route,
headerBackButtonMenuEnabled: isRemovePrevented !== undefined ? !isRemovePrevented : headerBackButtonMenuEnabled,
headerBackTitle: options.headerBackTitle !== undefined ? options.headerBackTitle : undefined,
headerHeight,
headerShown: header !== undefined ? false : headerShown,
headerTopInsetEnabled,
headerTransparent,
headerBack
});
const onHeaderHeightChange = hasCustomHeader ?
// If we have a custom header, don't use native header height
undefined :
// On Fabric, there's a bug where native event drivers for Animated objects
// are created after the first notifications about the header height
// from the native side, `onHeaderHeightChange` event does not notify
// `animatedHeaderHeight` about initial values on appearing screens at the moment.
Animated.event([{
nativeEvent: {
headerHeight: rawAnimatedHeaderHeight
}
}], {
useNativeDriver,
listener: e => {
if (e.nativeEvent && typeof e.nativeEvent === 'object' && 'headerHeight' in e.nativeEvent && typeof e.nativeEvent.headerHeight === 'number') {
const headerHeight = e.nativeEvent.headerHeight;
// Only debounce if header has large title or search bar
// As it's the only case where the header height can change frequently
const doesHeaderAnimate = Platform.OS === 'ios' && (options.headerLargeTitleEnabled || options.headerSearchBarOptions);
if (doesHeaderAnimate) {
setHeaderHeightDebounced(headerHeight);
} else {
if (Platform.OS === 'android' && headerHeight !== 0 && headerHeight <= ANDROID_DEFAULT_HEADER_HEIGHT) {
// FIXME: On Android, events may get delivered out-of-order
// https://github.com/facebook/react-native/issues/54636
// We seem to get header height without status bar height first,
// and then the correct height with status bar height included
// But due to out-of-order delivery, we may get the correct height first
// and then the one without status bar height
// This is hack to include status bar height if it's not already included
// It only works because header height doesn't change dynamically on Android
setHeaderHeight(headerHeight + insets.top);
} else {
setHeaderHeight(headerHeight);
}
}
}
}
});
return /*#__PURE__*/_jsx(NavigationProvider, {
route: route,
navigation: navigation,
children: /*#__PURE__*/_jsx(ScreenStackItem, {
screenId: route.key,
activityState: isPreloaded ? 0 : 2,
style: StyleSheet.absoluteFill,
"aria-hidden": !focused,
customAnimationOnSwipe: animationMatchesGesture,
fullScreenSwipeEnabled: fullScreenGestureEnabled,
fullScreenSwipeShadowEnabled: fullScreenGestureShadowEnabled,
freezeOnBlur: freezeOnBlur,
gestureEnabled: Platform.OS === 'android' ?
// This prop enables handling of system back gestures on Android
// Since we handle them in JS side, we disable this
false : gestureEnabled,
homeIndicatorHidden: autoHideHomeIndicator,
hideKeyboardOnSwipe: keyboardHandlingEnabled,
navigationBarColor: navigationBarColor,
navigationBarTranslucent: navigationBarTranslucent,
navigationBarHidden: navigationBarHidden,
replaceAnimation: animationTypeForReplace,
stackPresentation: presentation === 'card' ? 'push' : presentation,
stackAnimation: animation,
screenOrientation: orientation,
sheetAllowedDetents: sheetAllowedDetents,
sheetLargestUndimmedDetentIndex: sheetLargestUndimmedDetentIndex,
sheetGrabberVisible: sheetGrabberVisible,
sheetInitialDetentIndex: sheetInitialDetentIndex,
sheetCornerRadius: sheetCornerRadius,
sheetElevation: sheetElevation,
sheetExpandsWhenScrolledToEdge: sheetExpandsWhenScrolledToEdge,
sheetShouldOverflowTopInset: sheetShouldOverflowTopInset,
sheetDefaultResizeAnimationEnabled: sheetResizeAnimationEnabled,
statusBarAnimation: statusBarAnimation,
statusBarHidden: statusBarHidden,
statusBarStyle: statusBarStyle,
statusBarColor: statusBarBackgroundColor,
statusBarTranslucent: statusBarTranslucent,
swipeDirection: gestureDirectionOverride,
transitionDuration: animationDuration,
onWillAppear: onWillAppear,
onWillDisappear: onWillDisappear,
onAppear: onAppear,
onDisappear: onDisappear,
onDismissed: onDismissed,
onGestureCancel: onGestureCancel,
onSheetDetentChanged: onSheetDetentChanged,
gestureResponseDistance: gestureResponseDistance,
nativeBackButtonDismissalEnabled: false // on Android
,
onHeaderBackButtonClicked: onHeaderBackButtonClicked,
preventNativeDismiss: isRemovePrevented // on iOS
,
scrollEdgeEffects: {
bottom: scrollEdgeEffects?.bottom ?? 'automatic',
top: scrollEdgeEffects?.top ?? 'automatic',
left: scrollEdgeEffects?.left ?? 'automatic',
right: scrollEdgeEffects?.right ?? 'automatic'
},
onNativeDismissCancelled: onNativeDismissCancelled,
onHeaderHeightChange: onHeaderHeightChange,
contentStyle: [presentation !== 'transparentModal' && presentation !== 'containedTransparentModal' && {
backgroundColor: colors.background
}, contentStyle],
headerConfig: headerConfig,
unstable_sheetFooter: unstable_sheetFooter
// When ts-expect-error is added, it affects all the props below it
// So we keep any props that need it at the end
// Otherwise invalid props may not be caught by TypeScript
,
shouldFreeze: shouldFreeze,
children: /*#__PURE__*/_jsx(AnimatedHeaderHeightContext.Provider, {
value: animatedHeaderHeight,
children: /*#__PURE__*/_jsxs(HeaderHeightContext.Provider, {
value: headerShown !== false ? headerHeight : parentHeaderHeight ?? 0,
children: [headerBackground != null ?
/*#__PURE__*/
/**
* To show a custom header background, we render it at the top of the screen below the header
* The header also needs to be positioned absolutely (with `translucent` style)
*/
_jsx(View, {
style: [styles.background, headerTransparent ? styles.translucent : null, {
height: headerHeight
}],
children: headerBackground()
}) : null, header != null && headerShown !== false ? /*#__PURE__*/_jsx(View, {
onLayout: e => {
const headerHeight = e.nativeEvent.layout.height;
setHeaderHeight(headerHeight);
rawAnimatedHeaderHeight.setValue(headerHeight);
},
style: [styles.header, headerTransparent ? styles.absolute : null],
children: header({
back: headerBack,
options,
route,
navigation
})
}) : null, /*#__PURE__*/_jsx(HeaderShownContext.Provider, {
value: isParentHeaderShown || headerShown !== false,
children: /*#__PURE__*/_jsx(HeaderBackContext.Provider, {
value: headerBack,
children: render()
})
})]
})
})
})
});
};
export function NativeStackView({
state,
navigation,
descriptors,
describe
}) {
const {
setNextDismissedKey
} = useDismissedRouteError(state);
useInvalidPreventRemoveError(descriptors);
const modalRouteKeys = getModalRouteKeys(state.routes, descriptors);
const preloadedDescriptors = state.preloadedRoutes.reduce((acc, route) => {
acc[route.key] = acc[route.key] || describe(route, true);
return acc;
}, {});
return /*#__PURE__*/_jsx(SafeAreaProviderCompat, {
children: /*#__PURE__*/_jsx(ScreenStack, {
style: styles.container,
children: state.routes.concat(state.preloadedRoutes).map((route, index) => {
const descriptor = descriptors[route.key] ?? preloadedDescriptors[route.key];
const isFocused = state.index === index;
const isBelowFocused = state.index - 1 === index;
const previousKey = state.routes[index - 1]?.key;
const nextKey = state.routes[index + 1]?.key;
const previousDescriptor = previousKey ? descriptors[previousKey] : undefined;
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
const isModal = modalRouteKeys.includes(route.key);
const isModalOnIos = isModal && Platform.OS === 'ios';
const isPreloaded = preloadedDescriptors[route.key] !== undefined && descriptors[route.key] === undefined;
// On Fabric, when screen is frozen, animated and reanimated values are not updated
// due to component being unmounted. To avoid this, we don't freeze the previous screen there
const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos : !isPreloaded && !isFocused && !isModalOnIos;
return /*#__PURE__*/_jsx(SceneView, {
index: index,
focused: isFocused,
shouldFreeze: shouldFreeze,
descriptor: descriptor,
previousDescriptor: previousDescriptor,
nextDescriptor: nextDescriptor,
isPresentationModal: isModal,
isPreloaded: isPreloaded,
onWillDisappear: () => {
navigation.emit({
type: 'transitionStart',
data: {
closing: true
},
target: route.key
});
},
onWillAppear: () => {
navigation.emit({
type: 'transitionStart',
data: {
closing: false
},
target: route.key
});
},
onAppear: () => {
navigation.emit({
type: 'transitionEnd',
data: {
closing: false
},
target: route.key
});
},
onDisappear: () => {
navigation.emit({
type: 'transitionEnd',
data: {
closing: true
},
target: route.key
});
},
onDismissed: event => {
navigation.dispatch({
...StackActions.pop(event.nativeEvent.dismissCount),
source: route.key,
target: state.key
});
setNextDismissedKey(route.key);
},
onHeaderBackButtonClicked: () => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: state.key
});
},
onNativeDismissCancelled: event => {
navigation.dispatch({
...StackActions.pop(event.nativeEvent.dismissCount),
source: route.key,
target: state.key
});
},
onGestureCancel: () => {
navigation.emit({
type: 'gestureCancel',
target: route.key
});
},
onSheetDetentChanged: event => {
navigation.emit({
type: 'sheetDetentChange',
target: route.key,
data: {
index: event.nativeEvent.index,
stable: event.nativeEvent.isStable
}
});
}
}, route.key);
})
})
});
}
const styles = StyleSheet.create({
container: {
flex: 1
},
header: {
zIndex: 1
},
absolute: {
position: 'absolute',
top: 0,
start: 0,
end: 0
},
translucent: {
position: 'absolute',
top: 0,
start: 0,
end: 0,
zIndex: 1,
elevation: 1
},
background: {
overflow: 'hidden'
}
});
//# sourceMappingURL=NativeStackView.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,367 @@
"use strict";
import { getHeaderTitle, HeaderTitle } from '@react-navigation/elements';
import { useLocale, useTheme } from '@react-navigation/native';
import color from 'color';
import { Platform, StyleSheet, View } from 'react-native';
import { isSearchBarAvailableForCurrentPlatform, ScreenStackHeaderBackButtonImage, ScreenStackHeaderCenterView, ScreenStackHeaderLeftView, ScreenStackHeaderRightView, ScreenStackHeaderSearchBarView, SearchBar } from 'react-native-screens';
import { processFonts } from './FontProcessor';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const processBarButtonItems = (items, colors, fonts) => {
return items?.map((item, index) => {
if (item.type === 'custom') {
// Handled with `ScreenStackHeaderLeftView` or `ScreenStackHeaderRightView`
return null;
}
if (item.type === 'spacing') {
if (item.spacing == null) {
throw new Error(`Spacing item must have a 'spacing' property defined: ${JSON.stringify(item)}`);
}
return item;
}
if (item.type === 'button' || item.type === 'menu') {
if (item.type === 'menu' && item.menu == null) {
throw new Error(`Menu item must have a 'menu' property defined: ${JSON.stringify(item)}`);
}
const {
badge,
label,
labelStyle,
icon,
...rest
} = item;
const processedItemCommon = {
...rest,
index,
title: label,
titleStyle: {
...fonts.regular,
...labelStyle
},
icon: transformIcon(icon)
};
let processedItem;
if (processedItemCommon.type === 'menu' && item.type === 'menu') {
const {
multiselectable,
layout
} = item.menu;
processedItem = {
...processedItemCommon,
menu: {
...processedItemCommon.menu,
singleSelection: !multiselectable,
displayAsPalette: layout === 'palette',
items: item.menu.items.map(getMenuItem)
}
};
} else if (processedItemCommon.type === 'button' && item.type === 'button') {
processedItem = processedItemCommon;
} else {
throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button' and 'menu'.`);
}
if (badge) {
const badgeBackgroundColor = badge.style?.backgroundColor ?? colors.notification;
const badgeTextColor = color(badgeBackgroundColor).isLight() ? 'black' : 'white';
processedItem = {
...processedItem,
badge: {
...badge,
value: String(badge.value),
style: {
backgroundColor: badgeBackgroundColor,
color: badgeTextColor,
...fonts.regular,
...badge.style
}
}
};
}
return processedItem;
}
throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`);
}).filter(item => item != null);
};
const transformIcon = icon => {
if (icon?.type === 'image') {
return icon.tinted === false ? {
type: 'imageSource',
imageSource: icon.source
} : {
type: 'templateSource',
templateSource: icon.source
};
}
return icon;
};
const getMenuItem = item => {
if (item.type === 'submenu') {
const {
label,
icon,
inline,
layout,
items,
multiselectable,
...rest
} = item;
return {
...rest,
icon: transformIcon(icon),
title: label,
displayAsPalette: layout === 'palette',
displayInline: inline,
singleSelection: !multiselectable,
items: items.map(getMenuItem)
};
}
const {
label,
icon,
description,
...rest
} = item;
return {
...rest,
icon: transformIcon(icon),
title: label,
subtitle: description
};
};
export function useHeaderConfigProps({
headerBackIcon,
headerBackImageSource,
headerBackButtonDisplayMode,
headerBackButtonMenuEnabled,
headerBackTitle,
headerBackTitleStyle,
headerBackVisible,
headerShadowVisible,
headerLargeStyle,
headerLargeTitle: headerLargeTitleDeprecated,
headerLargeTitleEnabled = headerLargeTitleDeprecated,
headerLargeTitleShadowVisible,
headerLargeTitleStyle,
headerBackground,
headerLeft,
headerRight,
headerShown,
headerStyle,
headerBlurEffect,
headerTintColor,
headerTitle,
headerTitleAlign,
headerTitleStyle,
headerTransparent,
headerSearchBarOptions,
headerTopInsetEnabled,
headerBack,
route,
title,
unstable_headerLeftItems: headerLeftItems,
unstable_headerRightItems: headerRightItems
}) {
const {
direction
} = useLocale();
const {
colors,
fonts,
dark
} = useTheme();
const tintColor = headerTintColor ?? (Platform.OS === 'ios' ? colors.primary : colors.text);
const headerBackTitleStyleFlattened = StyleSheet.flatten([fonts.regular, headerBackTitleStyle]) || {};
const headerLargeTitleStyleFlattened = StyleSheet.flatten([Platform.select({
ios: fonts.heavy,
default: fonts.medium
}), headerLargeTitleStyle]) || {};
const headerTitleStyleFlattened = StyleSheet.flatten([Platform.select({
ios: fonts.bold,
default: fonts.medium
}), headerTitleStyle]) || {};
const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
const [backTitleFontFamily, largeTitleFontFamily, titleFontFamily] = processFonts([headerBackTitleStyleFlattened.fontFamily, headerLargeTitleStyleFlattened.fontFamily, headerTitleStyleFlattened.fontFamily]);
const backTitleFontSize = 'fontSize' in headerBackTitleStyleFlattened ? headerBackTitleStyleFlattened.fontSize : undefined;
const titleText = getHeaderTitle({
title,
headerTitle
}, route.name);
const titleColor = 'color' in headerTitleStyleFlattened ? headerTitleStyleFlattened.color : headerTintColor ?? colors.text;
const titleFontSize = 'fontSize' in headerTitleStyleFlattened ? headerTitleStyleFlattened.fontSize : undefined;
const titleFontWeight = headerTitleStyleFlattened.fontWeight;
const largeTitleBackgroundColor = headerLargeStyleFlattened.backgroundColor;
const largeTitleColor = 'color' in headerLargeTitleStyleFlattened ? headerLargeTitleStyleFlattened.color : undefined;
const largeTitleFontSize = 'fontSize' in headerLargeTitleStyleFlattened ? headerLargeTitleStyleFlattened.fontSize : undefined;
const largeTitleFontWeight = headerLargeTitleStyleFlattened.fontWeight;
const headerTitleStyleSupported = {
color: titleColor
};
if (headerTitleStyleFlattened.fontFamily != null) {
headerTitleStyleSupported.fontFamily = headerTitleStyleFlattened.fontFamily;
}
if (titleFontSize != null) {
headerTitleStyleSupported.fontSize = titleFontSize;
}
if (titleFontWeight != null) {
headerTitleStyleSupported.fontWeight = titleFontWeight;
}
const headerBackgroundColor = headerStyleFlattened.backgroundColor ?? (headerBackground != null || headerTransparent ||
// The title becomes invisible if background color is set with large title on iOS 26
Platform.OS === 'ios' && headerLargeTitleEnabled ? 'transparent' : colors.card);
const canGoBack = headerBack != null;
const headerLeftElement = headerLeft?.({
tintColor,
canGoBack,
label: headerBackTitle ?? headerBack?.title,
// `href` is only applicable to web
href: undefined
});
const headerRightElement = headerRight?.({
tintColor,
canGoBack
});
const headerTitleElement = typeof headerTitle === 'function' ? headerTitle({
tintColor,
children: titleText
}) : null;
const supportsHeaderSearchBar = typeof isSearchBarAvailableForCurrentPlatform === 'boolean' ? isSearchBarAvailableForCurrentPlatform :
// Fallback for older versions of react-native-screens
Platform.OS === 'ios' && SearchBar != null;
const hasHeaderSearchBar = supportsHeaderSearchBar && headerSearchBarOptions != null;
/**
* We need to set this in if:
* - Back button should stay visible when `headerLeft` is specified
* - If `headerTitle` for Android is specified, so we only need to remove the title and keep the back button
*/
const backButtonInCustomView = headerBackVisible || Platform.OS === 'android' && headerTitleElement != null && headerLeftElement == null;
const translucent = headerBackground != null || headerTransparent ||
// When using a SearchBar or large title, the header needs to be translucent for it to work on iOS
(hasHeaderSearchBar || headerLargeTitleEnabled) && Platform.OS === 'ios' && headerTransparent !== false;
const isBackButtonDisplayModeAvailable =
// On iOS 14+
Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 14 && (
// Doesn't have custom styling, by default System, see: https://github.com/software-mansion/react-native-screens/pull/2105#discussion_r1565222738
backTitleFontFamily == null || backTitleFontFamily === 'System') && backTitleFontSize == null &&
// Back button menu is not disabled
headerBackButtonMenuEnabled !== false;
const isCenterViewRenderedAndroid = headerTitleAlign === 'center';
const leftItems = headerLeftItems?.({
tintColor,
canGoBack
});
let rightItems = headerRightItems?.({
tintColor,
canGoBack
});
if (rightItems) {
// iOS renders right items in reverse order
// So we need to reverse them here to match the order
rightItems = [...rightItems].reverse();
}
const children = /*#__PURE__*/_jsxs(_Fragment, {
children: [Platform.OS === 'ios' ? /*#__PURE__*/_jsxs(_Fragment, {
children: [leftItems ? leftItems.map((item, index) => {
if (item.type === 'custom') {
return /*#__PURE__*/_jsx(ScreenStackHeaderLeftView
// eslint-disable-next-line @eslint-react/no-array-index-key
, {
hidesSharedBackground: item.hidesSharedBackground,
children: item.element
}, index);
}
return null;
}) : headerLeftElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderLeftView, {
children: headerLeftElement
}) : null, headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
children: headerTitleElement
}) : null]
}) : /*#__PURE__*/_jsxs(_Fragment, {
children: [headerLeftElement != null || typeof headerTitle === 'function' ?
/*#__PURE__*/
// The style passed to header left, together with title element being wrapped
// in flex view is reqruied for proper header layout, in particular,
// for the text truncation to work.
_jsxs(ScreenStackHeaderLeftView, {
style: !isCenterViewRenderedAndroid ? {
flex: 1
} : null,
children: [headerLeftElement, headerTitleAlign !== 'center' ? typeof headerTitle === 'function' ? /*#__PURE__*/_jsx(View, {
style: {
flex: 1
},
children: headerTitleElement
}) : /*#__PURE__*/_jsx(View, {
style: {
flex: 1
},
children: /*#__PURE__*/_jsx(HeaderTitle, {
tintColor: tintColor,
style: headerTitleStyleSupported,
children: titleText
})
}) : null]
}) : null, isCenterViewRenderedAndroid ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
children: typeof headerTitle === 'function' ? headerTitleElement : /*#__PURE__*/_jsx(HeaderTitle, {
tintColor: tintColor,
style: headerTitleStyleSupported,
children: titleText
})
}) : null]
}), headerBackIcon !== undefined || headerBackImageSource !== undefined ? /*#__PURE__*/_jsx(ScreenStackHeaderBackButtonImage, {
source: headerBackIcon?.source ?? headerBackImageSource
}) : null, Platform.OS === 'ios' && rightItems ? rightItems.map((item, index) => {
if (item.type === 'custom') {
return /*#__PURE__*/_jsx(ScreenStackHeaderRightView
// eslint-disable-next-line @eslint-react/no-array-index-key
, {
hidesSharedBackground: item.hidesSharedBackground,
children: item.element
}, index);
}
return null;
}) : headerRightElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderRightView, {
children: headerRightElement
}) : null, hasHeaderSearchBar ? /*#__PURE__*/_jsx(ScreenStackHeaderSearchBarView, {
children: /*#__PURE__*/_jsx(SearchBar, {
...headerSearchBarOptions
})
}) : null]
});
return {
backButtonInCustomView,
backgroundColor: headerBackgroundColor,
backTitle: headerBackTitle,
backTitleVisible: isBackButtonDisplayModeAvailable ? undefined : headerBackButtonDisplayMode !== 'minimal',
backButtonDisplayMode: isBackButtonDisplayModeAvailable ? headerBackButtonDisplayMode : undefined,
backTitleFontFamily,
backTitleFontSize,
blurEffect: headerBlurEffect,
color: tintColor,
direction,
disableBackButtonMenu: headerBackButtonMenuEnabled === false,
hidden: headerShown === false,
hideBackButton: headerBackVisible === false,
hideShadow: headerShadowVisible === false || headerBackground != null || headerTransparent && headerShadowVisible !== true,
largeTitle: headerLargeTitleEnabled,
largeTitleBackgroundColor,
largeTitleColor,
largeTitleFontFamily,
largeTitleFontSize,
largeTitleFontWeight,
largeTitleHideShadow: headerLargeTitleShadowVisible === false,
title: titleText,
titleColor,
titleFontFamily,
titleFontSize,
titleFontWeight: String(titleFontWeight),
topInsetEnabled: headerTopInsetEnabled,
translucent: translucent === true,
children,
headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
experimental_userInterfaceStyle: dark ? 'dark' : 'light'
};
}
//# sourceMappingURL=useHeaderConfigProps.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"type":"module"}

View File

@@ -0,0 +1,17 @@
/**
* Navigators
*/
export { createNativeStackNavigator } from './navigators/createNativeStackNavigator';
/**
* Views
*/
export { NativeStackView } from './views/NativeStackView';
/**
* Hooks
*/
export { useAnimatedHeaderHeight } from './utils/useAnimatedHeaderHeight';
/**
* Types
*/
export type { NativeStackHeaderBackProps, NativeStackHeaderItem, NativeStackHeaderItemButton, NativeStackHeaderItemCustom, NativeStackHeaderItemMenu, NativeStackHeaderItemMenuAction, NativeStackHeaderItemMenuSubmenu, NativeStackHeaderItemProps, NativeStackHeaderItemSpacing, NativeStackHeaderLeftProps, NativeStackHeaderProps, NativeStackHeaderRightProps, NativeStackNavigationEventMap, NativeStackNavigationOptions, NativeStackNavigationProp, NativeStackNavigatorProps, NativeStackOptionsArgs, NativeStackScreenProps, } from './types';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,0BAA0B,EAAE,MAAM,yCAAyC,CAAC;AAErF;;GAEG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;GAEG;AACH,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E;;GAEG;AACH,YAAY,EACV,0BAA0B,EAC1B,qBAAqB,EACrB,2BAA2B,EAC3B,2BAA2B,EAC3B,yBAAyB,EACzB,+BAA+B,EAC/B,gCAAgC,EAChC,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAC1B,sBAAsB,EACtB,2BAA2B,EAC3B,6BAA6B,EAC7B,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,SAAS,CAAC"}

View File

@@ -0,0 +1,16 @@
import { type NavigatorTypeBagBase, type ParamListBase, type StackNavigationState, type StaticConfig, type TypedNavigator } from '@react-navigation/native';
import type { NativeStackNavigationEventMap, NativeStackNavigationOptions, NativeStackNavigationProp, NativeStackNavigatorProps } from '../types';
declare function NativeStackNavigator({ id, initialRouteName, UNSTABLE_routeNamesChangeBehavior, children, layout, screenListeners, screenOptions, screenLayout, UNSTABLE_router, ...rest }: NativeStackNavigatorProps): import("react/jsx-runtime").JSX.Element;
export declare function createNativeStackNavigator<const ParamList extends ParamListBase, const NavigatorID extends string | undefined = string | undefined, const TypeBag extends NavigatorTypeBagBase = {
ParamList: ParamList;
NavigatorID: NavigatorID;
State: StackNavigationState<ParamList>;
ScreenOptions: NativeStackNavigationOptions;
EventMap: NativeStackNavigationEventMap;
NavigationList: {
[RouteName in keyof ParamList]: NativeStackNavigationProp<ParamList, RouteName, NavigatorID>;
};
Navigator: typeof NativeStackNavigator;
}, const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>>(config?: Config): TypedNavigator<TypeBag, Config>;
export {};
//# sourceMappingURL=createNativeStackNavigator.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"createNativeStackNavigator.d.ts","sourceRoot":"","sources":["../../../../src/navigators/createNativeStackNavigator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAGlB,KAAK,oBAAoB,EAGzB,KAAK,YAAY,EACjB,KAAK,cAAc,EAEpB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,EAC1B,MAAM,UAAU,CAAC;AAGlB,iBAAS,oBAAoB,CAAC,EAC5B,EAAE,EACF,gBAAgB,EAChB,iCAAiC,EACjC,QAAQ,EACR,MAAM,EACN,eAAe,EACf,aAAa,EACb,YAAY,EACZ,eAAe,EACf,GAAG,IAAI,EACR,EAAE,yBAAyB,2CA+D3B;AAED,wBAAgB,0BAA0B,CACxC,KAAK,CAAC,SAAS,SAAS,aAAa,EACrC,KAAK,CAAC,WAAW,SAAS,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,EACjE,KAAK,CAAC,OAAO,SAAS,oBAAoB,GAAG;IAC3C,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC;IACzB,KAAK,EAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC;IACvC,aAAa,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,EAAE,6BAA6B,CAAC;IACxC,cAAc,EAAE;SACb,SAAS,IAAI,MAAM,SAAS,GAAG,yBAAyB,CACvD,SAAS,EACT,SAAS,EACT,WAAW,CACZ;KACF,CAAC;IACF,SAAS,EAAE,OAAO,oBAAoB,CAAC;CACxC,EACD,KAAK,CAAC,MAAM,SAAS,YAAY,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,EAClE,MAAM,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,MAAM,CAAC,CAElD"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export declare function debounce<T extends (...args: any[]) => void>(func: T, duration: number): T;
//# sourceMappingURL=debounce.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"debounce.d.ts","sourceRoot":"","sources":["../../../../src/utils/debounce.tsx"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,EACzD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,MAAM,GACf,CAAC,CAUH"}

View File

@@ -0,0 +1,4 @@
import type { Route } from '@react-navigation/native';
import type { NativeStackDescriptorMap } from '../types';
export declare const getModalRouteKeys: (routes: Route<string>[], descriptors: NativeStackDescriptorMap) => string[];
//# sourceMappingURL=getModalRoutesKeys.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"getModalRoutesKeys.d.ts","sourceRoot":"","sources":["../../../../src/utils/getModalRoutesKeys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAEtD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,KAAK,CAAC,MAAM,CAAC,EAAE,EACvB,aAAa,wBAAwB,aAmB/B,CAAC"}

View File

@@ -0,0 +1,5 @@
import * as React from 'react';
import type { Animated } from 'react-native';
export declare const AnimatedHeaderHeightContext: React.Context<Animated.AnimatedInterpolation<number> | undefined>;
export declare function useAnimatedHeaderHeight(): Animated.AnimatedInterpolation<number>;
//# sourceMappingURL=useAnimatedHeaderHeight.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"useAnimatedHeaderHeight.d.ts","sourceRoot":"","sources":["../../../../src/utils/useAnimatedHeaderHeight.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,eAAO,MAAM,2BAA2B,mEAE5B,CAAC;AAEb,wBAAgB,uBAAuB,2CAUtC"}

View File

@@ -0,0 +1,6 @@
import type { ParamListBase, StackNavigationState } from '@react-navigation/native';
import * as React from 'react';
export declare function useDismissedRouteError(state: StackNavigationState<ParamListBase>): {
setNextDismissedKey: React.Dispatch<React.SetStateAction<string | null>>;
};
//# sourceMappingURL=useDismissedRouteError.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"useDismissedRouteError.d.ts","sourceRoot":"","sources":["../../../../src/utils/useDismissedRouteError.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC;;EAsB3C"}

View File

@@ -0,0 +1,3 @@
import type { NativeStackDescriptorMap } from '../types';
export declare function useInvalidPreventRemoveError(descriptors: NativeStackDescriptorMap): void;
//# sourceMappingURL=useInvalidPreventRemoveError.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"useInvalidPreventRemoveError.d.ts","sourceRoot":"","sources":["../../../../src/utils/useInvalidPreventRemoveError.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAEzD,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,wBAAwB,QAwBtC"}

View File

@@ -0,0 +1,2 @@
export declare function processFonts(_: (string | undefined)[]): (string | undefined)[];
//# sourceMappingURL=FontProcessor.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FontProcessor.d.ts","sourceRoot":"","sources":["../../../../src/views/FontProcessor.tsx"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAC1B,CAAC,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACxB,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAExB"}

View File

@@ -0,0 +1,2 @@
export declare function processFonts(fontFamilies: (string | undefined)[]): (string | undefined)[];
//# sourceMappingURL=FontProcessor.native.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FontProcessor.native.d.ts","sourceRoot":"","sources":["../../../../src/views/FontProcessor.native.tsx"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAC1B,YAAY,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GACnC,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAMxB"}

View File

@@ -0,0 +1,7 @@
import React from 'react';
type FooterProps = {
children?: React.ReactNode;
};
export declare function FooterComponent({ children }: FooterProps): import("react/jsx-runtime").JSX.Element;
export {};
//# sourceMappingURL=FooterComponent.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FooterComponent.d.ts","sourceRoot":"","sources":["../../../../src/views/FooterComponent.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,KAAK,WAAW,GAAG;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,CAAC;AAEF,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW,2CAExD"}

View File

@@ -0,0 +1,11 @@
import { type ParamListBase, type RouteProp, type StackNavigationState } from '@react-navigation/native';
import type { NativeStackDescriptor, NativeStackDescriptorMap, NativeStackNavigationHelpers } from '../types';
type Props = {
state: StackNavigationState<ParamListBase>;
navigation: NativeStackNavigationHelpers;
descriptors: NativeStackDescriptorMap;
describe: (route: RouteProp<ParamListBase>, placeholder: boolean) => NativeStackDescriptor;
};
export declare function NativeStackView({ state, descriptors, describe }: Props): import("react/jsx-runtime").JSX.Element;
export {};
//# sourceMappingURL=NativeStackView.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"NativeStackView.d.ts","sourceRoot":"","sources":["../../../../src/views/NativeStackView.tsx"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,oBAAoB,EAE1B,MAAM,0BAA0B,CAAC;AAIlC,OAAO,KAAK,EACV,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC7B,MAAM,UAAU,CAAC;AAGlB,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAE3C,UAAU,EAAE,4BAA4B,CAAC;IACzC,WAAW,EAAE,wBAAwB,CAAC;IACtC,QAAQ,EAAE,CACR,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,EAC/B,WAAW,EAAE,OAAO,KACjB,qBAAqB,CAAC;CAC5B,CAAC;AAOF,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAiJtE"}

View File

@@ -0,0 +1,11 @@
import { type ParamListBase, type RouteProp, type StackNavigationState } from '@react-navigation/native';
import type { NativeStackDescriptor, NativeStackDescriptorMap, NativeStackNavigationHelpers } from '../types';
type Props = {
state: StackNavigationState<ParamListBase>;
navigation: NativeStackNavigationHelpers;
descriptors: NativeStackDescriptorMap;
describe: (route: RouteProp<ParamListBase>, placeholder: boolean) => NativeStackDescriptor;
};
export declare function NativeStackView({ state, navigation, descriptors, describe, }: Props): import("react/jsx-runtime").JSX.Element;
export {};
//# sourceMappingURL=NativeStackView.native.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"NativeStackView.native.d.ts","sourceRoot":"","sources":["../../../../src/views/NativeStackView.native.tsx"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,SAAS,EAEd,KAAK,oBAAoB,EAG1B,MAAM,0BAA0B,CAAC;AAkBlC,OAAO,KAAK,EACV,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC7B,MAAM,UAAU,CAAC;AAiclB,KAAK,KAAK,GAAG;IACX,KAAK,EAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAC3C,UAAU,EAAE,4BAA4B,CAAC;IACzC,WAAW,EAAE,wBAAwB,CAAC;IACtC,QAAQ,EAAE,CACR,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,EAC/B,WAAW,EAAE,OAAO,KACjB,qBAAqB,CAAC;CAC5B,CAAC;AAEF,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,GACT,EAAE,KAAK,2CA6HP"}

View File

@@ -0,0 +1,15 @@
import { type Route } from '@react-navigation/native';
import { type ScreenStackHeaderConfigProps } from 'react-native-screens';
import type { NativeStackNavigationOptions } from '../types';
type Props = NativeStackNavigationOptions & {
headerTopInsetEnabled: boolean;
headerHeight: number;
headerBack: {
title?: string | undefined;
href: undefined;
} | undefined;
route: Route<string>;
};
export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }: Props): ScreenStackHeaderConfigProps;
export {};
//# sourceMappingURL=useHeaderConfigProps.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"useHeaderConfigProps.d.ts","sourceRoot":"","sources":["../../../../src/views/useHeaderConfigProps.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,KAAK,EAIX,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAQL,KAAK,4BAA4B,EAKlC,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,UAAU,CAAC;AAGlB,KAAK,KAAK,GAAG,4BAA4B,GAAG;IAC1C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC;IACxE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB,CAAC;AAqJF,wBAAgB,oBAAoB,CAAC,EACnC,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,2BAA2B,EAC3B,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAAE,0BAA0B,EAC5C,uBAAoD,EACpD,6BAA6B,EAC7B,qBAAqB,EACrB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,UAAU,EACV,KAAK,EACL,KAAK,EACL,wBAAwB,EAAE,eAAe,EACzC,yBAAyB,EAAE,gBAAgB,GAC5C,EAAE,KAAK,GAAG,4BAA4B,CAiTtC"}

View File

@@ -0,0 +1,93 @@
{
"name": "@react-navigation/native-stack",
"description": "Native stack navigator using react-native-screens",
"version": "7.14.4",
"keywords": [
"react-native-component",
"react-component",
"react-native",
"react-navigation",
"ios",
"android",
"native",
"stack"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/react-navigation/react-navigation.git",
"directory": "packages/native-stack"
},
"bugs": {
"url": "https://github.com/software-mansion/react-native-screens/issues"
},
"homepage": "https://github.com/software-mansion/react-native-screens#readme",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
"exports": {
".": {
"source": "./src/index.tsx",
"types": "./lib/typescript/src/index.d.ts",
"default": "./lib/module/index.js"
},
"./package.json": "./package.json"
},
"files": [
"src",
"lib",
"!**/__tests__"
],
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"prepack": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/elements": "^2.9.10",
"color": "^4.2.3",
"sf-symbols-typescript": "^2.1.0",
"warn-once": "^0.1.1"
},
"devDependencies": {
"@jest/globals": "^30.0.0",
"@react-navigation/native": "^7.1.33",
"@testing-library/react-native": "^13.2.1",
"@types/react": "~19.0.10",
"del-cli": "^6.0.0",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-builder-bob": "^0.40.17",
"react-native-screens": "^4.24.0",
"react-test-renderer": "19.1.0",
"typescript": "^5.9.2"
},
"peerDependencies": {
"@react-navigation/native": "^7.1.33",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
},
"react-native-builder-bob": {
"source": "src",
"output": "lib",
"targets": [
[
"module",
{
"esm": true
}
],
[
"typescript",
{
"project": "tsconfig.build.json"
}
]
]
},
"gitHead": "69573410534dc9a467ace6e749e82d32058b3092"
}

View File

@@ -0,0 +1,38 @@
/**
* Navigators
*/
export { createNativeStackNavigator } from './navigators/createNativeStackNavigator';
/**
* Views
*/
export { NativeStackView } from './views/NativeStackView';
/**
* Hooks
*/
export { useAnimatedHeaderHeight } from './utils/useAnimatedHeaderHeight';
/**
* Types
*/
export type {
NativeStackHeaderBackProps,
NativeStackHeaderItem,
NativeStackHeaderItemButton,
NativeStackHeaderItemCustom,
NativeStackHeaderItemMenu,
NativeStackHeaderItemMenuAction,
NativeStackHeaderItemMenuSubmenu,
NativeStackHeaderItemProps,
NativeStackHeaderItemSpacing,
NativeStackHeaderLeftProps,
NativeStackHeaderProps,
NativeStackHeaderRightProps,
NativeStackNavigationEventMap,
NativeStackNavigationOptions,
NativeStackNavigationProp,
NativeStackNavigatorProps,
NativeStackOptionsArgs,
NativeStackScreenProps,
} from './types';

View File

@@ -0,0 +1,123 @@
import {
createNavigatorFactory,
type EventArg,
NavigationMetaContext,
type NavigatorTypeBagBase,
type ParamListBase,
type StackActionHelpers,
StackActions,
type StackNavigationState,
StackRouter,
type StackRouterOptions,
type StaticConfig,
type TypedNavigator,
useNavigationBuilder,
} from '@react-navigation/native';
import * as React from 'react';
import type {
NativeStackNavigationEventMap,
NativeStackNavigationOptions,
NativeStackNavigationProp,
NativeStackNavigatorProps,
} from '../types';
import { NativeStackView } from '../views/NativeStackView';
function NativeStackNavigator({
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router,
...rest
}: NativeStackNavigatorProps) {
const { state, describe, descriptors, navigation, NavigationContent } =
useNavigationBuilder<
StackNavigationState<ParamListBase>,
StackRouterOptions,
StackActionHelpers<ParamListBase>,
NativeStackNavigationOptions,
NativeStackNavigationEventMap
>(StackRouter, {
id,
initialRouteName,
UNSTABLE_routeNamesChangeBehavior,
children,
layout,
screenListeners,
screenOptions,
screenLayout,
UNSTABLE_router,
});
const meta = React.useContext(NavigationMetaContext);
React.useEffect(() => {
if (meta && 'type' in meta && meta.type === 'native-tabs') {
// If we're inside native tabs, we don't need to handle popToTop
// It's handled natively by native tabs
return;
}
// @ts-expect-error: there may not be a tab navigator in parent
return navigation?.addListener?.('tabPress', (e: any) => {
const isFocused = navigation.isFocused();
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (
state.index > 0 &&
isFocused &&
!(e as EventArg<'tabPress', true>).defaultPrevented
) {
// When user taps on already focused tab and we're inside the tab,
// reset the stack to replicate native behaviour
navigation.dispatch({
...StackActions.popToTop(),
target: state.key,
});
}
});
});
}, [meta, navigation, state.index, state.key]);
return (
<NavigationContent>
<NativeStackView
{...rest}
state={state}
navigation={navigation}
descriptors={descriptors}
describe={describe}
/>
</NavigationContent>
);
}
export function createNativeStackNavigator<
const ParamList extends ParamListBase,
const NavigatorID extends string | undefined = string | undefined,
const TypeBag extends NavigatorTypeBagBase = {
ParamList: ParamList;
NavigatorID: NavigatorID;
State: StackNavigationState<ParamList>;
ScreenOptions: NativeStackNavigationOptions;
EventMap: NativeStackNavigationEventMap;
NavigationList: {
[RouteName in keyof ParamList]: NativeStackNavigationProp<
ParamList,
RouteName,
NavigatorID
>;
};
Navigator: typeof NativeStackNavigator;
},
const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
>(config?: Config): TypedNavigator<TypeBag, Config> {
return createNavigatorFactory(NativeStackNavigator)(config);
}

1205
node_modules/@react-navigation/native-stack/src/types.tsx generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
export function debounce<T extends (...args: any[]) => void>(
func: T,
duration: number
): T {
let timeout: ReturnType<typeof setTimeout>;
return function (this: unknown, ...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, duration);
} as T;
}

View File

@@ -0,0 +1,26 @@
import type { Route } from '@react-navigation/native';
import type { NativeStackDescriptorMap } from '../types';
export const getModalRouteKeys = (
routes: Route<string>[],
descriptors: NativeStackDescriptorMap
) =>
routes.reduce<string[]>((acc, route) => {
const { presentation } = descriptors[route.key]?.options ?? {};
if (
(acc.length && !presentation) ||
presentation === 'modal' ||
presentation === 'transparentModal' ||
presentation === 'containedModal' ||
presentation === 'containedTransparentModal' ||
presentation === 'fullScreenModal' ||
presentation === 'formSheet' ||
presentation === 'pageSheet'
) {
acc.push(route.key);
}
return acc;
}, []);

View File

@@ -0,0 +1,18 @@
import * as React from 'react';
import type { Animated } from 'react-native';
export const AnimatedHeaderHeightContext = React.createContext<
Animated.AnimatedInterpolation<number> | undefined
>(undefined);
export function useAnimatedHeaderHeight() {
const animatedValue = React.useContext(AnimatedHeaderHeightContext);
if (animatedValue === undefined) {
throw new Error(
"Couldn't find the header height. Are you inside a screen in a native stack navigator?"
);
}
return animatedValue;
}

View File

@@ -0,0 +1,30 @@
import type {
ParamListBase,
StackNavigationState,
} from '@react-navigation/native';
import * as React from 'react';
export function useDismissedRouteError(
state: StackNavigationState<ParamListBase>
) {
const [nextDismissedKey, setNextDismissedKey] = React.useState<string | null>(
null
);
const dismissedRouteName = nextDismissedKey
? state.routes.find((route) => route.key === nextDismissedKey)?.name
: null;
React.useEffect(() => {
if (dismissedRouteName) {
const message =
`The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` +
`This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` +
`Consider using a 'usePreventRemove' hook with 'headerBackButtonMenuEnabled: false' to prevent users from natively going back multiple screens.`;
console.error(message);
}
}, [dismissedRouteName]);
return { setNextDismissedKey };
}

View File

@@ -0,0 +1,31 @@
import { usePreventRemoveContext } from '@react-navigation/native';
import * as React from 'react';
import type { NativeStackDescriptorMap } from '../types';
export function useInvalidPreventRemoveError(
descriptors: NativeStackDescriptorMap
) {
const { preventedRoutes } = usePreventRemoveContext();
const preventedRouteKey = Object.keys(preventedRoutes)[0];
const preventedDescriptor = descriptors[preventedRouteKey];
const isHeaderBackButtonMenuEnabledOnPreventedScreen =
preventedDescriptor?.options?.headerBackButtonMenuEnabled;
const preventedRouteName = preventedDescriptor?.route?.name;
React.useEffect(() => {
if (
preventedRouteKey != null &&
isHeaderBackButtonMenuEnabledOnPreventedScreen
) {
const message =
`The screen ${preventedRouteName} uses 'usePreventRemove' hook alongside 'headerBackButtonMenuEnabled: true', which is not supported. \n\n` +
`Consider removing 'headerBackButtonMenuEnabled: true' from ${preventedRouteName} screen to get rid of this error.`;
console.error(message);
}
}, [
preventedRouteKey,
isHeaderBackButtonMenuEnabledOnPreventedScreen,
preventedRouteName,
]);
}

View File

@@ -0,0 +1,12 @@
// @ts-expect-error importing private module
import ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes';
export function processFonts(
fontFamilies: (string | undefined)[]
): (string | undefined)[] {
const fontFamilyProcessor = ReactNativeStyleAttributes.fontFamily?.process;
if (typeof fontFamilyProcessor === 'function') {
return fontFamilies.map(fontFamilyProcessor);
}
return fontFamilies;
}

View File

@@ -0,0 +1,5 @@
export function processFonts(
_: (string | undefined)[]
): (string | undefined)[] {
throw new Error('Not supported on Web');
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { ScreenFooter } from 'react-native-screens';
type FooterProps = {
children?: React.ReactNode;
};
export function FooterComponent({ children }: FooterProps) {
return <ScreenFooter collapsable={false}>{children}</ScreenFooter>;
}

View File

@@ -0,0 +1,655 @@
import {
getDefaultHeaderHeight,
getHeaderTitle,
HeaderBackContext,
HeaderHeightContext,
HeaderShownContext,
SafeAreaProviderCompat,
useFrameSize,
} from '@react-navigation/elements';
import {
NavigationProvider,
type ParamListBase,
type RouteProp,
StackActions,
type StackNavigationState,
usePreventRemoveContext,
useTheme,
} from '@react-navigation/native';
import * as React from 'react';
import {
Animated,
Platform,
StatusBar,
StyleSheet,
useAnimatedValue,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
compatibilityFlags,
type ScreenProps,
ScreenStack,
ScreenStackItem,
} from 'react-native-screens';
import type {
NativeStackDescriptor,
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
} from '../types';
import { debounce } from '../utils/debounce';
import { getModalRouteKeys } from '../utils/getModalRoutesKeys';
import { AnimatedHeaderHeightContext } from '../utils/useAnimatedHeaderHeight';
import { useDismissedRouteError } from '../utils/useDismissedRouteError';
import { useInvalidPreventRemoveError } from '../utils/useInvalidPreventRemoveError';
import { useHeaderConfigProps } from './useHeaderConfigProps';
const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
function isFabric() {
return 'nativeFabricUIManager' in global;
}
type SceneViewProps = {
index: number;
focused: boolean;
shouldFreeze: boolean;
descriptor: NativeStackDescriptor;
previousDescriptor?: NativeStackDescriptor;
nextDescriptor?: NativeStackDescriptor;
isPresentationModal?: boolean;
isPreloaded?: boolean;
onWillDisappear: () => void;
onWillAppear: () => void;
onAppear: () => void;
onDisappear: () => void;
onDismissed: ScreenProps['onDismissed'];
onHeaderBackButtonClicked: ScreenProps['onHeaderBackButtonClicked'];
onNativeDismissCancelled: ScreenProps['onDismissed'];
onGestureCancel: ScreenProps['onGestureCancel'];
onSheetDetentChanged: ScreenProps['onSheetDetentChanged'];
};
const useNativeDriver = Platform.OS !== 'web';
const SceneView = ({
index,
focused,
shouldFreeze,
descriptor,
previousDescriptor,
nextDescriptor,
isPresentationModal,
isPreloaded,
onWillDisappear,
onWillAppear,
onAppear,
onDisappear,
onDismissed,
onHeaderBackButtonClicked,
onNativeDismissCancelled,
onGestureCancel,
onSheetDetentChanged,
}: SceneViewProps) => {
const { route, navigation, options, render } = descriptor;
let {
animation,
animationMatchesGesture,
presentation = isPresentationModal ? 'modal' : 'card',
fullScreenGestureEnabled,
} = options;
const {
animationDuration,
animationTypeForReplace = 'push',
fullScreenGestureShadowEnabled = true,
gestureEnabled,
gestureDirection = presentation === 'card' ? 'horizontal' : 'vertical',
gestureResponseDistance,
header,
headerBackButtonMenuEnabled,
headerShown,
headerBackground,
headerTransparent,
autoHideHomeIndicator,
keyboardHandlingEnabled,
navigationBarColor,
navigationBarTranslucent,
navigationBarHidden,
orientation,
sheetAllowedDetents = [1.0],
sheetLargestUndimmedDetentIndex = -1,
sheetGrabberVisible = false,
sheetCornerRadius = -1.0,
sheetElevation = 24,
sheetExpandsWhenScrolledToEdge = true,
sheetInitialDetentIndex = 0,
sheetShouldOverflowTopInset = false,
sheetResizeAnimationEnabled = true,
statusBarAnimation,
statusBarHidden,
statusBarStyle,
statusBarTranslucent,
statusBarBackgroundColor,
unstable_sheetFooter,
scrollEdgeEffects,
freezeOnBlur,
contentStyle,
} = options;
if (gestureDirection === 'vertical' && Platform.OS === 'ios') {
// for `vertical` direction to work, we need to set `fullScreenGestureEnabled` to `true`
// so the screen can be dismissed from any point on screen.
// `animationMatchesGesture` needs to be set to `true` so the `animation` set by user can be used,
// otherwise `simple_push` will be used.
// Also, the default animation for this direction seems to be `slide_from_bottom`.
if (fullScreenGestureEnabled === undefined) {
fullScreenGestureEnabled = true;
}
if (animationMatchesGesture === undefined) {
animationMatchesGesture = true;
}
if (animation === undefined) {
animation = 'slide_from_bottom';
}
}
// workaround for rn-screens where gestureDirection has to be set on both
// current and previous screen - software-mansion/react-native-screens/pull/1509
const nextGestureDirection = nextDescriptor?.options.gestureDirection;
const gestureDirectionOverride =
nextGestureDirection != null ? nextGestureDirection : gestureDirection;
if (index === 0) {
// first screen should always be treated as `card`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `card`
presentation = 'card';
}
const { colors } = useTheme();
const insets = useSafeAreaInsets();
// `modal`, `formSheet` and `pageSheet` presentations do not take whole screen, so should not take the inset.
const isModal =
presentation === 'modal' ||
presentation === 'formSheet' ||
presentation === 'pageSheet';
// Modals are fullscreen in landscape only on iPhone
const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const parentHeaderBack = React.useContext(HeaderBackContext);
const isLandscape = useFrameSize((frame) => frame.width > frame.height);
const topInset =
isParentHeaderShown ||
(Platform.OS === 'ios' && isModal) ||
(isIPhone && isLandscape)
? 0
: insets.top;
const defaultHeaderHeight = useFrameSize((frame) =>
Platform.select({
// FIXME: Currently screens isn't using Material 3
// So our `getDefaultHeaderHeight` doesn't return the correct value
// So we hardcode the value here for now until screens is updated
android: ANDROID_DEFAULT_HEADER_HEIGHT + topInset,
default: getDefaultHeaderHeight(frame, isModal, topInset),
})
);
const { preventedRoutes } = usePreventRemoveContext();
const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight);
// eslint-disable-next-line react-hooks/exhaustive-deps
const setHeaderHeightDebounced = React.useCallback(
// Debounce the header height updates to avoid excessive re-renders
debounce(setHeaderHeight, 100),
[]
);
const hasCustomHeader = header != null;
const usesNewAndroidHeaderHeightImplementation =
'usesNewAndroidHeaderHeightImplementation' in compatibilityFlags &&
compatibilityFlags['usesNewAndroidHeaderHeightImplementation'] === true;
let headerHeightCorrectionOffset = 0;
if (
Platform.OS === 'android' &&
!hasCustomHeader &&
!usesNewAndroidHeaderHeightImplementation
) {
const statusBarHeight = StatusBar.currentHeight ?? 0;
// On Android, the native header height is not correctly calculated
// It includes status bar height even if statusbar is not translucent
// And the statusbar value itself doesn't match the actual status bar height
// So we subtract the bogus status bar height and add the actual top inset
headerHeightCorrectionOffset = -statusBarHeight + topInset;
}
const rawAnimatedHeaderHeight = useAnimatedValue(defaultHeaderHeight);
const animatedHeaderHeight = React.useMemo(
() =>
Animated.add<number>(
rawAnimatedHeaderHeight,
headerHeightCorrectionOffset
),
[headerHeightCorrectionOffset, rawAnimatedHeaderHeight]
);
// During the very first render topInset is > 0 when running
// in non edge-to-edge mode on Android, while on every consecutive render
// topInset === 0, causing header content to jump, as we add padding on the first frame,
// just to remove it in next one. To prevent this, when statusBarTranslucent is set,
// we apply additional padding in header only if its true.
// For more details see: https://github.com/react-navigation/react-navigation/pull/12014
const headerTopInsetEnabled =
typeof statusBarTranslucent === 'boolean'
? statusBarTranslucent
: topInset !== 0;
const canGoBack = previousDescriptor != null || parentHeaderBack != null;
const backTitle = previousDescriptor
? getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name)
: parentHeaderBack?.title;
const headerBack = React.useMemo(() => {
if (canGoBack) {
return {
href: undefined, // No href needed for native
title: backTitle,
};
}
return undefined;
}, [canGoBack, backTitle]);
const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;
const headerConfig = useHeaderConfigProps({
...options,
route,
headerBackButtonMenuEnabled:
isRemovePrevented !== undefined
? !isRemovePrevented
: headerBackButtonMenuEnabled,
headerBackTitle:
options.headerBackTitle !== undefined
? options.headerBackTitle
: undefined,
headerHeight,
headerShown: header !== undefined ? false : headerShown,
headerTopInsetEnabled,
headerTransparent,
headerBack,
});
const onHeaderHeightChange = hasCustomHeader
? // If we have a custom header, don't use native header height
undefined
: // On Fabric, there's a bug where native event drivers for Animated objects
// are created after the first notifications about the header height
// from the native side, `onHeaderHeightChange` event does not notify
// `animatedHeaderHeight` about initial values on appearing screens at the moment.
Animated.event(
[
{
nativeEvent: {
headerHeight: rawAnimatedHeaderHeight,
},
},
],
{
useNativeDriver,
listener: (e) => {
if (
e.nativeEvent &&
typeof e.nativeEvent === 'object' &&
'headerHeight' in e.nativeEvent &&
typeof e.nativeEvent.headerHeight === 'number'
) {
const headerHeight = e.nativeEvent.headerHeight;
// Only debounce if header has large title or search bar
// As it's the only case where the header height can change frequently
const doesHeaderAnimate =
Platform.OS === 'ios' &&
(options.headerLargeTitleEnabled ||
options.headerSearchBarOptions);
if (doesHeaderAnimate) {
setHeaderHeightDebounced(headerHeight);
} else {
if (
Platform.OS === 'android' &&
headerHeight !== 0 &&
headerHeight <= ANDROID_DEFAULT_HEADER_HEIGHT
) {
// FIXME: On Android, events may get delivered out-of-order
// https://github.com/facebook/react-native/issues/54636
// We seem to get header height without status bar height first,
// and then the correct height with status bar height included
// But due to out-of-order delivery, we may get the correct height first
// and then the one without status bar height
// This is hack to include status bar height if it's not already included
// It only works because header height doesn't change dynamically on Android
setHeaderHeight(headerHeight + insets.top);
} else {
setHeaderHeight(headerHeight);
}
}
}
},
}
);
return (
<NavigationProvider route={route} navigation={navigation}>
<ScreenStackItem
screenId={route.key}
activityState={isPreloaded ? 0 : 2}
style={StyleSheet.absoluteFill}
aria-hidden={!focused}
customAnimationOnSwipe={animationMatchesGesture}
fullScreenSwipeEnabled={fullScreenGestureEnabled}
fullScreenSwipeShadowEnabled={fullScreenGestureShadowEnabled}
freezeOnBlur={freezeOnBlur}
gestureEnabled={
Platform.OS === 'android'
? // This prop enables handling of system back gestures on Android
// Since we handle them in JS side, we disable this
false
: gestureEnabled
}
homeIndicatorHidden={autoHideHomeIndicator}
hideKeyboardOnSwipe={keyboardHandlingEnabled}
navigationBarColor={navigationBarColor}
navigationBarTranslucent={navigationBarTranslucent}
navigationBarHidden={navigationBarHidden}
replaceAnimation={animationTypeForReplace}
stackPresentation={presentation === 'card' ? 'push' : presentation}
stackAnimation={animation}
screenOrientation={orientation}
sheetAllowedDetents={sheetAllowedDetents}
sheetLargestUndimmedDetentIndex={sheetLargestUndimmedDetentIndex}
sheetGrabberVisible={sheetGrabberVisible}
sheetInitialDetentIndex={sheetInitialDetentIndex}
sheetCornerRadius={sheetCornerRadius}
sheetElevation={sheetElevation}
sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
sheetShouldOverflowTopInset={sheetShouldOverflowTopInset}
sheetDefaultResizeAnimationEnabled={sheetResizeAnimationEnabled}
statusBarAnimation={statusBarAnimation}
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
statusBarColor={statusBarBackgroundColor}
statusBarTranslucent={statusBarTranslucent}
swipeDirection={gestureDirectionOverride}
transitionDuration={animationDuration}
onWillAppear={onWillAppear}
onWillDisappear={onWillDisappear}
onAppear={onAppear}
onDisappear={onDisappear}
onDismissed={onDismissed}
onGestureCancel={onGestureCancel}
onSheetDetentChanged={onSheetDetentChanged}
gestureResponseDistance={gestureResponseDistance}
nativeBackButtonDismissalEnabled={false} // on Android
onHeaderBackButtonClicked={onHeaderBackButtonClicked}
preventNativeDismiss={isRemovePrevented} // on iOS
scrollEdgeEffects={{
bottom: scrollEdgeEffects?.bottom ?? 'automatic',
top: scrollEdgeEffects?.top ?? 'automatic',
left: scrollEdgeEffects?.left ?? 'automatic',
right: scrollEdgeEffects?.right ?? 'automatic',
}}
onNativeDismissCancelled={onNativeDismissCancelled}
onHeaderHeightChange={onHeaderHeightChange}
contentStyle={[
presentation !== 'transparentModal' &&
presentation !== 'containedTransparentModal' && {
backgroundColor: colors.background,
},
contentStyle,
]}
headerConfig={headerConfig}
unstable_sheetFooter={unstable_sheetFooter}
// When ts-expect-error is added, it affects all the props below it
// So we keep any props that need it at the end
// Otherwise invalid props may not be caught by TypeScript
shouldFreeze={shouldFreeze}
>
<AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
<HeaderHeightContext.Provider
value={
headerShown !== false ? headerHeight : (parentHeaderHeight ?? 0)
}
>
{headerBackground != null ? (
/**
* To show a custom header background, we render it at the top of the screen below the header
* The header also needs to be positioned absolutely (with `translucent` style)
*/
<View
style={[
styles.background,
headerTransparent ? styles.translucent : null,
{ height: headerHeight },
]}
>
{headerBackground()}
</View>
) : null}
{header != null && headerShown !== false ? (
<View
onLayout={(e) => {
const headerHeight = e.nativeEvent.layout.height;
setHeaderHeight(headerHeight);
rawAnimatedHeaderHeight.setValue(headerHeight);
}}
style={[
styles.header,
headerTransparent ? styles.absolute : null,
]}
>
{header({
back: headerBack,
options,
route,
navigation,
})}
</View>
) : null}
<HeaderShownContext.Provider
value={isParentHeaderShown || headerShown !== false}
>
<HeaderBackContext.Provider value={headerBack}>
{render()}
</HeaderBackContext.Provider>
</HeaderShownContext.Provider>
</HeaderHeightContext.Provider>
</AnimatedHeaderHeightContext.Provider>
</ScreenStackItem>
</NavigationProvider>
);
};
type Props = {
state: StackNavigationState<ParamListBase>;
navigation: NativeStackNavigationHelpers;
descriptors: NativeStackDescriptorMap;
describe: (
route: RouteProp<ParamListBase>,
placeholder: boolean
) => NativeStackDescriptor;
};
export function NativeStackView({
state,
navigation,
descriptors,
describe,
}: Props) {
const { setNextDismissedKey } = useDismissedRouteError(state);
useInvalidPreventRemoveError(descriptors);
const modalRouteKeys = getModalRouteKeys(state.routes, descriptors);
const preloadedDescriptors =
state.preloadedRoutes.reduce<NativeStackDescriptorMap>((acc, route) => {
acc[route.key] = acc[route.key] || describe(route, true);
return acc;
}, {});
return (
<SafeAreaProviderCompat>
<ScreenStack style={styles.container}>
{state.routes.concat(state.preloadedRoutes).map((route, index) => {
const descriptor =
descriptors[route.key] ?? preloadedDescriptors[route.key];
const isFocused = state.index === index;
const isBelowFocused = state.index - 1 === index;
const previousKey = state.routes[index - 1]?.key;
const nextKey = state.routes[index + 1]?.key;
const previousDescriptor = previousKey
? descriptors[previousKey]
: undefined;
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
const isModal = modalRouteKeys.includes(route.key);
const isModalOnIos = isModal && Platform.OS === 'ios';
const isPreloaded =
preloadedDescriptors[route.key] !== undefined &&
descriptors[route.key] === undefined;
// On Fabric, when screen is frozen, animated and reanimated values are not updated
// due to component being unmounted. To avoid this, we don't freeze the previous screen there
const shouldFreeze = isFabric()
? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos
: !isPreloaded && !isFocused && !isModalOnIos;
return (
<SceneView
key={route.key}
index={index}
focused={isFocused}
shouldFreeze={shouldFreeze}
descriptor={descriptor}
previousDescriptor={previousDescriptor}
nextDescriptor={nextDescriptor}
isPresentationModal={isModal}
isPreloaded={isPreloaded}
onWillDisappear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: true },
target: route.key,
});
}}
onWillAppear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: false },
target: route.key,
});
}}
onAppear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: false },
target: route.key,
});
}}
onDisappear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: true },
target: route.key,
});
}}
onDismissed={(event) => {
navigation.dispatch({
...StackActions.pop(event.nativeEvent.dismissCount),
source: route.key,
target: state.key,
});
setNextDismissedKey(route.key);
}}
onHeaderBackButtonClicked={() => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: state.key,
});
}}
onNativeDismissCancelled={(event) => {
navigation.dispatch({
...StackActions.pop(event.nativeEvent.dismissCount),
source: route.key,
target: state.key,
});
}}
onGestureCancel={() => {
navigation.emit({
type: 'gestureCancel',
target: route.key,
});
}}
onSheetDetentChanged={(event) => {
navigation.emit({
type: 'sheetDetentChange',
target: route.key,
data: {
index: event.nativeEvent.index,
stable: event.nativeEvent.isStable,
},
});
}}
/>
);
})}
</ScreenStack>
</SafeAreaProviderCompat>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
zIndex: 1,
},
absolute: {
position: 'absolute',
top: 0,
start: 0,
end: 0,
},
translucent: {
position: 'absolute',
top: 0,
start: 0,
end: 0,
zIndex: 1,
elevation: 1,
},
background: {
overflow: 'hidden',
},
});

View File

@@ -0,0 +1,219 @@
import {
getHeaderTitle,
Header,
HeaderBackButton,
HeaderBackContext,
SafeAreaProviderCompat,
Screen,
useHeaderHeight,
} from '@react-navigation/elements';
import {
type ParamListBase,
type RouteProp,
type StackNavigationState,
useLinkBuilder,
} from '@react-navigation/native';
import * as React from 'react';
import { Animated, Image, StyleSheet, View } from 'react-native';
import type {
NativeStackDescriptor,
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
} from '../types';
import { AnimatedHeaderHeightContext } from '../utils/useAnimatedHeaderHeight';
type Props = {
state: StackNavigationState<ParamListBase>;
// This is used for the native implementation of the stack.
navigation: NativeStackNavigationHelpers;
descriptors: NativeStackDescriptorMap;
describe: (
route: RouteProp<ParamListBase>,
placeholder: boolean
) => NativeStackDescriptor;
};
const TRANSPARENT_PRESENTATIONS = [
'transparentModal',
'containedTransparentModal',
];
export function NativeStackView({ state, descriptors, describe }: Props) {
const parentHeaderBack = React.useContext(HeaderBackContext);
const { buildHref } = useLinkBuilder();
const preloadedDescriptors =
state.preloadedRoutes.reduce<NativeStackDescriptorMap>((acc, route) => {
acc[route.key] = acc[route.key] || describe(route, true);
return acc;
}, {});
return (
<SafeAreaProviderCompat>
{state.routes.concat(state.preloadedRoutes).map((route, i) => {
const isFocused = state.index === i;
const previousKey = state.routes[i - 1]?.key;
const nextKey = state.routes[i + 1]?.key;
const previousDescriptor = previousKey
? descriptors[previousKey]
: undefined;
const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
const { options, navigation, render } =
descriptors[route.key] ?? preloadedDescriptors[route.key];
const headerBack = previousDescriptor
? {
title: getHeaderTitle(
previousDescriptor.options,
previousDescriptor.route.name
),
href: buildHref(
previousDescriptor.route.name,
previousDescriptor.route.params
),
}
: parentHeaderBack;
const canGoBack = headerBack != null;
const {
header,
headerShown,
headerBackIcon,
headerBackImageSource,
headerLeft,
headerTransparent,
headerBackTitle,
presentation,
contentStyle,
...rest
} = options;
const nextPresentation = nextDescriptor?.options.presentation;
const isPreloaded =
preloadedDescriptors[route.key] !== undefined &&
descriptors[route.key] === undefined;
return (
<Screen
key={route.key}
focused={isFocused}
route={route}
navigation={navigation}
headerShown={headerShown}
headerTransparent={headerTransparent}
header={
header !== undefined ? (
header({
back: headerBack,
options,
route,
navigation,
})
) : (
<Header
{...rest}
back={headerBack}
title={getHeaderTitle(options, route.name)}
headerLeft={
typeof headerLeft === 'function'
? ({ label, ...rest }) =>
headerLeft({
...rest,
label: headerBackTitle ?? label,
})
: headerLeft === undefined && canGoBack
? ({ tintColor, label, ...rest }) => (
<HeaderBackButton
{...rest}
label={headerBackTitle ?? label}
tintColor={tintColor}
backImage={
headerBackIcon !== undefined ||
headerBackImageSource !== undefined
? () => (
<Image
source={
headerBackIcon?.source ??
headerBackImageSource
}
resizeMode="contain"
tintColor={tintColor}
style={styles.backImage}
/>
)
: undefined
}
onPress={navigation.goBack}
/>
)
: headerLeft
}
headerTransparent={headerTransparent}
/>
)
}
style={[
StyleSheet.absoluteFill,
{
display:
(isFocused ||
(nextPresentation != null &&
TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
!isPreloaded
? 'flex'
: 'none',
},
presentation != null &&
TRANSPARENT_PRESENTATIONS.includes(presentation)
? { backgroundColor: 'transparent' }
: null,
]}
>
<HeaderBackContext.Provider value={headerBack}>
<AnimatedHeaderHeightProvider>
<View style={[styles.contentContainer, contentStyle]}>
{render()}
</View>
</AnimatedHeaderHeightProvider>
</HeaderBackContext.Provider>
</Screen>
);
})}
</SafeAreaProviderCompat>
);
}
const AnimatedHeaderHeightProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const headerHeight = useHeaderHeight();
const [animatedHeaderHeight] = React.useState(
() => new Animated.Value(headerHeight)
);
React.useEffect(() => {
animatedHeaderHeight.setValue(headerHeight);
}, [animatedHeaderHeight, headerHeight]);
return (
<AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
{children}
</AnimatedHeaderHeightContext.Provider>
);
};
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
},
backImage: {
height: 24,
width: 24,
margin: 3,
},
});

View File

@@ -0,0 +1,525 @@
import { getHeaderTitle, HeaderTitle } from '@react-navigation/elements';
import {
type Route,
type Theme,
useLocale,
useTheme,
} from '@react-navigation/native';
import color from 'color';
import { Platform, StyleSheet, type TextStyle, View } from 'react-native';
import {
type HeaderBarButtonItemMenuAction,
type HeaderBarButtonItemSubmenu,
type HeaderBarButtonItemWithAction,
type HeaderBarButtonItemWithMenu,
isSearchBarAvailableForCurrentPlatform,
ScreenStackHeaderBackButtonImage,
ScreenStackHeaderCenterView,
type ScreenStackHeaderConfigProps,
ScreenStackHeaderLeftView,
ScreenStackHeaderRightView,
ScreenStackHeaderSearchBarView,
SearchBar,
} from 'react-native-screens';
import type {
NativeStackHeaderItem,
NativeStackHeaderItemButton,
NativeStackHeaderItemMenuAction,
NativeStackHeaderItemMenuSubmenu,
NativeStackNavigationOptions,
} from '../types';
import { processFonts } from './FontProcessor';
type Props = NativeStackNavigationOptions & {
headerTopInsetEnabled: boolean;
headerHeight: number;
headerBack: { title?: string | undefined; href: undefined } | undefined;
route: Route<string>;
};
const processBarButtonItems = (
items: NativeStackHeaderItem[] | undefined,
colors: Theme['colors'],
fonts: Theme['fonts']
) => {
return items
?.map((item, index) => {
if (item.type === 'custom') {
// Handled with `ScreenStackHeaderLeftView` or `ScreenStackHeaderRightView`
return null;
}
if (item.type === 'spacing') {
if (item.spacing == null) {
throw new Error(
`Spacing item must have a 'spacing' property defined: ${JSON.stringify(
item
)}`
);
}
return item;
}
if (item.type === 'button' || item.type === 'menu') {
if (item.type === 'menu' && item.menu == null) {
throw new Error(
`Menu item must have a 'menu' property defined: ${JSON.stringify(
item
)}`
);
}
const { badge, label, labelStyle, icon, ...rest } = item;
const processedItemCommon = {
...rest,
index,
title: label,
titleStyle: {
...fonts.regular,
...labelStyle,
},
icon: transformIcon(icon),
};
let processedItem:
| HeaderBarButtonItemWithAction
| HeaderBarButtonItemWithMenu;
if (processedItemCommon.type === 'menu' && item.type === 'menu') {
const { multiselectable, layout } = item.menu;
processedItem = {
...processedItemCommon,
menu: {
...processedItemCommon.menu,
singleSelection: !multiselectable,
displayAsPalette: layout === 'palette',
items: item.menu.items.map(getMenuItem),
},
};
} else if (
processedItemCommon.type === 'button' &&
item.type === 'button'
) {
processedItem = processedItemCommon;
} else {
throw new Error(
`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button' and 'menu'.`
);
}
if (badge) {
const badgeBackgroundColor =
badge.style?.backgroundColor ?? colors.notification;
const badgeTextColor = color(badgeBackgroundColor).isLight()
? 'black'
: 'white';
processedItem = {
...processedItem,
badge: {
...badge,
value: String(badge.value),
style: {
backgroundColor: badgeBackgroundColor,
color: badgeTextColor,
...fonts.regular,
...badge.style,
},
},
};
}
return processedItem;
}
throw new Error(
`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`
);
})
.filter((item) => item != null);
};
const transformIcon = (
icon: NativeStackHeaderItemButton['icon']
):
| HeaderBarButtonItemWithAction['icon']
| HeaderBarButtonItemWithMenu['icon'] => {
if (icon?.type === 'image') {
return icon.tinted === false
? { type: 'imageSource', imageSource: icon.source }
: { type: 'templateSource', templateSource: icon.source };
}
return icon;
};
const getMenuItem = (
item: NativeStackHeaderItemMenuAction | NativeStackHeaderItemMenuSubmenu
): HeaderBarButtonItemMenuAction | HeaderBarButtonItemSubmenu => {
if (item.type === 'submenu') {
const { label, icon, inline, layout, items, multiselectable, ...rest } =
item;
return {
...rest,
icon: transformIcon(icon),
title: label,
displayAsPalette: layout === 'palette',
displayInline: inline,
singleSelection: !multiselectable,
items: items.map(getMenuItem),
};
}
const { label, icon, description, ...rest } = item;
return {
...rest,
icon: transformIcon(icon),
title: label,
subtitle: description,
};
};
export function useHeaderConfigProps({
headerBackIcon,
headerBackImageSource,
headerBackButtonDisplayMode,
headerBackButtonMenuEnabled,
headerBackTitle,
headerBackTitleStyle,
headerBackVisible,
headerShadowVisible,
headerLargeStyle,
headerLargeTitle: headerLargeTitleDeprecated,
headerLargeTitleEnabled = headerLargeTitleDeprecated,
headerLargeTitleShadowVisible,
headerLargeTitleStyle,
headerBackground,
headerLeft,
headerRight,
headerShown,
headerStyle,
headerBlurEffect,
headerTintColor,
headerTitle,
headerTitleAlign,
headerTitleStyle,
headerTransparent,
headerSearchBarOptions,
headerTopInsetEnabled,
headerBack,
route,
title,
unstable_headerLeftItems: headerLeftItems,
unstable_headerRightItems: headerRightItems,
}: Props): ScreenStackHeaderConfigProps {
const { direction } = useLocale();
const { colors, fonts, dark } = useTheme();
const tintColor =
headerTintColor ?? (Platform.OS === 'ios' ? colors.primary : colors.text);
const headerBackTitleStyleFlattened =
StyleSheet.flatten([fonts.regular, headerBackTitleStyle]) || {};
const headerLargeTitleStyleFlattened =
StyleSheet.flatten([
Platform.select({ ios: fonts.heavy, default: fonts.medium }),
headerLargeTitleStyle,
]) || {};
const headerTitleStyleFlattened =
StyleSheet.flatten([
Platform.select({ ios: fonts.bold, default: fonts.medium }),
headerTitleStyle,
]) || {};
const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
const [backTitleFontFamily, largeTitleFontFamily, titleFontFamily] =
processFonts([
headerBackTitleStyleFlattened.fontFamily,
headerLargeTitleStyleFlattened.fontFamily,
headerTitleStyleFlattened.fontFamily,
]);
const backTitleFontSize =
'fontSize' in headerBackTitleStyleFlattened
? headerBackTitleStyleFlattened.fontSize
: undefined;
const titleText = getHeaderTitle({ title, headerTitle }, route.name);
const titleColor =
'color' in headerTitleStyleFlattened
? headerTitleStyleFlattened.color
: (headerTintColor ?? colors.text);
const titleFontSize =
'fontSize' in headerTitleStyleFlattened
? headerTitleStyleFlattened.fontSize
: undefined;
const titleFontWeight = headerTitleStyleFlattened.fontWeight;
const largeTitleBackgroundColor = headerLargeStyleFlattened.backgroundColor;
const largeTitleColor =
'color' in headerLargeTitleStyleFlattened
? headerLargeTitleStyleFlattened.color
: undefined;
const largeTitleFontSize =
'fontSize' in headerLargeTitleStyleFlattened
? headerLargeTitleStyleFlattened.fontSize
: undefined;
const largeTitleFontWeight = headerLargeTitleStyleFlattened.fontWeight;
const headerTitleStyleSupported: TextStyle = { color: titleColor };
if (headerTitleStyleFlattened.fontFamily != null) {
headerTitleStyleSupported.fontFamily = headerTitleStyleFlattened.fontFamily;
}
if (titleFontSize != null) {
headerTitleStyleSupported.fontSize = titleFontSize;
}
if (titleFontWeight != null) {
headerTitleStyleSupported.fontWeight = titleFontWeight;
}
const headerBackgroundColor =
headerStyleFlattened.backgroundColor ??
(headerBackground != null ||
headerTransparent ||
// The title becomes invisible if background color is set with large title on iOS 26
(Platform.OS === 'ios' && headerLargeTitleEnabled)
? 'transparent'
: colors.card);
const canGoBack = headerBack != null;
const headerLeftElement = headerLeft?.({
tintColor,
canGoBack,
label: headerBackTitle ?? headerBack?.title,
// `href` is only applicable to web
href: undefined,
});
const headerRightElement = headerRight?.({
tintColor,
canGoBack,
});
const headerTitleElement =
typeof headerTitle === 'function'
? headerTitle({
tintColor,
children: titleText,
})
: null;
const supportsHeaderSearchBar =
typeof isSearchBarAvailableForCurrentPlatform === 'boolean'
? isSearchBarAvailableForCurrentPlatform
: // Fallback for older versions of react-native-screens
Platform.OS === 'ios' && SearchBar != null;
const hasHeaderSearchBar =
supportsHeaderSearchBar && headerSearchBarOptions != null;
/**
* We need to set this in if:
* - Back button should stay visible when `headerLeft` is specified
* - If `headerTitle` for Android is specified, so we only need to remove the title and keep the back button
*/
const backButtonInCustomView =
headerBackVisible ||
(Platform.OS === 'android' &&
headerTitleElement != null &&
headerLeftElement == null);
const translucent =
headerBackground != null ||
headerTransparent ||
// When using a SearchBar or large title, the header needs to be translucent for it to work on iOS
((hasHeaderSearchBar || headerLargeTitleEnabled) &&
Platform.OS === 'ios' &&
headerTransparent !== false);
const isBackButtonDisplayModeAvailable =
// On iOS 14+
Platform.OS === 'ios' &&
parseInt(Platform.Version, 10) >= 14 &&
// Doesn't have custom styling, by default System, see: https://github.com/software-mansion/react-native-screens/pull/2105#discussion_r1565222738
(backTitleFontFamily == null || backTitleFontFamily === 'System') &&
backTitleFontSize == null &&
// Back button menu is not disabled
headerBackButtonMenuEnabled !== false;
const isCenterViewRenderedAndroid = headerTitleAlign === 'center';
const leftItems = headerLeftItems?.({
tintColor,
canGoBack,
});
let rightItems = headerRightItems?.({
tintColor,
canGoBack,
});
if (rightItems) {
// iOS renders right items in reverse order
// So we need to reverse them here to match the order
rightItems = [...rightItems].reverse();
}
const children = (
<>
{Platform.OS === 'ios' ? (
<>
{leftItems ? (
leftItems.map((item, index) => {
if (item.type === 'custom') {
return (
<ScreenStackHeaderLeftView
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
hidesSharedBackground={item.hidesSharedBackground}
>
{item.element}
</ScreenStackHeaderLeftView>
);
}
return null;
})
) : headerLeftElement != null ? (
<ScreenStackHeaderLeftView>
{headerLeftElement}
</ScreenStackHeaderLeftView>
) : null}
{headerTitleElement != null ? (
<ScreenStackHeaderCenterView>
{headerTitleElement}
</ScreenStackHeaderCenterView>
) : null}
</>
) : (
<>
{headerLeftElement != null || typeof headerTitle === 'function' ? (
// The style passed to header left, together with title element being wrapped
// in flex view is reqruied for proper header layout, in particular,
// for the text truncation to work.
<ScreenStackHeaderLeftView
style={!isCenterViewRenderedAndroid ? { flex: 1 } : null}
>
{headerLeftElement}
{headerTitleAlign !== 'center' ? (
typeof headerTitle === 'function' ? (
<View style={{ flex: 1 }}>{headerTitleElement}</View>
) : (
<View style={{ flex: 1 }}>
<HeaderTitle
tintColor={tintColor}
style={headerTitleStyleSupported}
>
{titleText}
</HeaderTitle>
</View>
)
) : null}
</ScreenStackHeaderLeftView>
) : null}
{isCenterViewRenderedAndroid ? (
<ScreenStackHeaderCenterView>
{typeof headerTitle === 'function' ? (
headerTitleElement
) : (
<HeaderTitle
tintColor={tintColor}
style={headerTitleStyleSupported}
>
{titleText}
</HeaderTitle>
)}
</ScreenStackHeaderCenterView>
) : null}
</>
)}
{headerBackIcon !== undefined || headerBackImageSource !== undefined ? (
<ScreenStackHeaderBackButtonImage
source={headerBackIcon?.source ?? headerBackImageSource}
/>
) : null}
{Platform.OS === 'ios' && rightItems ? (
rightItems.map((item, index) => {
if (item.type === 'custom') {
return (
<ScreenStackHeaderRightView
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
hidesSharedBackground={item.hidesSharedBackground}
>
{item.element}
</ScreenStackHeaderRightView>
);
}
return null;
})
) : headerRightElement != null ? (
<ScreenStackHeaderRightView>
{headerRightElement}
</ScreenStackHeaderRightView>
) : null}
{hasHeaderSearchBar ? (
<ScreenStackHeaderSearchBarView>
<SearchBar {...headerSearchBarOptions} />
</ScreenStackHeaderSearchBarView>
) : null}
</>
);
return {
backButtonInCustomView,
backgroundColor: headerBackgroundColor,
backTitle: headerBackTitle,
backTitleVisible: isBackButtonDisplayModeAvailable
? undefined
: headerBackButtonDisplayMode !== 'minimal',
backButtonDisplayMode: isBackButtonDisplayModeAvailable
? headerBackButtonDisplayMode
: undefined,
backTitleFontFamily,
backTitleFontSize,
blurEffect: headerBlurEffect,
color: tintColor,
direction,
disableBackButtonMenu: headerBackButtonMenuEnabled === false,
hidden: headerShown === false,
hideBackButton: headerBackVisible === false,
hideShadow:
headerShadowVisible === false ||
headerBackground != null ||
(headerTransparent && headerShadowVisible !== true),
largeTitle: headerLargeTitleEnabled,
largeTitleBackgroundColor,
largeTitleColor,
largeTitleFontFamily,
largeTitleFontSize,
largeTitleFontWeight,
largeTitleHideShadow: headerLargeTitleShadowVisible === false,
title: titleText,
titleColor,
titleFontFamily,
titleFontSize,
titleFontWeight: String(titleFontWeight),
topInsetEnabled: headerTopInsetEnabled,
translucent: translucent === true,
children,
headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
experimental_userInterfaceStyle: dark ? 'dark' : 'light',
} as const;
}