t3-code-android-nightly/patches/@react-navigation%2Fnative-stack@7.17.6.patch
Julius Marminge 4669eab8e0
fix(mobile): stabilize iOS header item transitions (#8607)
Co-authored-by: Julius Marminge <julius@mac.lan>
2026-08-28 18:41:42 -07:00

191 lines
8.1 KiB
Diff

diff --git a/lib/module/views/NativeStackView.native.js b/lib/module/views/NativeStackView.native.js
index c342e90bffde9ad5044b6167bdde2e19be219270..5d3e4407aa1bb68b873cf164adf5a9f0b487bb0b 100644
--- a/lib/module/views/NativeStackView.native.js
+++ b/lib/module/views/NativeStackView.native.js
@@ -370,6 +370,17 @@ export function NativeStackView({
return /*#__PURE__*/_jsx(SafeAreaProviderCompat, {
children: /*#__PURE__*/_jsx(ScreenStack, {
style: styles.container,
+ onFinishTransitioning: () => {
+ // Surface UIKit's transition-completion callback to every route of
+ // this navigator. Unlike transitionEnd, this also fires when a modal
+ // finishes dismissing — where the presenting screen below receives no
+ // appearance callbacks — and for a gesture-driven dismissal it fires
+ // before the state pop, while the modal route is still the focused
+ // one, so the event must not be targeted at a single route.
+ navigation.emit({
+ type: 'finishTransitioning'
+ });
+ },
children: state.routes.concat(state.preloadedRoutes).map((route, index) => {
const descriptor = descriptors[route.key] ?? preloadedDescriptors[route.key];
const isFocused = state.index === index;
diff --git a/lib/module/views/useHeaderConfigProps.js b/lib/module/views/useHeaderConfigProps.js
index 9c21aa48cb580704aa17e59d9cdadcc612a1e4ff..a1b3a38368c5ef2257570f382e87df5d6f55b11e 100644
--- a/lib/module/views/useHeaderConfigProps.js
+++ b/lib/module/views/useHeaderConfigProps.js
@@ -19,6 +19,12 @@ const processBarButtonItems = (items, colors, fonts) => {
}
return item;
}
+ if (item.type === 'mailSearchToolbar') {
+ return {
+ ...item,
+ index
+ };
+ }
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)}`);
@@ -79,7 +85,7 @@ const processBarButtonItems = (items, colors, fonts) => {
}
return processedItem;
}
- throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`);
+ throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom', 'spacing' and 'mailSearchToolbar'.`);
}).filter(item => item != null);
};
const transformIcon = icon => {
@@ -158,8 +164,12 @@ export function useHeaderConfigProps({
headerBack,
route,
title,
+ unstable_headerCenterItems: headerCenterItems,
unstable_headerLeftItems: headerLeftItems,
- unstable_headerRightItems: headerRightItems
+ unstable_headerRightItems: headerRightItems,
+ unstable_headerSubtitle: headerSubtitle,
+ unstable_headerToolbarItems: headerToolbarItems,
+ unstable_navigationItemStyle: navigationItemStyle
}) {
const {
direction
@@ -255,6 +265,10 @@ export function useHeaderConfigProps({
tintColor,
canGoBack
});
+ const centerItems = headerCenterItems?.({
+ tintColor,
+ canGoBack
+ });
let rightItems = headerRightItems?.({
tintColor,
canGoBack
@@ -264,6 +278,10 @@ export function useHeaderConfigProps({
// So we need to reverse them here to match the order
rightItems = [...rightItems].reverse();
}
+ const toolbarItems = headerToolbarItems?.({
+ tintColor,
+ canGoBack
+ });
const children = /*#__PURE__*/_jsxs(_Fragment, {
children: [Platform.OS === 'ios' ? /*#__PURE__*/_jsxs(_Fragment, {
children: [leftItems ? leftItems.map((item, index) => {
@@ -272,13 +290,23 @@ export function useHeaderConfigProps({
// eslint-disable-next-line @eslint-react/no-array-index-key
, {
hidesSharedBackground: item.hidesSharedBackground,
+ identifier: item.identifier,
children: item.element
}, index);
}
return null;
}) : headerLeftElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderLeftView, {
children: headerLeftElement
- }) : null, headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
+ }) : null, centerItems ? centerItems.map((item, index) => {
+ if (item.type === 'custom') {
+ return /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
+ hidesSharedBackground: item.hidesSharedBackground,
+ identifier: item.identifier,
+ children: item.element
+ }, index);
+ }
+ return null;
+ }) : headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
children: headerTitleElement
}) : null]
}) : /*#__PURE__*/_jsxs(_Fragment, {
@@ -321,6 +349,7 @@ export function useHeaderConfigProps({
// eslint-disable-next-line @eslint-react/no-array-index-key
, {
hidesSharedBackground: item.hidesSharedBackground,
+ identifier: item.identifier,
children: item.element
}, index);
}
@@ -356,6 +385,7 @@ export function useHeaderConfigProps({
largeTitleFontWeight,
largeTitleHideShadow: headerLargeTitleShadowVisible === false,
title: titleText,
+ subtitle: headerSubtitle,
titleColor,
titleFontFamily,
titleFontSize,
@@ -364,6 +394,9 @@ export function useHeaderConfigProps({
disableTopInsetApplication: !headerTopInsetEnabled,
translucent: translucent === true,
children,
+ navigationItemStyle,
+ headerToolbarItems: processBarButtonItems(toolbarItems, colors, fonts),
+ headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts),
headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
experimental_userInterfaceStyle: dark ? 'dark' : 'light'
diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts
index 2f1351a89f0f2e67e854dc81e392198326b71834..ccf8556911fa1fa17588f06b56edb2151a712bf0 100644
--- a/lib/typescript/src/types.d.ts
+++ b/lib/typescript/src/types.d.ts
@@ -1091,6 +1091,13 @@ export type NativeStackHeaderItemSpacing = {
*/
export type NativeStackHeaderItemCustom = {
type: 'custom';
+ /**
+ * An identifier used to match items across transitions.
+ * Only available from iOS 26.0 and later.
+ *
+ * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/identifier
+ */
+ identifier?: string;
/**
* A React Element to display as the item.
*/
diff --git a/src/types.tsx b/src/types.tsx
index 7488b1cf20afe36b5c585fbfcd369471fdb6ab8d..f4bd78e48c687024ba84e998fac79e9eb16f1a80 100644
--- a/src/types.tsx
+++ b/src/types.tsx
@@ -1156,6 +1156,13 @@ export type NativeStackHeaderItemSpacing = {
*/
export type NativeStackHeaderItemCustom = {
type: 'custom';
+ /**
+ * An identifier used to match items across transitions.
+ * Only available from iOS 26.0 and later.
+ *
+ * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/identifier
+ */
+ identifier?: string;
/**
* A React Element to display as the item.
*/
diff --git a/src/views/useHeaderConfigProps.tsx b/src/views/useHeaderConfigProps.tsx
index 3f3fff405048b3c2250afbe99dd7f35cecb9a6d5..bb062b898cfa77f8c02f951dc9d81b51672d7d0e 100644
--- a/src/views/useHeaderConfigProps.tsx
+++ b/src/views/useHeaderConfigProps.tsx
@@ -397,6 +397,7 @@ export function useHeaderConfigProps({
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
hidesSharedBackground={item.hidesSharedBackground}
+ identifier={item.identifier}
>
{item.element}
</ScreenStackHeaderLeftView>
@@ -471,6 +472,7 @@ export function useHeaderConfigProps({
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
hidesSharedBackground={item.hidesSharedBackground}
+ identifier={item.identifier}
>
{item.element}
</ScreenStackHeaderRightView>