Open History to see the last commit
1diff --git a/lib/module/views/NativeStackView.native.js b/lib/module/views/NativeStackView.native.js
2index 6882090..f1e14e1 100644
3--- a/lib/module/views/NativeStackView.native.js
4+++ b/lib/module/views/NativeStackView.native.js
5@@ -17,8 +17,12 @@ const ANDROID_DEFAULT_HEADER_HEIGHT = 56;
6 function isFabric() {
7 return 'nativeFabricUIManager' in global;
8 }
9+function SceneNavigationProvider({ presentationEnvelope, ...props }) {
10+ return presentationEnvelope ? props.children : /*#__PURE__*/_jsx(NavigationProvider, props);
11+}
12 const useNativeDriver = Platform.OS !== 'web';
13 const SceneView = /*#__PURE__*/React.memo(function SceneView({
14+ presentationEnvelope,
15 index,
16 focused,
17 shouldFreeze,
18@@ -144,6 +148,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({
19 sheetAllowedDetents = [1.0],
20 sheetLargestUndimmedDetentIndex = -1,
21 sheetGrabberVisible = false,
22+ sheetPrefersHorizontalBars = false,
23 sheetCornerRadius = -1.0,
24 sheetElevation = 24,
25 sheetExpandsWhenScrolledToEdge = true,
26@@ -305,7 +310,8 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({
27 }
28 }
29 });
30- return /*#__PURE__*/_jsx(NavigationProvider, {
31+ return /*#__PURE__*/_jsx(SceneNavigationProvider, {
32+ presentationEnvelope,
33 route: route,
34 navigation: navigation,
35 children: /*#__PURE__*/_jsx(ScreenStackItem, {
36@@ -333,6 +339,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({
37 sheetAllowedDetents: sheetAllowedDetents,
38 sheetLargestUndimmedDetentIndex: sheetLargestUndimmedDetentIndex,
39 sheetGrabberVisible: sheetGrabberVisible,
40+ sheetPrefersHorizontalBars: sheetPrefersHorizontalBars,
41 sheetInitialDetentIndex: sheetInitialDetentIndex,
42 sheetCornerRadius: sheetCornerRadius,
43 sheetElevation: sheetElevation,
44@@ -427,6 +434,7 @@ const SceneView = /*#__PURE__*/React.memo(function SceneView({
45 });
46 });
47 export function NativeStackView({
48+ presentationEnvelope,
49 state,
50 navigation,
51 descriptors,
52@@ -444,6 +452,17 @@ export function NativeStackView({
53 return /*#__PURE__*/_jsx(SafeAreaProviderCompat, {
54 children: /*#__PURE__*/_jsx(ScreenStack, {
55 style: styles.container,
56+ onFinishTransitioning: () => {
57+ // Surface UIKit's transition-completion callback to every route of
58+ // this navigator. Unlike transitionEnd, this also fires when a modal
59+ // finishes dismissing — where the presenting screen below receives no
60+ // appearance callbacks — and for a gesture-driven dismissal it fires
61+ // before the state pop, while the modal route is still the focused
62+ // one, so the event must not be targeted at a single route.
63+ navigation.emit({
64+ type: 'finishTransitioning'
65+ });
66+ },
67 children: state.routes.concat(state.preloadedRoutes).map((route, index) => {
68 const descriptor = descriptors[route.key] ?? preloadedDescriptors[route.key];
69 const isFocused = state.index === index;
70@@ -460,6 +479,7 @@ export function NativeStackView({
71 // due to component being unmounted. To avoid this, we don't freeze the previous screen there
72 const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused && !isModalOnIos : !isPreloaded && !isFocused && !isModalOnIos;
73 return /*#__PURE__*/_jsx(SceneView, {
74+ presentationEnvelope,
75 index: index,
76 focused: isFocused,
77 shouldFreeze: shouldFreeze,
78diff --git a/lib/module/views/useHeaderConfigProps.js b/lib/module/views/useHeaderConfigProps.js
79index 08055b2..ccf1fde 100644
80--- a/lib/module/views/useHeaderConfigProps.js
81+++ b/lib/module/views/useHeaderConfigProps.js
82@@ -18,6 +18,12 @@ const processBarButtonItems = (items, colors, fonts) => {
83 }
84 return item;
85 }
86+ if (item.type === 'mailSearchToolbar' || item.type === 'searchBarPlacement') {
87+ return {
88+ ...item,
89+ index
90+ };
91+ }
92 if (item.type === 'button' || item.type === 'menu') {
93 if (item.type === 'menu' && item.menu == null) {
94 throw new Error(`Menu item must have a 'menu' property defined: ${JSON.stringify(item)}`);
95@@ -81,7 +87,7 @@ const processBarButtonItems = (items, colors, fonts) => {
96 }
97 return processedItem;
98 }
99- throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`);
100+ throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom', 'spacing', 'mailSearchToolbar' and 'searchBarPlacement'.`);
101 }).filter(item => item != null);
102 };
103 const transformIcon = icon => {
104@@ -165,8 +171,12 @@ export function useHeaderConfigProps({
105 route,
106 title,
107 unstable_headerInsets: headerInsets,
108+ unstable_headerCenterItems: headerCenterItems,
109 unstable_headerLeftItems: headerLeftItems,
110- unstable_headerRightItems: headerRightItems
111+ unstable_headerRightItems: headerRightItems,
112+ unstable_headerSubtitle: headerSubtitle,
113+ unstable_headerToolbarItems: headerToolbarItems,
114+ unstable_navigationItemStyle: navigationItemStyle
115 }) {
116 const {
117 direction
118@@ -264,6 +274,10 @@ export function useHeaderConfigProps({
119 tintColor,
120 canGoBack
121 });
122+ const centerItems = headerCenterItems?.({
123+ tintColor,
124+ canGoBack
125+ });
126 let rightItems = headerRightItems?.({
127 tintColor,
128 canGoBack
129@@ -273,6 +287,10 @@ export function useHeaderConfigProps({
130 // So we need to reverse them here to match the order
131 rightItems = [...rightItems].reverse();
132 }
133+ const toolbarItems = headerToolbarItems?.({
134+ tintColor,
135+ canGoBack
136+ });
137 const children = /*#__PURE__*/_jsxs(_Fragment, {
138 children: [Platform.OS === 'ios' ? /*#__PURE__*/_jsxs(_Fragment, {
139 children: [leftItems ? leftItems.map((item, index) => {
140@@ -281,6 +299,7 @@ export function useHeaderConfigProps({
141 // eslint-disable-next-line @eslint-react/no-array-index-key
142 , {
143 hidesSharedBackground: item.hidesSharedBackground,
144+ identifier: item.identifier,
145 children: item.element
146 }, index);
147 }
148@@ -288,7 +307,16 @@ export function useHeaderConfigProps({
149 }) : headerLeftElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderLeftView, {
150 hidesSharedBackground: headerLeftBackgroundVisible === false,
151 children: headerLeftElement
152- }) : null, headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
153+ }) : null, centerItems?.length ? centerItems.map((item, index) => {
154+ if (item.type === 'custom') {
155+ return /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
156+ hidesSharedBackground: item.hidesSharedBackground,
157+ identifier: item.identifier,
158+ children: item.element
159+ }, index);
160+ }
161+ return null;
162+ }) : headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, {
163 children: headerTitleElement
164 }) : null]
165 }) : /*#__PURE__*/_jsxs(_Fragment, {
166@@ -331,6 +359,7 @@ export function useHeaderConfigProps({
167 // eslint-disable-next-line @eslint-react/no-array-index-key
168 , {
169 hidesSharedBackground: item.hidesSharedBackground,
170+ identifier: item.identifier,
171 children: item.element
172 }, index);
173 }
174@@ -367,6 +396,7 @@ export function useHeaderConfigProps({
175 largeTitleFontWeight,
176 largeTitleHideShadow: headerLargeTitleShadowVisible === false,
177 title: titleText,
178+ subtitle: headerSubtitle,
179 titleColor,
180 titleFontFamily,
181 titleFontSize,
182@@ -378,6 +408,9 @@ export function useHeaderConfigProps({
183 disableBottomInsetApplication: headerInsets?.bottom !== undefined ? !headerInsets.bottom : undefined,
184 translucent: translucent === true,
185 children,
186+ navigationItemStyle,
187+ headerToolbarItems: processBarButtonItems(toolbarItems, colors, fonts),
188+ headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts),
189 headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts),
190 headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts),
191 experimental_userInterfaceStyle: dark ? 'dark' : 'light'
192diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts
193index 27a3d8e..7312e4e 100644
194--- a/lib/typescript/src/types.d.ts
195+++ b/lib/typescript/src/types.d.ts
196@@ -694,6 +694,8 @@ export type NativeStackNavigationOptions = {
197 * @platform ios
198 */
199 sheetGrabberVisible?: boolean;
200+ /** Requests horizontal bars on iOS 27.1+. Defaults to UIKit automatic placement. */
201+ sheetPrefersHorizontalBars?: boolean;
202 /**
203 * The largest sheet detent for which a view underneath won't be dimmed.
204 * Works only when `presentation` is set to `formSheet`.
205@@ -903,6 +905,8 @@ type SharedHeaderItem = {
206 * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/sharesbackground
207 */
208 sharesBackground?: boolean;
209+ /** Native bar axis preference on iOS 27.1 and later. */
210+ axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred';
211 /**
212 * An identifier used to match items across transitions.
213 * Only available from iOS 26.0 and later.
214@@ -1148,6 +1152,13 @@ export type NativeStackHeaderItemSpacing = {
215 */
216 export type NativeStackHeaderItemCustom = {
217 type: 'custom';
218+ /**
219+ * An identifier used to match items across transitions.
220+ * Only available from iOS 26.0 and later.
221+ *
222+ * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/identifier
223+ */
224+ identifier?: string;
225 /**
226 * A React Element to display as the item.
227 */
228diff --git a/lib/typescript/src/views/NativeStackView.d.ts b/lib/typescript/src/views/NativeStackView.d.ts
229index cc688e2..0ec33c8 100644
230--- a/lib/typescript/src/views/NativeStackView.d.ts
231+++ b/lib/typescript/src/views/NativeStackView.d.ts
232@@ -2,6 +2,7 @@ import { type ParamListBase, type RouteProp, type StackNavigationState } from '@
233 import * as React from 'react';
234 import type { NativeStackDescriptor, NativeStackDescriptorMap, NativeStackNavigationHelpers } from '../types.js';
235 type Props = {
236+ presentationEnvelope?: boolean;
237 state: StackNavigationState<ParamListBase>;
238 navigation: NativeStackNavigationHelpers;
239 descriptors: NativeStackDescriptorMap;
240diff --git a/lib/typescript/src/views/NativeStackView.native.d.ts b/lib/typescript/src/views/NativeStackView.native.d.ts
241index d533cd9..857b7ad 100644
242--- a/lib/typescript/src/views/NativeStackView.native.d.ts
243+++ b/lib/typescript/src/views/NativeStackView.native.d.ts
244@@ -2,11 +2,12 @@ import { type ParamListBase, type RouteProp, type StackNavigationState } from '@
245 import * as React from 'react';
246 import type { NativeStackDescriptor, NativeStackDescriptorMap, NativeStackNavigationHelpers } from '../types.js';
247 type Props = {
248+ presentationEnvelope?: boolean;
249 state: StackNavigationState<ParamListBase>;
250 navigation: NativeStackNavigationHelpers;
251 descriptors: NativeStackDescriptorMap;
252 describe: (route: RouteProp<ParamListBase>, placeholder: boolean) => NativeStackDescriptor;
253 };
254-export declare function NativeStackView({ state, navigation, descriptors, describe, }: Props): React.JSX.Element;
255+export declare function NativeStackView({ presentationEnvelope, state, navigation, descriptors, describe, }: Props): React.JSX.Element;
256 export {};
257 //# sourceMappingURL=NativeStackView.native.d.ts.map
258\ No newline at end of file
259diff --git a/src/types.tsx b/src/types.tsx
260index 5d63f41..f949a11 100644
261--- a/src/types.tsx
262+++ b/src/types.tsx
263@@ -746,6 +746,8 @@ export type NativeStackNavigationOptions = {
264 * @platform ios
265 */
266 sheetGrabberVisible?: boolean;
267+ /** Requests horizontal bars on iOS 27.1+. Defaults to UIKit automatic placement. */
268+ sheetPrefersHorizontalBars?: boolean;
269 /**
270 * The largest sheet detent for which a view underneath won't be dimmed.
271 * Works only when `presentation` is set to `formSheet`.
272@@ -959,6 +961,8 @@ type SharedHeaderItem = {
273 * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/sharesbackground
274 */
275 sharesBackground?: boolean;
276+ /** Native bar axis preference on iOS 27.1 and later. */
277+ axisBehavior?: 'automatic' | 'horizontalOnly' | 'verticalPreferred';
278 /**
279 * An identifier used to match items across transitions.
280 * Only available from iOS 26.0 and later.
281@@ -1213,6 +1217,13 @@ export type NativeStackHeaderItemSpacing = {
282 */
283 export type NativeStackHeaderItemCustom = {
284 type: 'custom';
285+ /**
286+ * An identifier used to match items across transitions.
287+ * Only available from iOS 26.0 and later.
288+ *
289+ * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/identifier
290+ */
291+ identifier?: string;
292 /**
293 * A React Element to display as the item.
294 */
295diff --git a/src/views/NativeStackView.native.tsx b/src/views/NativeStackView.native.tsx
296index aa13bc7..225daf9 100644
297--- a/src/views/NativeStackView.native.tsx
298+++ b/src/views/NativeStackView.native.tsx
299@@ -52,7 +52,14 @@ function isFabric() {
300 return 'nativeFabricUIManager' in global;
301 }
303+// A presentation envelope contains descriptors from its owning navigator.
304+// Its synthetic route must not override those descriptors' focus state.
305+function SceneNavigationProvider({ presentationEnvelope, ...props }: React.ComponentProps<typeof NavigationProvider> & { presentationEnvelope?: boolean }) {
306+ return presentationEnvelope ? props.children : <NavigationProvider {...props} />;
307+}
308+
309 type SceneViewProps = {
310+ presentationEnvelope?: boolean;
311 index: number;
312 focused: boolean;
313 shouldFreeze: boolean;
314@@ -71,6 +78,7 @@ type SceneViewProps = {
315 const useNativeDriver = Platform.OS !== 'web';
317 const SceneView = React.memo(function SceneView({
318+ presentationEnvelope,
319 index,
320 focused,
321 shouldFreeze,
322@@ -199,6 +207,7 @@ const SceneView = React.memo(function SceneView({
323 sheetAllowedDetents = [1.0],
324 sheetLargestUndimmedDetentIndex = -1,
325 sheetGrabberVisible = false,
326+ sheetPrefersHorizontalBars = false,
327 sheetCornerRadius = -1.0,
328 sheetElevation = 24,
329 sheetExpandsWhenScrolledToEdge = true,
330@@ -434,7 +443,7 @@ const SceneView = React.memo(function SceneView({
331 );
333 return (
334- <NavigationProvider route={route} navigation={navigation}>
335+ <SceneNavigationProvider presentationEnvelope={presentationEnvelope} route={route} navigation={navigation}>
336 <ScreenStackItem
337 screenId={route.key}
338 activityState={isPreloaded ? 0 : 2}
339@@ -463,6 +472,7 @@ const SceneView = React.memo(function SceneView({
340 sheetAllowedDetents={sheetAllowedDetents}
341 sheetLargestUndimmedDetentIndex={sheetLargestUndimmedDetentIndex}
342 sheetGrabberVisible={sheetGrabberVisible}
343+ sheetPrefersHorizontalBars={sheetPrefersHorizontalBars}
344 sheetInitialDetentIndex={sheetInitialDetentIndex}
345 sheetCornerRadius={sheetCornerRadius}
346 sheetElevation={sheetElevation}
347@@ -571,11 +581,12 @@ const SceneView = React.memo(function SceneView({
348 </HeaderHeightContext.Provider>
349 </AnimatedHeaderHeightContext.Provider>
350 </ScreenStackItem>
351- </NavigationProvider>
352+ </SceneNavigationProvider>
353 );
354 });
356 type Props = {
357+ presentationEnvelope?: boolean;
358 state: StackNavigationState<ParamListBase>;
359 navigation: NativeStackNavigationHelpers;
360 descriptors: NativeStackDescriptorMap;
361@@ -586,6 +597,7 @@ type Props = {
362 };
364 export function NativeStackView({
365+ presentationEnvelope,
366 state,
367 navigation,
368 descriptors,
369@@ -633,6 +645,7 @@ export function NativeStackView({
371 return (
372 <SceneView
373+ presentationEnvelope={presentationEnvelope}
374 key={route.key}
375 index={index}
376 focused={isFocused}
377diff --git a/src/views/NativeStackView.tsx b/src/views/NativeStackView.tsx
378index c50a3fa..6305ed5 100644
379--- a/src/views/NativeStackView.tsx
380+++ b/src/views/NativeStackView.tsx
381@@ -25,6 +25,7 @@ import type {
382 import { AnimatedHeaderHeightContext } from '../utils/useAnimatedHeaderHeight';
384 type Props = {
385+ presentationEnvelope?: boolean;
386 state: StackNavigationState<ParamListBase>;
387 // This is used for the native implementation of the stack.
388 navigation: NativeStackNavigationHelpers;
389diff --git a/src/views/useHeaderConfigProps.tsx b/src/views/useHeaderConfigProps.tsx
390index 739134e..9bb056e 100644
391--- a/src/views/useHeaderConfigProps.tsx
392+++ b/src/views/useHeaderConfigProps.tsx
393@@ -412,6 +412,7 @@ export function useHeaderConfigProps({
394 // eslint-disable-next-line @eslint-react/no-array-index-key
395 key={index}
396 hidesSharedBackground={item.hidesSharedBackground}
397+ identifier={item.identifier}
398 >
399 {item.element}
400 </ScreenStackHeaderLeftView>
401@@ -488,6 +489,7 @@ export function useHeaderConfigProps({
402 // eslint-disable-next-line @eslint-react/no-array-index-key
403 key={index}
404 hidesSharedBackground={item.hidesSharedBackground}
405+ identifier={item.identifier}
406 >
407 {item.element}
408 </ScreenStackHeaderRightView>