diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.ktindex 03d8d9f3a5c403c93282d53cbacf1a339e877e85..54d5eabc6c8a408e711a5be97286fdbb8ab1c461 100644--- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt+++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt@@ -103,6 +103,34 @@ class ScreenStackHeaderConfigViewManager : config.setTitle(title) } + // The t3 patch adds these iOS glass-chrome props to the native spec; they have+ // no Android implementation, but the codegen-generated interface requires the+ // setters to exist. No-ops keep the Android build compiling.+ override fun setSubtitle(+ config: ScreenStackHeaderConfig,+ subtitle: String?,+ ) = Unit++ override fun setLargeSubtitle(+ config: ScreenStackHeaderConfig,+ largeSubtitle: String?,+ ) = Unit++ override fun setNavigationItemStyle(+ config: ScreenStackHeaderConfig,+ navigationItemStyle: String?,+ ) = Unit++ override fun setHeaderCenterBarButtonItems(+ config: ScreenStackHeaderConfig,+ headerCenterBarButtonItems: ReadableArray?,+ ) = Unit++ override fun setHeaderToolbarItems(+ config: ScreenStackHeaderConfig,+ headerToolbarItems: ReadableArray?,+ ) = Unit+ override fun setTitleFontFamily( config: ScreenStackHeaderConfig, titleFontFamily: String?,diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.hindex ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644--- a/ios/RNSBarButtonItem.h+++ b/ios/RNSBarButtonItem.h@@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); menuAction:(RNSBarButtonMenuItemAction)menuAction imageLoader:(RCTImageLoader *)imageLoader; ++ (UIMenu *)initUIMenuWithDict:(NSDictionary<NSString *, id> *)dict+ menuAction:(RNSBarButtonMenuItemAction)menuAction+ imageLoader:(RCTImageLoader *)imageLoader;+ @enddiff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mmindex 0eb1f09dee82edd99e3e614938233db5cdeaebfe..73298f10807520970f625e166d7f5dd1338206f1 100644--- a/ios/RNSBarButtonItem.mm+++ b/ios/RNSBarButtonItem.mm@@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; }+#if !TARGET_OS_TV && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)+ NSNumber *glassEffectNum = dict[@"glassEffect"];+ if (@available(iOS 26.0, *)) {+ if ([glassEffectNum boolValue]) {+ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration];+ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule;+ UIBackgroundConfiguration *background = configuration.background;+ background.strokeWidth = 0.0;+ background.strokeColor = UIColor.clearColor;+ configuration.background = background;+ configuration.contentInsets = NSDirectionalEdgeInsetsZero;+ if (title != nil) {+ configuration.title = title;+ }++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil];+ button.translatesAutoresizingMaskIntoConstraints = NO;+ button.enabled = self.enabled;+ button.accessibilityLabel = self.accessibilityLabel;+ button.accessibilityHint = self.accessibilityHint;++ CGFloat resolvedWidth = width != nil ? width.doubleValue : 56.0;+ [button.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES;+ [button.heightAnchor constraintEqualToConstant:50.0].active = YES;++ [[self class] resolveImageFromConfig:dict+ imageLoader:imageLoader+ completionBlock:^(UIImage *img) {+ UIButtonConfiguration *updatedConfiguration = button.configuration;+ updatedConfiguration.image = img;+ button.configuration = updatedConfiguration;+ }];++ NSDictionary *menu = dict[@"menu"];+ if (menu) {+ button.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction imageLoader:imageLoader];+ button.showsMenuAsPrimaryAction = YES;+ } else {+ NSString *buttonId = dict[@"buttonId"];+ if (buttonId && action) {+ UIAction *pressAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull sender) {+ action(buttonId);+ }];+ [button addAction:pressAction forControlEvents:UIControlEventTouchUpInside];+ }+ }++ self.customView = button;+ return self;+ }+ }+#endif #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) {diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.hindex 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644--- a/ios/RNSScreenStackHeaderConfig.h+++ b/ios/RNSScreenStackHeaderConfig.h@@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, retain) NSString *title;+@property (nonatomic, retain) NSString *subtitle;+@property (nonatomic, retain) NSString *largeSubtitle; @property (nonatomic, retain) NSString *titleFontFamily; @property (nonatomic, retain) NSNumber *titleFontSize; @property (nonatomic, retain) NSString *titleFontWeight;@@ -45,9 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode;+@property (nonatomic) NSInteger navigationItemStyle; @property (nonatomic) RNSBlurEffectStyle blurEffect; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerLeftBarButtonItems;+@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerCenterBarButtonItems;+@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerToolbarItems; @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; NS_ASSUME_NONNULL_ENDdiff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mmindex 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb2199dcbd58 100644--- a/ios/RNSScreenStackHeaderConfig.mm+++ b/ios/RNSScreenStackHeaderConfig.mm@@ -25,11 +25,33 @@ #import "RNSSearchBar.h" #import "UINavigationBar+RNSUtility.h" +#import <objc/runtime.h>+ namespace react = facebook::react; static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17;++// Keys for the last-applied JS bar button configs, associated with the+// navigation item so unrelated header updates (title, subtitle, tint) don't+// recreate the native buttons they configure.+static char RNSAppliedHeaderBarButtonConfigsKey;+static char RNSAppliedToolbarConfigsKey; static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +static NSInteger navigationItemStyleFromCppEquivalent(+ react::RNSScreenStackHeaderConfigNavigationItemStyle navigationItemStyle)+{+ switch (navigationItemStyle) {+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Browser:+ return UINavigationItemStyleBrowser;+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Editor:+ return UINavigationItemStyleEditor;+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Navigator:+ default:+ return UINavigationItemStyleNavigator;+ }+}+ @interface RCTImageLoader (Private) - (id<RCTImageCache>)imageCache; @end@@ -47,6 +69,9 @@ + (BOOL)rnscreens_isBlankOrNull:(NSString *)string @end @interface RNSScreenStackHeaderConfig () <RCTMountingTransactionObserving>+#if !TARGET_OS_TV+- (NSArray<UIBarButtonItemGroup *> *)barButtonItemGroupsFromItems:(NSArray<UIBarButtonItem *> *)items;+#endif @end @implementation RNSScreenStackHeaderConfig {@@ -81,6 +106,7 @@ - (void)initProps self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES;+ _navigationItemStyle = UINavigationItemStyleNavigator; _blurEffect = RNSBlurEffectStyleNone; } @@ -496,6 +522,10 @@ + (void)updateViewController:(UIViewController *)vc if (shouldHide) { navitem.title = config.title;+ if (@available(iOS 26.0, *)) {+ navitem.subtitle = config.subtitle;+ navitem.largeSubtitle = config.largeSubtitle;+ } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated];@@ -512,11 +542,19 @@ + (void)updateViewController:(UIViewController *)vc } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever;++ if (@available(iOS 16.0, *)) {+ navitem.style = (UINavigationItemStyle)config.navigationItemStyle;+ } #endif UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; navitem.standardAppearance = appearance; navitem.compactAppearance = appearance;+ if (@available(iOS 26.0, *)) {+ navitem.subtitle = config.subtitle;+ navitem.largeSubtitle = config.largeSubtitle;+ } // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV@@ -637,10 +675,458 @@ + (void)updateViewController:(UIViewController *)vc // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title;- navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems- withCurrentItems:navitem.leftBarButtonItems];- navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems- withCurrentItems:navitem.rightBarButtonItems];+ NSArray *headerLeftConfigs = config.headerLeftBarButtonItems ?: @[];+ NSArray *headerRightConfigs = config.headerRightBarButtonItems ?: @[];+ NSArray *headerCenterConfigs = config.headerCenterBarButtonItems ?: @[];+ NSArray<UIBarButtonItem *> *subviewLeftItems = navitem.leftBarButtonItems ?: @[];+ NSArray<UIBarButtonItem *> *subviewRightItems = navitem.rightBarButtonItems ?: @[];+ // The key includes the config instance's identity: cached items capture+ // this config's event emitter in their press handlers, so a remounted+ // header-config view with value-equal configs must still rebuild — reusing+ // the old items would dispatch presses into the dead config's emitter.+ NSArray *headerItemsKey =+ @[ @((uintptr_t)config), headerLeftConfigs, headerRightConfigs, headerCenterConfigs ];+ // Rebuilding bar button items creates brand-new native buttons (glass+ // UIButton custom views on iOS 26). Replacing them while UIKit animates an+ // existing one (menu capsule morph, push/pop glass transitions) strands the+ // animation overlay — a stuck expanded capsule or an unmasked square back+ // button. When the JS configs are unchanged, keep the already-applied items.+ // Subview-backed items are re-derived every pass, so their presence forces+ // the rebuild path.+ BOOL reuseHeaderBarButtonItems = subviewLeftItems.count == 0 && subviewRightItems.count == 0 &&+ [objc_getAssociatedObject(navitem, &RNSAppliedHeaderBarButtonConfigsKey) isEqual:headerItemsKey];+ if (!reuseHeaderBarButtonItems) {+ NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems+ withCurrentItems:navitem.leftBarButtonItems+ navigationItem:navitem];+ NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems+ withCurrentItems:navitem.rightBarButtonItems+ navigationItem:navitem];+ NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems+ withCurrentItems:@[]+ navigationItem:navitem];+#if !TARGET_OS_TV+ if (@available(iOS 16.0, *)) {+ navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems];+ navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems];+ if (@available(iOS 26.0, *)) {+ navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems];+ }+ navitem.leftBarButtonItems = nil;+ navitem.rightBarButtonItems = nil;+ } else {+ navitem.leftBarButtonItems = leftBarButtonItems;+ navitem.rightBarButtonItems = rightBarButtonItems;+ }+#else+ navitem.leftBarButtonItems = leftBarButtonItems;+ navitem.rightBarButtonItems = rightBarButtonItems;+#endif+ // Only dict-driven items can be reused: with subview-backed items in the+ // mix the applied state depends on view identity, so clear the key to+ // force a rebuild on the next pass.+ objc_setAssociatedObject(+ navitem,+ &RNSAppliedHeaderBarButtonConfigsKey,+ subviewLeftItems.count == 0 && subviewRightItems.count == 0 ? headerItemsKey : nil,+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);+ }+ NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil;+ for (NSDictionary<NSString *, id> *toolbarConfig in config.headerToolbarItems) {+ if (toolbarConfig[@"mailSearchToolbar"]) {+ mailSearchToolbarConfig = toolbarConfig;+ break;+ }+ }+ if (mailSearchToolbarConfig == nil) {+ for (NSDictionary<NSString *, id> *rightConfig in config.headerRightBarButtonItems) {+ if ([rightConfig[@"bottomMailSearchToolbar"] boolValue]) {+ mailSearchToolbarConfig = rightConfig;+ break;+ }+ }+ }++ static NSInteger const RNSMailSearchToolbarViewTag = 260628;+ UIView *chromeHostView = vc.view;+ UIView *existingMailSearchToolbar = [chromeHostView viewWithTag:RNSMailSearchToolbarViewTag];+ if (existingMailSearchToolbar == nil) {+ existingMailSearchToolbar = [vc.view viewWithTag:RNSMailSearchToolbarViewTag];+ }+ // Reuse the live toolbar when nothing it was built from changed: rebuilding+ // replaces the fallback UISearchTextField with a fresh empty one, dropping+ // the user's in-progress search text and first responder on every unrelated+ // header update. Keyed on the config instance (the button/search blocks+ // capture its event emitter), the toolbar config values, and the host width+ // (the width constraint constant is resolved from it at build time).+ static char RNSAppliedMailSearchToolbarConfigKey;+ NSArray *mailSearchToolbarKey = mailSearchToolbarConfig != nil+ ? @[ @((uintptr_t)config), mailSearchToolbarConfig, @(chromeHostView.bounds.size.width) ]+ : nil;+ BOOL reuseMailSearchToolbar = existingMailSearchToolbar != nil && mailSearchToolbarKey != nil &&+ [objc_getAssociatedObject(existingMailSearchToolbar, &RNSAppliedMailSearchToolbarConfigKey)+ isEqual:mailSearchToolbarKey];+ if (!reuseMailSearchToolbar) {+ [existingMailSearchToolbar removeFromSuperview];+ }++ if (mailSearchToolbarConfig != nil && !reuseMailSearchToolbar) {+#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)+ if (@available(iOS 26.0, *)) {+ CGFloat horizontalInset = 18.0;+ NSNumber *width = mailSearchToolbarConfig[@"width"];+ CGFloat hostWidth = chromeHostView.bounds.size.width;+ if (hostWidth <= 0.0) {+ hostWidth = vc.view.bounds.size.width;+ }+ if (hostWidth <= 0.0) {+ hostWidth = [UIScreen mainScreen].bounds.size.width;+ }+ CGFloat fallbackWidth = MIN(560.0, MAX(300.0, hostWidth - (horizontalInset * 2.0)));+ CGFloat resolvedWidth = width != nil ? width.doubleValue : fallbackWidth;+ if (hostWidth > 0.0) {+ resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0));+ }++ CGFloat toolbarHeight = 48.0;+ CGFloat toolbarRadius = toolbarHeight / 2.0;+ CGFloat buttonSize = toolbarHeight;+ CGFloat sideButtonReserve = buttonSize + 8.0;+ CGFloat searchFieldHeight = 42.0;+ CGFloat toolbarBottomSpacing = 4.0;+ UIFont *searchFont = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle3];+ NSDictionary<NSAttributedStringKey, id> *placeholderAttributes = @{+ NSForegroundColorAttributeName : UIColor.secondaryLabelColor,+ NSFontAttributeName : searchFont,+ };+ UIKeyboardLayoutGuide *keyboardLayoutGuide = chromeHostView.keyboardLayoutGuide;++ UIView *toolbarHost = [[UIView alloc] init];+ toolbarHost.tag = RNSMailSearchToolbarViewTag;+ objc_setAssociatedObject(+ toolbarHost,+ &RNSAppliedMailSearchToolbarConfigKey,+ mailSearchToolbarKey,+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);+ toolbarHost.translatesAutoresizingMaskIntoConstraints = NO;+ [chromeHostView addSubview:toolbarHost];+ // The screen stays mounted beneath pushed routes, so its keyboard layout+ // guide can track a keyboard owned by another screen. Keep the toolbar at+ // rest unless its own search field is editing.+ NSLayoutConstraint *keyboardAvoidConstraint =+ [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor+ constant:-toolbarBottomSpacing];+ keyboardAvoidConstraint.priority = UILayoutPriorityDefaultLow;+ NSLayoutConstraint *restingBottomConstraint =+ [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor+ constant:-toolbarBottomSpacing];+ restingBottomConstraint.priority = UILayoutPriorityDefaultHigh;+ [NSLayoutConstraint activateConstraints:@[+ [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor],+ [toolbarHost.bottomAnchor constraintLessThanOrEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor+ constant:-toolbarBottomSpacing],+ keyboardAvoidConstraint,+ restingBottomConstraint,+ [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth],+ [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight],+ ]];+ [chromeHostView bringSubviewToFront:toolbarHost];++ UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular];+ glassEffect.interactive = YES;+ UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect];+ glassView.clipsToBounds = YES;+ glassView.layer.cornerRadius = toolbarRadius;+ glassView.translatesAutoresizingMaskIntoConstraints = NO;+ [toolbarHost addSubview:glassView];+ BOOL hasFilterButton =+ mailSearchToolbarConfig[@"filterButtonId"] != nil || mailSearchToolbarConfig[@"filterMenu"] != nil;+ BOOL hasComposeButton =+ mailSearchToolbarConfig[@"composeButtonId"] != nil || mailSearchToolbarConfig[@"composeMenu"] != nil;+ CGFloat glassLeadingInset = hasFilterButton ? sideButtonReserve : 0.0;+ CGFloat glassTrailingInset = hasComposeButton ? -sideButtonReserve : 0.0;+ NSLayoutConstraint *glassLeadingConstraint =+ [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:glassLeadingInset];+ NSLayoutConstraint *glassTrailingConstraint =+ [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:glassTrailingInset];+ [NSLayoutConstraint activateConstraints:@[+ glassLeadingConstraint,+ glassTrailingConstraint,+ [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor],+ [glassView.heightAnchor constraintEqualToConstant:toolbarHeight],+ ]];++ void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) {+ auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>(+ config->_eventEmitter);+ if (eventEmitter && buttonId) {+ eventEmitter->onPressHeaderBarButtonItem(+ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{+ .buttonId = std::string([buttonId UTF8String])});+ }+ };++ void (^emitMenuPress)(NSString *) = ^(NSString *menuId) {+ auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>(+ config->_eventEmitter);+ if (eventEmitter && menuId) {+ eventEmitter->onPressHeaderBarButtonMenuItem(+ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{+ .menuId = std::string([menuId UTF8String])});+ }+ };++ UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) =+ ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) {+ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration];+ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule;+ configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"];+ configuration.baseForegroundColor = UIColor.labelColor;+ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil];+ if (menuConfig != nil) {+ button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig+ menuAction:emitMenuPress+ imageLoader:config->_imageLoader];+ button.showsMenuAsPrimaryAction = YES;+ } else if (buttonId != nil) {+ UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) {+ emitButtonPress(buttonId);+ }];+ [button addAction:action forControlEvents:UIControlEventTouchUpInside];+ }+ return button;+ };++ BOOL useFallbackSearchField = [mailSearchToolbarConfig[@"useFallbackSearchField"] boolValue];+ UISearchBar *searchBar =+ !useFallbackSearchField && navitem.searchController != nil ? navitem.searchController.searchBar : nil;+ NSString *placeholder = mailSearchToolbarConfig[@"placeholder"];+ UITextField *resolvedSearchTextField = nil;+ if (searchBar != nil) {+ if (placeholder != nil) {+ searchBar.placeholder = placeholder;+ }+ searchBar.hidden = NO;+ searchBar.alpha = 1.0;+ searchBar.userInteractionEnabled = YES;+ searchBar.searchBarStyle = UISearchBarStyleMinimal;+ searchBar.backgroundImage = [UIImage new];+ searchBar.searchTextField.hidden = NO;+ searchBar.searchTextField.alpha = 1.0;+ searchBar.searchTextField.backgroundColor = UIColor.clearColor;+ searchBar.searchTextField.font = searchFont;+ searchBar.searchTextField.adjustsFontForContentSizeCategory = YES;+ searchBar.searchTextField.textColor = UIColor.labelColor;+ searchBar.searchTextField.tintColor = UIColor.labelColor;+ resolvedSearchTextField = searchBar.searchTextField;+ if (placeholder != nil) {+ searchBar.searchTextField.attributedPlaceholder =+ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes];+ }+ searchBar.translatesAutoresizingMaskIntoConstraints = NO;+ [glassView.contentView addSubview:searchBar];+ [NSLayoutConstraint activateConstraints:@[+ [searchBar.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:8.0],+ [searchBar.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-8.0],+ [searchBar.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor],+ [searchBar.heightAnchor constraintEqualToConstant:searchFieldHeight],+ ]];+ } else {+ UISearchTextField *searchField = [UISearchTextField new];+ if (placeholder != nil) {+ searchField.placeholder = placeholder;+ searchField.attributedPlaceholder =+ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes];+ }+ NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"];Lines 1–500 of 1778. Load more
diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt index 03d8d9f3a5c403c93282d53cbacf1a339e877e85..54d5eabc6c8a408e711a5be97286fdbb8ab1c461 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt @@ -103,6 +103,34 @@ class ScreenStackHeaderConfigViewManager : config.setTitle(title) } + // The t3 patch adds these iOS glass-chrome props to the native spec; they have + // no Android implementation, but the codegen-generated interface requires the + // setters to exist. No-ops keep the Android build compiling. + override fun setSubtitle( + config: ScreenStackHeaderConfig, + subtitle: String?, + ) = Unit + + override fun setLargeSubtitle( + config: ScreenStackHeaderConfig, + largeSubtitle: String?, + ) = Unit + + override fun setNavigationItemStyle( + config: ScreenStackHeaderConfig, + navigationItemStyle: String?, + ) = Unit + + override fun setHeaderCenterBarButtonItems( + config: ScreenStackHeaderConfig, + headerCenterBarButtonItems: ReadableArray?, + ) = Unit + + override fun setHeaderToolbarItems( + config: ScreenStackHeaderConfig, + headerToolbarItems: ReadableArray?, + ) = Unit + override fun setTitleFontFamily( config: ScreenStackHeaderConfig, titleFontFamily: String?, diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h index ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644 --- a/ios/RNSBarButtonItem.h +++ b/ios/RNSBarButtonItem.h @@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); menuAction:(RNSBarButtonMenuItemAction)menuAction imageLoader:(RCTImageLoader *)imageLoader; ++ (UIMenu *)initUIMenuWithDict:(NSDictionary<NSString *, id> *)dict + menuAction:(RNSBarButtonMenuItemAction)menuAction + imageLoader:(RCTImageLoader *)imageLoader; + @end diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 0eb1f09dee82edd99e3e614938233db5cdeaebfe..73298f10807520970f625e166d7f5dd1338206f1 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; } +#if !TARGET_OS_TV && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + NSNumber *glassEffectNum = dict[@"glassEffect"]; + if (@available(iOS 26.0, *)) { + if ([glassEffectNum boolValue]) { + UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; + configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; + UIBackgroundConfiguration *background = configuration.background; + background.strokeWidth = 0.0; + background.strokeColor = UIColor.clearColor; + configuration.background = background; + configuration.contentInsets = NSDirectionalEdgeInsetsZero; + if (title != nil) { + configuration.title = title; + } + + UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; + button.translatesAutoresizingMaskIntoConstraints = NO; + button.enabled = self.enabled; + button.accessibilityLabel = self.accessibilityLabel; + button.accessibilityHint = self.accessibilityHint; + + CGFloat resolvedWidth = width != nil ? width.doubleValue : 56.0; + [button.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; + [button.heightAnchor constraintEqualToConstant:50.0].active = YES; + + [[self class] resolveImageFromConfig:dict + imageLoader:imageLoader + completionBlock:^(UIImage *img) { + UIButtonConfiguration *updatedConfiguration = button.configuration; + updatedConfiguration.image = img; + button.configuration = updatedConfiguration; + }]; + + NSDictionary *menu = dict[@"menu"]; + if (menu) { + button.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction imageLoader:imageLoader]; + button.showsMenuAsPrimaryAction = YES; + } else { + NSString *buttonId = dict[@"buttonId"]; + if (buttonId && action) { + UIAction *pressAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull sender) { + action(buttonId); + }]; + [button addAction:pressAction forControlEvents:UIControlEventTouchUpInside]; + } + } + + self.customView = button; + return self; + } + } +#endif #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) { diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, retain) NSString *title; +@property (nonatomic, retain) NSString *subtitle; +@property (nonatomic, retain) NSString *largeSubtitle; @property (nonatomic, retain) NSString *titleFontFamily; @property (nonatomic, retain) NSNumber *titleFontSize; @property (nonatomic, retain) NSString *titleFontWeight; @@ -45,9 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; +@property (nonatomic) NSInteger navigationItemStyle; @property (nonatomic) RNSBlurEffectStyle blurEffect; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerLeftBarButtonItems; +@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerCenterBarButtonItems; +@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerToolbarItems; @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 5970e3e3a624b9498b8dedfc16831df03a274d0c..5ebff085788d813f1139eb6f9129fb2199dcbd58 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -25,11 +25,33 @@ #import "RNSSearchBar.h" #import "UINavigationBar+RNSUtility.h" +#import <objc/runtime.h> + namespace react = facebook::react; static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17; + +// Keys for the last-applied JS bar button configs, associated with the +// navigation item so unrelated header updates (title, subtitle, tint) don't +// recreate the native buttons they configure. +static char RNSAppliedHeaderBarButtonConfigsKey; +static char RNSAppliedToolbarConfigsKey; static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +static NSInteger navigationItemStyleFromCppEquivalent( + react::RNSScreenStackHeaderConfigNavigationItemStyle navigationItemStyle) +{ + switch (navigationItemStyle) { + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Browser: + return UINavigationItemStyleBrowser; + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Editor: + return UINavigationItemStyleEditor; + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Navigator: + default: + return UINavigationItemStyleNavigator; + } +} + @interface RCTImageLoader (Private) - (id<RCTImageCache>)imageCache; @end @@ -47,6 +69,9 @@ + (BOOL)rnscreens_isBlankOrNull:(NSString *)string @end @interface RNSScreenStackHeaderConfig () <RCTMountingTransactionObserving> +#if !TARGET_OS_TV +- (NSArray<UIBarButtonItemGroup *> *)barButtonItemGroupsFromItems:(NSArray<UIBarButtonItem *> *)items; +#endif @end @implementation RNSScreenStackHeaderConfig { @@ -81,6 +106,7 @@ - (void)initProps self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES; + _navigationItemStyle = UINavigationItemStyleNavigator; _blurEffect = RNSBlurEffectStyleNone; } @@ -496,6 +522,10 @@ + (void)updateViewController:(UIViewController *)vc if (shouldHide) { navitem.title = config.title; + if (@available(iOS 26.0, *)) { + navitem.subtitle = config.subtitle; + navitem.largeSubtitle = config.largeSubtitle; + } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated]; @@ -512,11 +542,19 @@ + (void)updateViewController:(UIViewController *)vc } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; + + if (@available(iOS 16.0, *)) { + navitem.style = (UINavigationItemStyle)config.navigationItemStyle; + } #endif UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; navitem.standardAppearance = appearance; navitem.compactAppearance = appearance; + if (@available(iOS 26.0, *)) { + navitem.subtitle = config.subtitle; + navitem.largeSubtitle = config.largeSubtitle; + } // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV @@ -637,10 +675,458 @@ + (void)updateViewController:(UIViewController *)vc // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title; - navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems - withCurrentItems:navitem.leftBarButtonItems]; - navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems - withCurrentItems:navitem.rightBarButtonItems]; + NSArray *headerLeftConfigs = config.headerLeftBarButtonItems ?: @[]; + NSArray *headerRightConfigs = config.headerRightBarButtonItems ?: @[]; + NSArray *headerCenterConfigs = config.headerCenterBarButtonItems ?: @[]; + NSArray<UIBarButtonItem *> *subviewLeftItems = navitem.leftBarButtonItems ?: @[]; + NSArray<UIBarButtonItem *> *subviewRightItems = navitem.rightBarButtonItems ?: @[]; + // The key includes the config instance's identity: cached items capture + // this config's event emitter in their press handlers, so a remounted + // header-config view with value-equal configs must still rebuild — reusing + // the old items would dispatch presses into the dead config's emitter. + NSArray *headerItemsKey = + @[ @((uintptr_t)config), headerLeftConfigs, headerRightConfigs, headerCenterConfigs ]; + // Rebuilding bar button items creates brand-new native buttons (glass + // UIButton custom views on iOS 26). Replacing them while UIKit animates an + // existing one (menu capsule morph, push/pop glass transitions) strands the + // animation overlay — a stuck expanded capsule or an unmasked square back + // button. When the JS configs are unchanged, keep the already-applied items. + // Subview-backed items are re-derived every pass, so their presence forces + // the rebuild path. + BOOL reuseHeaderBarButtonItems = subviewLeftItems.count == 0 && subviewRightItems.count == 0 && + [objc_getAssociatedObject(navitem, &RNSAppliedHeaderBarButtonConfigsKey) isEqual:headerItemsKey]; + if (!reuseHeaderBarButtonItems) { + NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems + withCurrentItems:navitem.leftBarButtonItems + navigationItem:navitem]; + NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems + withCurrentItems:navitem.rightBarButtonItems + navigationItem:navitem]; + NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems + withCurrentItems:@[] + navigationItem:navitem]; +#if !TARGET_OS_TV + if (@available(iOS 16.0, *)) { + navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems]; + navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems]; + if (@available(iOS 26.0, *)) { + navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; + } + navitem.leftBarButtonItems = nil; + navitem.rightBarButtonItems = nil; + } else { + navitem.leftBarButtonItems = leftBarButtonItems; + navitem.rightBarButtonItems = rightBarButtonItems; + } +#else + navitem.leftBarButtonItems = leftBarButtonItems; + navitem.rightBarButtonItems = rightBarButtonItems; +#endif + // Only dict-driven items can be reused: with subview-backed items in the + // mix the applied state depends on view identity, so clear the key to + // force a rebuild on the next pass. + objc_setAssociatedObject( + navitem, + &RNSAppliedHeaderBarButtonConfigsKey, + subviewLeftItems.count == 0 && subviewRightItems.count == 0 ? headerItemsKey : nil, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil; + for (NSDictionary<NSString *, id> *toolbarConfig in config.headerToolbarItems) { + if (toolbarConfig[@"mailSearchToolbar"]) { + mailSearchToolbarConfig = toolbarConfig; + break; + } + } + if (mailSearchToolbarConfig == nil) { + for (NSDictionary<NSString *, id> *rightConfig in config.headerRightBarButtonItems) { + if ([rightConfig[@"bottomMailSearchToolbar"] boolValue]) { + mailSearchToolbarConfig = rightConfig; + break; + } + } + } + + static NSInteger const RNSMailSearchToolbarViewTag = 260628; + UIView *chromeHostView = vc.view; + UIView *existingMailSearchToolbar = [chromeHostView viewWithTag:RNSMailSearchToolbarViewTag]; + if (existingMailSearchToolbar == nil) { + existingMailSearchToolbar = [vc.view viewWithTag:RNSMailSearchToolbarViewTag]; + } + // Reuse the live toolbar when nothing it was built from changed: rebuilding + // replaces the fallback UISearchTextField with a fresh empty one, dropping + // the user's in-progress search text and first responder on every unrelated + // header update. Keyed on the config instance (the button/search blocks + // capture its event emitter), the toolbar config values, and the host width + // (the width constraint constant is resolved from it at build time). + static char RNSAppliedMailSearchToolbarConfigKey; + NSArray *mailSearchToolbarKey = mailSearchToolbarConfig != nil + ? @[ @((uintptr_t)config), mailSearchToolbarConfig, @(chromeHostView.bounds.size.width) ] + : nil; + BOOL reuseMailSearchToolbar = existingMailSearchToolbar != nil && mailSearchToolbarKey != nil && + [objc_getAssociatedObject(existingMailSearchToolbar, &RNSAppliedMailSearchToolbarConfigKey) + isEqual:mailSearchToolbarKey]; + if (!reuseMailSearchToolbar) { + [existingMailSearchToolbar removeFromSuperview]; + } + + if (mailSearchToolbarConfig != nil && !reuseMailSearchToolbar) { +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + if (@available(iOS 26.0, *)) { + CGFloat horizontalInset = 18.0; + NSNumber *width = mailSearchToolbarConfig[@"width"]; + CGFloat hostWidth = chromeHostView.bounds.size.width; + if (hostWidth <= 0.0) { + hostWidth = vc.view.bounds.size.width; + } + if (hostWidth <= 0.0) { + hostWidth = [UIScreen mainScreen].bounds.size.width; + } + CGFloat fallbackWidth = MIN(560.0, MAX(300.0, hostWidth - (horizontalInset * 2.0))); + CGFloat resolvedWidth = width != nil ? width.doubleValue : fallbackWidth; + if (hostWidth > 0.0) { + resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0)); + } + + CGFloat toolbarHeight = 48.0; + CGFloat toolbarRadius = toolbarHeight / 2.0; + CGFloat buttonSize = toolbarHeight; + CGFloat sideButtonReserve = buttonSize + 8.0; + CGFloat searchFieldHeight = 42.0; + CGFloat toolbarBottomSpacing = 4.0; + UIFont *searchFont = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle3]; + NSDictionary<NSAttributedStringKey, id> *placeholderAttributes = @{ + NSForegroundColorAttributeName : UIColor.secondaryLabelColor, + NSFontAttributeName : searchFont, + }; + UIKeyboardLayoutGuide *keyboardLayoutGuide = chromeHostView.keyboardLayoutGuide; + + UIView *toolbarHost = [[UIView alloc] init]; + toolbarHost.tag = RNSMailSearchToolbarViewTag; + objc_setAssociatedObject( + toolbarHost, + &RNSAppliedMailSearchToolbarConfigKey, + mailSearchToolbarKey, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + toolbarHost.translatesAutoresizingMaskIntoConstraints = NO; + [chromeHostView addSubview:toolbarHost]; + // The screen stays mounted beneath pushed routes, so its keyboard layout + // guide can track a keyboard owned by another screen. Keep the toolbar at + // rest unless its own search field is editing. + NSLayoutConstraint *keyboardAvoidConstraint = + [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor + constant:-toolbarBottomSpacing]; + keyboardAvoidConstraint.priority = UILayoutPriorityDefaultLow; + NSLayoutConstraint *restingBottomConstraint = + [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor + constant:-toolbarBottomSpacing]; + restingBottomConstraint.priority = UILayoutPriorityDefaultHigh; + [NSLayoutConstraint activateConstraints:@[ + [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor], + [toolbarHost.bottomAnchor constraintLessThanOrEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor + constant:-toolbarBottomSpacing], + keyboardAvoidConstraint, + restingBottomConstraint, + [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth], + [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + [chromeHostView bringSubviewToFront:toolbarHost]; + + UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular]; + glassEffect.interactive = YES; + UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect]; + glassView.clipsToBounds = YES; + glassView.layer.cornerRadius = toolbarRadius; + glassView.translatesAutoresizingMaskIntoConstraints = NO; + [toolbarHost addSubview:glassView]; + BOOL hasFilterButton = + mailSearchToolbarConfig[@"filterButtonId"] != nil || mailSearchToolbarConfig[@"filterMenu"] != nil; + BOOL hasComposeButton = + mailSearchToolbarConfig[@"composeButtonId"] != nil || mailSearchToolbarConfig[@"composeMenu"] != nil; + CGFloat glassLeadingInset = hasFilterButton ? sideButtonReserve : 0.0; + CGFloat glassTrailingInset = hasComposeButton ? -sideButtonReserve : 0.0; + NSLayoutConstraint *glassLeadingConstraint = + [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:glassLeadingInset]; + NSLayoutConstraint *glassTrailingConstraint = + [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:glassTrailingInset]; + [NSLayoutConstraint activateConstraints:@[ + glassLeadingConstraint, + glassTrailingConstraint, + [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], + [glassView.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + + void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) { + auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( + config->_eventEmitter); + if (eventEmitter && buttonId) { + eventEmitter->onPressHeaderBarButtonItem( + facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{ + .buttonId = std::string([buttonId UTF8String])}); + } + }; + + void (^emitMenuPress)(NSString *) = ^(NSString *menuId) { + auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( + config->_eventEmitter); + if (eventEmitter && menuId) { + eventEmitter->onPressHeaderBarButtonMenuItem( + facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{ + .menuId = std::string([menuId UTF8String])}); + } + }; + + UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) = + ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) { + UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; + configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; + configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"]; + configuration.baseForegroundColor = UIColor.labelColor; + UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; + if (menuConfig != nil) { + button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig + menuAction:emitMenuPress + imageLoader:config->_imageLoader]; + button.showsMenuAsPrimaryAction = YES; + } else if (buttonId != nil) { + UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { + emitButtonPress(buttonId); + }]; + [button addAction:action forControlEvents:UIControlEventTouchUpInside]; + } + return button; + }; + + BOOL useFallbackSearchField = [mailSearchToolbarConfig[@"useFallbackSearchField"] boolValue]; + UISearchBar *searchBar = + !useFallbackSearchField && navitem.searchController != nil ? navitem.searchController.searchBar : nil; + NSString *placeholder = mailSearchToolbarConfig[@"placeholder"]; + UITextField *resolvedSearchTextField = nil; + if (searchBar != nil) { + if (placeholder != nil) { + searchBar.placeholder = placeholder; + } + searchBar.hidden = NO; + searchBar.alpha = 1.0; + searchBar.userInteractionEnabled = YES; + searchBar.searchBarStyle = UISearchBarStyleMinimal; + searchBar.backgroundImage = [UIImage new]; + searchBar.searchTextField.hidden = NO; + searchBar.searchTextField.alpha = 1.0; + searchBar.searchTextField.backgroundColor = UIColor.clearColor; + searchBar.searchTextField.font = searchFont; + searchBar.searchTextField.adjustsFontForContentSizeCategory = YES; + searchBar.searchTextField.textColor = UIColor.labelColor; + searchBar.searchTextField.tintColor = UIColor.labelColor; + resolvedSearchTextField = searchBar.searchTextField; + if (placeholder != nil) { + searchBar.searchTextField.attributedPlaceholder = + [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; + } + searchBar.translatesAutoresizingMaskIntoConstraints = NO; + [glassView.contentView addSubview:searchBar]; + [NSLayoutConstraint activateConstraints:@[ + [searchBar.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:8.0], + [searchBar.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-8.0], + [searchBar.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], + [searchBar.heightAnchor constraintEqualToConstant:searchFieldHeight], + ]]; + } else { + UISearchTextField *searchField = [UISearchTextField new]; + if (placeholder != nil) { + searchField.placeholder = placeholder; + searchField.attributedPlaceholder = + [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; + } + NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"];