From fa7d4dcb4b5e3f154d216f20525565f9e048a144 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Wed, 25 Jun 2025 15:27:45 +0200 Subject: [PATCH 01/99] basic stuff working but needs cleanup --- apps/Example.tsx | 6 +++ apps/src/screens/BarButtonItems.tsx | 58 +++++++++++++++++++++++++++++ ios/RNSBarButtonItem.h | 10 +++++ ios/RNSBarButtonItem.mm | 41 ++++++++++++++++++++ ios/RNSScreenStackHeaderConfig.h | 3 ++ ios/RNSScreenStackHeaderConfig.mm | 37 ++++++++++++++++++ 6 files changed, 155 insertions(+) create mode 100644 apps/src/screens/BarButtonItems.tsx create mode 100644 ios/RNSBarButtonItem.h create mode 100644 ios/RNSBarButtonItem.mm diff --git a/apps/Example.tsx b/apps/Example.tsx index 0fd66e7cc6..8ba25dbf2a 100644 --- a/apps/Example.tsx +++ b/apps/Example.tsx @@ -30,6 +30,7 @@ import Orientation from './src/screens/Orientation'; import SearchBar from './src/screens/SearchBar'; import Events from './src/screens/Events'; import Gestures from './src/screens/Gestures'; +import BarButtonItems from './src/screens/BarButtonItems'; import { GestureDetectorProvider } from 'react-native-screens/gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -127,6 +128,11 @@ const SCREENS: Record< component: Gestures, type: 'playground', }, + BarButtonItems: { + title: 'Bar Button Items', + component: BarButtonItems, + type: 'playground', + }, }; if (isTestSectionEnabled()) { diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx new file mode 100644 index 0000000000..56da8cf8ad --- /dev/null +++ b/apps/src/screens/BarButtonItems.tsx @@ -0,0 +1,58 @@ +// NOTE: The full native feature set (style, image, menu, etc.) is available, but the TS types in src/types.tsx need to be updated to match. This example uses only the currently typed props (title, icon, onPress, enabled). +import React, { useCallback } from 'react'; +import { View, Text, Alert, Image } from 'react-native'; +import { + createNativeStackNavigator, +} from '@react-navigation/native-stack'; + +const Screen = () => { + return + + UIBarButtonItem Features Demo + + • Title, icon, enabled/disabled, ergonomic onPress + • Full feature set (style, image, menu, etc.) available natively + • Update src/types.tsx to use all features in your app +; +}; + +const Stack = createNativeStackNavigator(); + +export default function BarButtonItemsExample() { + // Handlers for demonstration + return ( + + Alert.alert('Plain pressed'), + tintColor: 'red', + style: 2, + }, + ], + // Example: Right bar button items (using only typed props) + headerRightBarButtonItems: [ + { + onPress: () => Alert.alert('Search pressed'), + image: require('../assets/search_black.png'), + style: 1, + enabled: false, + }, + { + onPress: () => Alert.alert('Search pressed'), + image: require('../assets/search_black.png'), + style: 2, + }, + ], + }} + component={Screen} + /> + + + ); +} diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h new file mode 100644 index 0000000000..706d93b6af --- /dev/null +++ b/ios/RNSBarButtonItem.h @@ -0,0 +1,10 @@ +#import + +typedef void (^RNSBarButtonItemAction)(NSString *buttonId); + +@interface RNSBarButtonItem : UIBarButtonItem + +- (instancetype)initWithDictionary:(NSDictionary *)dict + action:(RNSBarButtonItemAction)action; + +@end diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm new file mode 100644 index 0000000000..1345da882d --- /dev/null +++ b/ios/RNSBarButtonItem.mm @@ -0,0 +1,41 @@ +#import "RNSBarButtonItem.h" +#import +#import + +static char RNSBarButtonItemActionKey; +static char RNSBarButtonItemIdKey; + +@implementation RNSBarButtonItem + +- (instancetype)initWithDictionary:(NSDictionary *)dict + action:(RNSBarButtonItemAction)action +{ + self = [super init]; + if (self) { + self.title = dict[@"title"]; + NSDictionary *imageObj = dict[@"image"]; + if (imageObj) { + self.image = [RCTConvert UIImage:imageObj]; + } + + NSString *buttonId = dict[@"buttonId"]; + if (buttonId && action) { + self.target = self; + self.action = @selector(handleBarButtonItemPress:); + objc_setAssociatedObject(self, &RNSBarButtonItemIdKey, buttonId, OBJC_ASSOCIATION_COPY_NONATOMIC); + objc_setAssociatedObject(self, &RNSBarButtonItemActionKey, action, OBJC_ASSOCIATION_COPY_NONATOMIC); + } + } + return self; +} + +- (void)handleBarButtonItemPress:(UIBarButtonItem *)item +{ + NSString *buttonId = objc_getAssociatedObject(self, &RNSBarButtonItemIdKey); + RNSBarButtonItemAction action = objc_getAssociatedObject(self, &RNSBarButtonItemActionKey); + if (action && buttonId) { + action(buttonId); + } +} + +@end diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 12d0c76934..61d39ab418 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -59,6 +59,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; @property (nonatomic) RNSBlurEffectStyle blurEffect; +@property (nonatomic, copy, nullable) NSArray *> *headerRightBarButtonItems; +@property (nonatomic, copy, nullable) NSArray *> *headerLeftBarButtonItems; +@property (nonatomic) RCTDirectEventBlock onPressHeaderBarButtonItem; NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index b01f97c04b..77290232cf 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -32,6 +32,8 @@ #import "RNSScreen.h" #import "RNSScreenStackHeaderConfig.h" #import "RNSSearchBar.h" +#import "RNSUIBarButtonItem.h" +#import "RNSBarButtonItem.h" #ifdef RCT_NEW_ARCH_ENABLED namespace react = facebook::react; @@ -620,6 +622,8 @@ + (void)updateViewController:(UIViewController *)vc #endif navitem.leftBarButtonItem = nil; navitem.rightBarButtonItem = nil; + navitem.leftBarButtonItems = nil; + navitem.rightBarButtonItems = nil; navitem.titleView = nil; #if !TARGET_OS_TV @@ -708,6 +712,15 @@ + (void)updateViewController:(UIViewController *)vc // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title; + // Set leftBarButtonItems if provided + if (config.headerLeftBarButtonItems) { + navitem.leftBarButtonItems = [config barButtonItemsFromDictionaries:config.headerLeftBarButtonItems]; + } + // Set rightBarButtonItems if provided + if (config.headerRightBarButtonItems) { + navitem.rightBarButtonItems = [config barButtonItemsFromDictionaries:config.headerRightBarButtonItems]; + } + if (animated && vc.transitionCoordinator != nil && vc.transitionCoordinator.presentationStyle == UIModalPresentationNone && !wasHidden) { // when there is an ongoing transition we may need to update navbar setting in animation block @@ -832,6 +845,26 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * } } +- (NSArray *)barButtonItemsFromDictionaries:(NSArray *> *)dicts { + NSMutableArray *items = [NSMutableArray arrayWithCapacity:dicts.count * 2 - 1]; + for (NSUInteger i = 0; i < dicts.count; i++) { + NSDictionary *dict = dicts[i]; + RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { + if (self.onPressHeaderBarButtonItem && buttonId) { + self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); + } + }]; + [items addObject:item]; + if (i < dicts.count - 1) { + UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + fixedSpace.width = 0; + [items addObject:fixedSpace]; + } + } + return items; +} + + RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { @@ -1115,6 +1148,7 @@ + (RCTImageSource *)imageSourceFromImageView:(RCTImageView *)view } #endif + @end #ifdef RCT_NEW_ARCH_ENABLED @@ -1170,6 +1204,9 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(backButtonDisplayMode, UINavigationItemBackButtonDisplayMode) RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) // `hidden` is an UIView property, we need to use different name internally RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) +RCT_EXPORT_VIEW_PROPERTY(headerLeftBarButtonItems, NSArray) +RCT_EXPORT_VIEW_PROPERTY(headerRightBarButtonItems, NSArray) +RCT_EXPORT_VIEW_PROPERTY(onPressHeaderBarButtonItem, RCTDirectEventBlock); @end From 178dcffb9db63de11937a645dd0bd7df4cd71114 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Wed, 25 Jun 2025 16:38:21 +0200 Subject: [PATCH 02/99] all basic props works --- apps/src/screens/BarButtonItems.tsx | 7 +++-- ios/RNSBarButtonItem.mm | 41 +++++++++++++++++++++++++++++ ios/RNSScreenStackHeaderConfig.mm | 10 +++---- react-navigation | 2 +- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index 56da8cf8ad..f5b6e1aa87 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -31,8 +31,7 @@ export default function BarButtonItemsExample() { { title: 'Plain', onPress: () => Alert.alert('Plain pressed'), - tintColor: 'red', - style: 2, + tintColor: '#ff0000', }, ], // Example: Right bar button items (using only typed props) @@ -40,13 +39,13 @@ export default function BarButtonItemsExample() { { onPress: () => Alert.alert('Search pressed'), image: require('../assets/search_black.png'), - style: 1, + style: 'Plain', enabled: false, }, { onPress: () => Alert.alert('Search pressed'), image: require('../assets/search_black.png'), - style: 2, + style: 'Prominent', }, ], }} diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 1345da882d..aa6ebfcf55 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -13,10 +13,51 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict self = [super init]; if (self) { self.title = dict[@"title"]; + NSDictionary *imageObj = dict[@"image"]; if (imageObj) { self.image = [RCTConvert UIImage:imageObj]; } + + id tintColorObj = dict[@"tintColor"]; + if (tintColorObj) { + self.tintColor = [RCTConvert UIColor:tintColorObj]; + } + + if (@available(iOS 16.0, *)) { + NSNumber *hiddenNum = dict[@"hidden"]; + if (hiddenNum != nil) { + self.hidden = [hiddenNum boolValue]; + } + } + + NSNumber *selectedNum = dict[@"selected"]; + if (selectedNum != nil) { + self.selected = [selectedNum boolValue]; + } + + NSNumber *enabledNum = dict[@"enabled"]; + if (enabledNum != nil) { + self.enabled = [enabledNum boolValue]; + } + + NSNumber *width = dict[@"width"]; + if (width) { + self.width = [width doubleValue]; + } + + NSString *style = dict[@"style"]; + if (style) { + if ([style isEqualToString:@"Done"]) { + self.style = UIBarButtonItemStyleDone; + } else if ([style isEqualToString:@"Prominent"]) { + if (@available(iOS 26.0, *)) { + self.style = UIBarButtonItemStyleProminent; + } + } else { + self.style = UIBarButtonItemStylePlain; + } + } NSString *buttonId = dict[@"buttonId"]; if (buttonId && action) { diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 77290232cf..1e1eefc7f2 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -855,11 +855,11 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * } }]; [items addObject:item]; - if (i < dicts.count - 1) { - UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - fixedSpace.width = 0; - [items addObject:fixedSpace]; - } + // if (i < dicts.count - 1) { + // UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + // fixedSpace.width = 0; + // [items addObject:fixedSpace]; + // } } return items; } diff --git a/react-navigation b/react-navigation index 38a2a320b0..e5861bb831 160000 --- a/react-navigation +++ b/react-navigation @@ -1 +1 @@ -Subproject commit 38a2a320b0a4b5514152f8e31d7ef480cae94d67 +Subproject commit e5861bb83100f84b04f45295dd50f43e3a7974f7 From 82d0011cb01d1e098be2d1bb74d9ce56c5af94ac Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Wed, 25 Jun 2025 22:01:43 +0200 Subject: [PATCH 03/99] add support for spacing --- apps/src/screens/BarButtonItems.tsx | 3 +++ ios/RNSScreenStackHeaderConfig.mm | 24 +++++++++++++----------- react-navigation | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index f5b6e1aa87..96d2d71996 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -42,6 +42,9 @@ export default function BarButtonItemsExample() { style: 'Plain', enabled: false, }, + { + spacing: 0, + }, { onPress: () => Alert.alert('Search pressed'), image: require('../assets/search_black.png'), diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 1e1eefc7f2..d346795e3d 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -849,17 +849,19 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * NSMutableArray *items = [NSMutableArray arrayWithCapacity:dicts.count * 2 - 1]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; - RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { - if (self.onPressHeaderBarButtonItem && buttonId) { - self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); - } - }]; - [items addObject:item]; - // if (i < dicts.count - 1) { - // UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; - // fixedSpace.width = 0; - // [items addObject:fixedSpace]; - // } + if (dict[@"buttonId"]) { + RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { + if (self.onPressHeaderBarButtonItem && buttonId) { + self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); + } + }]; + [items addObject:item]; + } else if (dict[@"spacing"]) { + UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; + NSNumber *spacingValue = dict[@"spacing"]; + fixedSpace.width = [spacingValue doubleValue]; + [items addObject:fixedSpace]; + } } return items; } diff --git a/react-navigation b/react-navigation index e5861bb831..31b58f2878 160000 --- a/react-navigation +++ b/react-navigation @@ -1 +1 @@ -Subproject commit e5861bb83100f84b04f45295dd50f43e3a7974f7 +Subproject commit 31b58f2878084c3ea0d49b5a1ab6beba38d5718d From c18409626727c80646c5efe8b9eff935ee54a51e Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Wed, 25 Jun 2025 23:08:24 +0200 Subject: [PATCH 04/99] some more ios26 props --- apps/src/screens/BarButtonItems.tsx | 38 +++++++++++++---------------- ios/RNSBarButtonItem.mm | 23 +++++++++++++++++ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index 96d2d71996..6dc3b077f7 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -1,37 +1,36 @@ // NOTE: The full native feature set (style, image, menu, etc.) is available, but the TS types in src/types.tsx need to be updated to match. This example uses only the currently typed props (title, icon, onPress, enabled). -import React, { useCallback } from 'react'; -import { View, Text, Alert, Image } from 'react-native'; +import React from 'react'; +import { View, Alert } from 'react-native'; import { createNativeStackNavigator, } from '@react-navigation/native-stack'; +import { ScrollView } from 'react-native-gesture-handler'; const Screen = () => { - return - - UIBarButtonItem Features Demo - - • Title, icon, enabled/disabled, ergonomic onPress - • Full feature set (style, image, menu, etc.) available natively - • Update src/types.tsx to use all features in your app -; + return ( + + + + + + ); }; const Stack = createNativeStackNavigator(); export default function BarButtonItemsExample() { - // Handlers for demonstration return ( - Alert.alert('Plain pressed'), - tintColor: '#ff0000', }, ], // Example: Right bar button items (using only typed props) @@ -40,20 +39,17 @@ export default function BarButtonItemsExample() { onPress: () => Alert.alert('Search pressed'), image: require('../assets/search_black.png'), style: 'Plain', - enabled: false, }, { - spacing: 0, - }, - { - onPress: () => Alert.alert('Search pressed'), + onPress: () => Alert.alert('Button pressed 2'), image: require('../assets/search_black.png'), style: 'Prominent', + tintColor: 'green', }, ], }} component={Screen} - /> + /> ); diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index aa6ebfcf55..93229a7d7a 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -46,6 +46,29 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict self.width = [width doubleValue]; } + if (@available(iOS 15.0, *)) { + NSNumber *changesSelectionAsPrimaryActionNum = dict[@"changesSelectionAsPrimaryAction"]; + if (changesSelectionAsPrimaryActionNum != nil) { + self.changesSelectionAsPrimaryAction = [changesSelectionAsPrimaryActionNum boolValue]; + } + } + + if (@available(iOS 26.0, *)) { + NSNumber *hidesSharedBackgroundNum = dict[@"hidesSharedBackground"]; + if (hidesSharedBackgroundNum != nil) { + self.hidesSharedBackground = [hidesSharedBackgroundNum boolValue]; + } + NSNumber *sharesBackgroundNum = dict[@"sharesBackground"]; + if (sharesBackgroundNum != nil) { + self.sharesBackground = [sharesBackgroundNum boolValue]; + } + NSString *identifier = dict[@"identifier"]; + if (identifier != nil) { + //self.identifier = identifier; + } + } + + NSString *style = dict[@"style"]; if (style) { if ([style isEqualToString:@"Done"]) { From 231ca6f1c920a74954132102d7abc41499aa4f7a Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Wed, 25 Jun 2025 23:41:31 +0200 Subject: [PATCH 05/99] support titleStyle --- apps/src/screens/BarButtonItems.tsx | 4 +-- ios/RNSBarButtonItem.mm | 38 ++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index 6dc3b077f7..bcafa9b35a 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -25,15 +25,15 @@ export default function BarButtonItemsExample() { name="BarButtonItems Demo" options={{ headerTransparent: true, + headerBlurEffect: 'regular', title: 'BarButtonItems Demo', - // Example: Left bar button items (using only typed props) headerLeftBarButtonItems: [ { title: 'Plain', onPress: () => Alert.alert('Plain pressed'), + titleStyle: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '800' }, }, ], - // Example: Right bar button items (using only typed props) headerRightBarButtonItems: [ { onPress: () => Alert.alert('Search pressed'), diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 93229a7d7a..ef2025106a 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -1,6 +1,7 @@ #import "RNSBarButtonItem.h" #import #import +#import static char RNSBarButtonItemActionKey; static char RNSBarButtonItemIdKey; @@ -18,18 +19,45 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict if (imageObj) { self.image = [RCTConvert UIImage:imageObj]; } + + NSDictionary *titleStyle = dict[@"titleStyle"]; + if (titleStyle) { + NSString *fontFamily = titleStyle[@"fontFamily"]; + NSNumber *fontSize = titleStyle[@"fontSize"]; + NSString *fontWeight = titleStyle[@"fontWeight"]; + NSMutableDictionary *attrs = [NSMutableDictionary new]; + if (fontFamily || fontWeight) { + attrs[NSFontAttributeName] = [RCTFont updateFont:nil + withFamily:fontFamily + size:fontSize + weight:fontWeight + style:nil + variant:nil + scaleMultiplier:1.0]; + } else { + attrs[NSFontAttributeName] = [UIFont systemFontOfSize:[fontSize floatValue]]; + } + [self setTitleTextAttributes:attrs forState:UIControlStateNormal]; + [self setTitleTextAttributes:attrs forState:UIControlStateHighlighted]; + [self setTitleTextAttributes:attrs forState:UIControlStateDisabled]; + [self setTitleTextAttributes:attrs forState:UIControlStateSelected]; + [self setTitleTextAttributes:attrs forState:UIControlStateFocused]; + } id tintColorObj = dict[@"tintColor"]; if (tintColorObj) { self.tintColor = [RCTConvert UIColor:tintColorObj]; } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_16_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0 if (@available(iOS 16.0, *)) { NSNumber *hiddenNum = dict[@"hidden"]; if (hiddenNum != nil) { self.hidden = [hiddenNum boolValue]; } } + #endif NSNumber *selectedNum = dict[@"selected"]; if (selectedNum != nil) { @@ -45,14 +73,18 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict if (width) { self.width = [width doubleValue]; } - + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_15_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0 if (@available(iOS 15.0, *)) { NSNumber *changesSelectionAsPrimaryActionNum = dict[@"changesSelectionAsPrimaryAction"]; if (changesSelectionAsPrimaryActionNum != nil) { self.changesSelectionAsPrimaryAction = [changesSelectionAsPrimaryActionNum boolValue]; } } + #endif + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_26_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 if (@available(iOS 26.0, *)) { NSNumber *hidesSharedBackgroundNum = dict[@"hidesSharedBackground"]; if (hidesSharedBackgroundNum != nil) { @@ -67,6 +99,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict //self.identifier = identifier; } } + #endif NSString *style = dict[@"style"]; @@ -74,9 +107,12 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict if ([style isEqualToString:@"Done"]) { self.style = UIBarButtonItemStyleDone; } else if ([style isEqualToString:@"Prominent"]) { + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_26_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 if (@available(iOS 26.0, *)) { self.style = UIBarButtonItemStyleProminent; } + #endif } else { self.style = UIBarButtonItemStylePlain; } From 2d942e2f92fb40adc464119bf87e76691f1a6951 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Thu, 26 Jun 2025 13:39:43 +0200 Subject: [PATCH 06/99] titleStyle color --- apps/src/screens/BarButtonItems.tsx | 28 +++++++++++++++--------- ios/RNSBarButtonItem.mm | 33 ++++++++++++++++++++++++++++- ios/RNSScreenStackHeaderConfig.mm | 2 +- react-navigation | 2 +- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index bcafa9b35a..3b6bd6f862 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -25,26 +25,34 @@ export default function BarButtonItemsExample() { name="BarButtonItems Demo" options={{ headerTransparent: true, - headerBlurEffect: 'regular', title: 'BarButtonItems Demo', headerLeftBarButtonItems: [ { - title: 'Plain', + title: '+allt', onPress: () => Alert.alert('Plain pressed'), - titleStyle: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '800' }, + titleStyle: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '800', color: 'black' }, + style: 'Prominent', + tintColor: 'yellow', }, - ], - headerRightBarButtonItems: [ { - onPress: () => Alert.alert('Search pressed'), image: require('../assets/search_black.png'), - style: 'Plain', + onPress: () => Alert.alert('Search pressed'), }, + ], + headerRightBarButtonItems: [ { - onPress: () => Alert.alert('Button pressed 2'), - image: require('../assets/search_black.png'), style: 'Prominent', - tintColor: 'green', + title: 'Menu', + menu: [ + { + title: 'Search', + onPress: () => Alert.alert('Search pressed'), + }, + { + title: 'Search with long text that wraps', + onPress: () => Alert.alert('Search with long text pressed'), + }, + ], }, ], }} diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index ef2025106a..1aa007da63 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -37,6 +37,10 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict } else { attrs[NSFontAttributeName] = [UIFont systemFontOfSize:[fontSize floatValue]]; } + id titleColor = titleStyle[@"color"]; + if (titleColor) { + attrs[NSForegroundColorAttributeName] = [RCTConvert UIColor:titleColor]; + } [self setTitleTextAttributes:attrs forState:UIControlStateNormal]; [self setTitleTextAttributes:attrs forState:UIControlStateHighlighted]; [self setTitleTextAttributes:attrs forState:UIControlStateDisabled]; @@ -118,6 +122,33 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict } } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 + if (@available(iOS 14.0, *)) { + NSArray *menuItems = dict[@"menu"]; + if (menuItems.count > 0) { + NSMutableArray *actions = [NSMutableArray new]; + for (NSDictionary *item in menuItems) { + NSString *title = item[@"title"]; + if (![title isKindOfClass:[NSString class]]) continue; + UIAction *actionElement = [UIAction actionWithTitle:title + image:nil + identifier:nil + handler:^(__kindof UIAction * _Nonnull a) { + RNSBarButtonItemAction parentAction = objc_getAssociatedObject(self, &RNSBarButtonItemActionKey); + if (parentAction) { + parentAction(title); + } + }]; + [actions addObject:actionElement]; + } + NSMutableArray *children = [NSMutableArray new]; + + [children addObject:[UIMenu menuWithTitle:@"på" children:actions]]; + self.menu = [UIMenu menuWithTitle:@"hej" children:children]; + } + } + #endif + NSString *buttonId = dict[@"buttonId"]; if (buttonId && action) { self.target = self; @@ -138,4 +169,4 @@ - (void)handleBarButtonItemPress:(UIBarButtonItem *)item } } -@end +@end diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index d346795e3d..883a13bd44 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -849,7 +849,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * NSMutableArray *items = [NSMutableArray arrayWithCapacity:dicts.count * 2 - 1]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; - if (dict[@"buttonId"]) { + if (dict[@"buttonId"] || dict[@"menu"]) { RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { if (self.onPressHeaderBarButtonItem && buttonId) { self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); diff --git a/react-navigation b/react-navigation index 31b58f2878..d812dd395b 160000 --- a/react-navigation +++ b/react-navigation @@ -1 +1 @@ -Subproject commit 31b58f2878084c3ea0d49b5a1ab6beba38d5718d +Subproject commit d812dd395b9ff89be4fb4950a4628be702f4e721 From fafe5eecd9d4778a5410258d659a44a116f85dac Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Thu, 26 Jun 2025 14:29:30 +0200 Subject: [PATCH 07/99] accesibility label and hint --- ios/RNSBarButtonItem.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 1aa007da63..66549ae84c 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -100,7 +100,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict } NSString *identifier = dict[@"identifier"]; if (identifier != nil) { - //self.identifier = identifier; + self.identifier = identifier; } } #endif @@ -122,6 +122,13 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict } } + if (dict[@"accessibilityLabel"]) { + self.accessibilityLabel = dict[@"accessibilityLabel"]; + } + if (dict[@"accessibilityHint"]) { + self.accessibilityHint = dict[@"accessibilityHint"]; + } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 if (@available(iOS 14.0, *)) { NSArray *menuItems = dict[@"menu"]; From 8f07970ee591563987a207b3462f729135c5393e Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Thu, 26 Jun 2025 22:38:25 +0200 Subject: [PATCH 08/99] UIMenu press callback --- apps/src/screens/BarButtonItems.tsx | 23 ++++++++++- ios/RNSBarButtonItem.h | 4 +- ios/RNSBarButtonItem.mm | 62 +++++++++++++++++++---------- ios/RNSScreenStackHeaderConfig.h | 1 + ios/RNSScreenStackHeaderConfig.mm | 18 ++++++--- 5 files changed, 77 insertions(+), 31 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index 3b6bd6f862..6be666b464 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -37,23 +37,42 @@ export default function BarButtonItemsExample() { { image: require('../assets/search_black.png'), onPress: () => Alert.alert('Search pressed'), + accessibilityLabel: 'SÖK HÄR', + accessibilityHint: 'Tryck för att söka', }, ], headerRightBarButtonItems: [ { style: 'Prominent', title: 'Menu', - menu: [ + tintColor: 'purple', + menu: { + items: [ { title: 'Search', + systemImage: 'magnifyingglass.circle.fill', onPress: () => Alert.alert('Search pressed'), }, { title: 'Search with long text that wraps', onPress: () => Alert.alert('Search with long text pressed'), }, + { + title: 'Submenu', + items: [ + { + title: 'Submenu Item 1', + systemImage: 'magnifyingglass.circle.fill', + onPress: () => Alert.alert('Submenu Item 1 pressed'), + }, + { + title: 'Submenu Item 2', + onPress: () => Alert.alert('Submenu Item 2 pressed'), + }, + ], + }, ], - }, + }}, ], }} component={Screen} diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h index 706d93b6af..045534baeb 100644 --- a/ios/RNSBarButtonItem.h +++ b/ios/RNSBarButtonItem.h @@ -1,10 +1,10 @@ #import typedef void (^RNSBarButtonItemAction)(NSString *buttonId); +typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); @interface RNSBarButtonItem : UIBarButtonItem -- (instancetype)initWithDictionary:(NSDictionary *)dict - action:(RNSBarButtonItemAction)action; +- (instancetype)initWithDictionary:(NSDictionary *)dict action:(RNSBarButtonItemAction)action menuAction:(RNSBarButtonMenuItemAction)menuAction; @end diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 66549ae84c..82487c203d 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -9,7 +9,9 @@ @implementation RNSBarButtonItem - (instancetype)initWithDictionary:(NSDictionary *)dict - action:(RNSBarButtonItemAction)action + action:(RNSBarButtonItemAction)action + menuAction:(RNSBarButtonMenuItemAction)menuAction + { self = [super init]; if (self) { @@ -131,27 +133,9 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 if (@available(iOS 14.0, *)) { - NSArray *menuItems = dict[@"menu"]; - if (menuItems.count > 0) { - NSMutableArray *actions = [NSMutableArray new]; - for (NSDictionary *item in menuItems) { - NSString *title = item[@"title"]; - if (![title isKindOfClass:[NSString class]]) continue; - UIAction *actionElement = [UIAction actionWithTitle:title - image:nil - identifier:nil - handler:^(__kindof UIAction * _Nonnull a) { - RNSBarButtonItemAction parentAction = objc_getAssociatedObject(self, &RNSBarButtonItemActionKey); - if (parentAction) { - parentAction(title); - } - }]; - [actions addObject:actionElement]; - } - NSMutableArray *children = [NSMutableArray new]; - - [children addObject:[UIMenu menuWithTitle:@"på" children:actions]]; - self.menu = [UIMenu menuWithTitle:@"hej" children:children]; + NSDictionary *menu = dict[@"menu"]; + if (menu) { + self.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction]; } } #endif @@ -167,6 +151,40 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict return self; } +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 ++(UIMenu*)initUIMenuWithDict:(NSDictionary *)dict menuAction:(RNSBarButtonMenuItemAction)menuAction +{ + if (@available(iOS 14.0, *)) { + NSArray* items = dict[@"items"]; + NSMutableArray *elements = [NSMutableArray new]; + if (items.count > 0) { + for (NSDictionary *item in items) { + NSString *menuId = item[@"menuId"]; + if (menuId) { + NSString *title = item[@"title"]; + NSString *systemImage = item[@"systemImage"]; + UIAction *actionElement = [UIAction actionWithTitle:title + image:systemImage ? [UIImage systemImageNamed:systemImage] : nil + identifier:nil + handler:^(__kindof UIAction * _Nonnull a) { + menuAction(menuId); + }]; + [elements addObject:actionElement]; + } else { + UIMenu *childMenu = [self initUIMenuWithDict:item menuAction:menuAction]; + if (childMenu) { + [elements addObject:childMenu]; + } + } + } + } + NSString *title = dict[@"title"]; + return [UIMenu menuWithTitle:title children:elements]; + } + return nil; +} +#endif + - (void)handleBarButtonItemPress:(UIBarButtonItem *)item { NSString *buttonId = objc_getAssociatedObject(self, &RNSBarButtonItemIdKey); diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 61d39ab418..fb884a0c1c 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) NSArray *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray *> *headerLeftBarButtonItems; @property (nonatomic) RCTDirectEventBlock onPressHeaderBarButtonItem; +@property (nonatomic) RCTDirectEventBlock onPressHeaderBarButtonMenuItem; NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 883a13bd44..457c6780ce 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -850,11 +850,18 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; if (dict[@"buttonId"] || dict[@"menu"]) { - RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { - if (self.onPressHeaderBarButtonItem && buttonId) { - self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); - } - }]; + RNSBarButtonItem *item = [[RNSBarButtonItem alloc] + initWithDictionary:dict + action:^(NSString *buttonId) { + if (self.onPressHeaderBarButtonItem && buttonId) { + self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); + } + } + menuAction:^(NSString *menuId) { + if (self.onPressHeaderBarButtonMenuItem && menuId) { + self.onPressHeaderBarButtonMenuItem(@{ @"menuId": menuId }); + } + }]; [items addObject:item]; } else if (dict[@"spacing"]) { UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; @@ -1209,6 +1216,7 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(headerLeftBarButtonItems, NSArray) RCT_EXPORT_VIEW_PROPERTY(headerRightBarButtonItems, NSArray) RCT_EXPORT_VIEW_PROPERTY(onPressHeaderBarButtonItem, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPressHeaderBarButtonMenuItem, RCTDirectEventBlock); @end From 5244d64429cb1cece0218f8b80bfe5c238b54661 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Thu, 26 Jun 2025 22:54:11 +0200 Subject: [PATCH 09/99] UIMenuElement state and attributes --- apps/src/screens/BarButtonItems.tsx | 6 ++++-- ios/RNSBarButtonItem.mm | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index 6be666b464..e50356ac63 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -31,7 +31,7 @@ export default function BarButtonItemsExample() { title: '+allt', onPress: () => Alert.alert('Plain pressed'), titleStyle: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '800', color: 'black' }, - style: 'Prominent', + style: 'prominent', tintColor: 'yellow', }, { @@ -43,7 +43,7 @@ export default function BarButtonItemsExample() { ], headerRightBarButtonItems: [ { - style: 'Prominent', + style: 'prominent', title: 'Menu', tintColor: 'purple', menu: { @@ -52,10 +52,12 @@ export default function BarButtonItemsExample() { title: 'Search', systemImage: 'magnifyingglass.circle.fill', onPress: () => Alert.alert('Search pressed'), + state: 'mixed', }, { title: 'Search with long text that wraps', onPress: () => Alert.alert('Search with long text pressed'), + attributes: 'keepsMenuPresented', }, { title: 'Submenu', diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 82487c203d..0d810c2b1f 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -110,9 +110,9 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict NSString *style = dict[@"style"]; if (style) { - if ([style isEqualToString:@"Done"]) { + if ([style isEqualToString:@"done"]) { self.style = UIBarButtonItemStyleDone; - } else if ([style isEqualToString:@"Prominent"]) { + } else if ([style isEqualToString:@"prominent"]) { #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_26_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_26_0 if (@available(iOS 26.0, *)) { @@ -169,6 +169,31 @@ +(UIMenu*)initUIMenuWithDict:(NSDictionary *)dict handler:^(__kindof UIAction * _Nonnull a) { menuAction(menuId); }]; + NSString *state = item[@"state"]; + if ([state isEqualToString:@"on"]) { + actionElement.state = UIMenuElementStateOn; + } else if ([state isEqualToString:@"off"]) { + actionElement.state = UIMenuElementStateOff; + } else if ([state isEqualToString:@"mixed"]) { + actionElement.state = UIMenuElementStateMixed; + } + + NSString *attributes = item[@"attributes"]; + if ([attributes isEqualToString:@"destructive"]) { + actionElement.attributes = UIMenuElementAttributesDestructive; + } else if ([attributes isEqualToString:@"disabled"]) { + actionElement.attributes = UIMenuElementAttributesDisabled; + } else if ([attributes isEqualToString:@"hidden"]) { + actionElement.attributes = UIMenuElementAttributesHidden; + } + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_16_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0 + else if (@available(iOS 16.0, *)) { + if ([attributes isEqualToString:@"keepsMenuPresented"]) { + actionElement.attributes = UIMenuElementAttributesKeepsMenuPresented; + } + } + #endif [elements addObject:actionElement]; } else { UIMenu *childMenu = [self initUIMenuWithDict:item menuAction:menuAction]; From 49fa2bee0a73ad67d37ee8e80de9dc35ba6ea208 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Thu, 26 Jun 2025 23:12:34 +0200 Subject: [PATCH 10/99] badge --- apps/src/screens/BarButtonItems.tsx | 6 ++++++ ios/RNSBarButtonItem.mm | 30 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index e50356ac63..dc70322a2e 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -33,6 +33,12 @@ export default function BarButtonItemsExample() { titleStyle: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '800', color: 'black' }, style: 'prominent', tintColor: 'yellow', + badge: { + value: '1', + color: 'white', + backgroundColor: 'red', + style: { fontFamily: 'Georgia', fontSize: 16, fontWeight: '100' }, + }, }, { image: require('../assets/search_black.png'), diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 0d810c2b1f..8011a4b160 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -104,6 +104,36 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict if (identifier != nil) { self.identifier = identifier; } + NSDictionary *badgeObj = dict[@"badge"]; + if (badgeObj != nil) { + UIBarButtonItemBadge *badge = [UIBarButtonItemBadge badgeWithString:badgeObj[@"value"]]; + id colorObj = badgeObj[@"color"]; + if (colorObj) { + badge.foregroundColor = [RCTConvert UIColor:colorObj]; + } + id backgroundColorObj = badgeObj[@"backgroundColor"]; + if (colorObj) { + badge.backgroundColor = [RCTConvert UIColor:backgroundColorObj]; + } + NSDictionary *style = badgeObj[@"style"]; + if (style) { + NSString *fontFamily = style[@"fontFamily"]; + NSNumber *fontSize = style[@"fontSize"]; + NSString *fontWeight = style[@"fontWeight"]; + if (fontSize || fontWeight) { + badge.font = [RCTFont updateFont:nil + withFamily:fontFamily + size:fontSize + weight:fontWeight + style:nil + variant:nil + scaleMultiplier:1.0]; + } else { + badge.font = [UIFont systemFontOfSize:[fontSize floatValue]]; + } + } + self.badge = badge; + } } #endif From f2a75f9f0c745c9661beb9e53a2e48c6447b6d3c Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Fri, 27 Jun 2025 12:15:50 +0200 Subject: [PATCH 11/99] new arch runs --- ios/RNSConvert.h | 1 + ios/RNSConvert.mm | 27 ++++++++ ios/RNSScreenStackHeaderConfig.mm | 69 ++++++++++++++++--- .../ScreenStackHeaderConfigNativeComponent.ts | 9 +++ 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/ios/RNSConvert.h b/ios/RNSConvert.h index 6be86d6f1f..d76be1b71f 100644 --- a/ios/RNSConvert.h +++ b/ios/RNSConvert.h @@ -61,6 +61,7 @@ namespace react = facebook::react; (react::RNSScreenRightScrollEdgeEffect)edgeEffect; + (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenTopScrollEdgeEffectCppEquivalent: (react::RNSScreenTopScrollEdgeEffect)edgeEffect; ++ (id)idFromFollyDynamic:(const folly::dynamic &)dyn; #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNSConvert.mm b/ios/RNSConvert.mm index 92b431ddff..e3ca73a935 100644 --- a/ios/RNSConvert.mm +++ b/ios/RNSConvert.mm @@ -327,6 +327,33 @@ + (RNSBlurEffectStyle)RNSBlurEffectStyleFromCppEquivalent:(react::RNSScreenStack } } ++ (id)idFromFollyDynamic:(const folly::dynamic &)dyn { + if (dyn.isNull()) { + return nil; + } else if (dyn.isBool()) { + return [NSNumber numberWithBool:dyn.getBool()]; + } else if (dyn.isInt()) { + return [NSNumber numberWithLongLong:dyn.getInt()]; + } else if (dyn.isDouble()) { + return [NSNumber numberWithDouble:dyn.getDouble()]; + } else if (dyn.isString()) { + return [NSString stringWithUTF8String:dyn.getString().c_str()]; + } else if (dyn.isArray()) { + NSMutableArray *array = [NSMutableArray arrayWithCapacity:dyn.size()]; + for (const auto &item : dyn) { + [array addObject:[self idFromFollyDynamic:item]]; + } + return array; + } else if (dyn.isObject()) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:dyn.size()]; + for (const auto &pair : dyn.items()) { + dict[@(pair.first.c_str())] = [self idFromFollyDynamic:pair.second]; + } + return dict; + } + return nil; +} + #endif // RCT_NEW_ARCH_ENABLED + (UIBlurEffectStyle)tryConvertRNSBlurEffectStyleToUIBlurEffectStyle:(RNSBlurEffectStyle)blurEffect diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 457c6780ce..71fde501b3 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -4,6 +4,7 @@ #import #import #import +#import #import #import #import @@ -853,15 +854,35 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithDictionary:dict action:^(NSString *buttonId) { - if (self.onPressHeaderBarButtonItem && buttonId) { - self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); - } - } +#ifdef RCT_NEW_ARCH_ENABLED + auto eventEmitter = std::static_pointer_cast(self->_eventEmitter); + if (eventEmitter && buttonId) { + eventEmitter->onPressHeaderBarButtonItem + (facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem { + .buttonId = std::string([buttonId UTF8String]) + }); + } +#else + if (self.onPressHeaderBarButtonItem && buttonId) { + self.onPressHeaderBarButtonItem(@{ @"buttonId": buttonId }); + } +#endif + } menuAction:^(NSString *menuId) { - if (self.onPressHeaderBarButtonMenuItem && menuId) { - self.onPressHeaderBarButtonMenuItem(@{ @"menuId": menuId }); - } - }]; +#ifdef RCT_NEW_ARCH_ENABLED + auto eventEmitter = std::static_pointer_cast(self->_eventEmitter); + if (eventEmitter && menuId) { + eventEmitter->onPressHeaderBarButtonMenuItem + (facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem { + .menuId = std::string([menuId UTF8String]) + }); + } +#else + if (self.onPressHeaderBarButtonMenuItem && menuId) { + self.onPressHeaderBarButtonMenuItem(@{ @"menuId": menuId }); + } +#endif + }]; [items addObject:item]; } else if (dict[@"spacing"]) { UIBarButtonItem *fixedSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil]; @@ -977,6 +998,11 @@ - (void)replaceNavigationBarViewsWithSnapshotOfSubview:(RNSScreenStackHeaderSubv } } +- (void)onPressHeaderBarButtonItemHandler:(NSString *)buttonId +{ + +} + static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) { switch (resizeMode) { @@ -1103,7 +1129,32 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: if (newScreenProps.blurEffect != oldScreenProps.blurEffect) { _blurEffect = [RNSConvert RNSBlurEffectStyleFromCppEquivalent:newScreenProps.blurEffect]; } - + + if (newScreenProps.headerLeftBarButtonItems != oldScreenProps.headerLeftBarButtonItems) { + const auto &vec = newScreenProps.headerLeftBarButtonItems; + NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; + for (const auto &item : vec) { + NSDictionary *dict = [RNSConvert idFromFollyDynamic:item]; + if ([dict isKindOfClass:[NSDictionary class]]) { + [array addObject:dict]; + } + } + _headerLeftBarButtonItems = array; + } + + + if (newScreenProps.headerRightBarButtonItems != oldScreenProps.headerRightBarButtonItems) { + const auto &vec = newScreenProps.headerRightBarButtonItems; + NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; + for (const auto &item : vec) { + NSDictionary *dict = [RNSConvert idFromFollyDynamic:item]; + if ([dict isKindOfClass:[NSDictionary class]]) { + [array addObject:dict]; + } + } + _headerRightBarButtonItems = array; + } + [self updateViewControllerIfNeeded]; if (needsNavigationControllerLayout) { diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts index 453c0bbde0..a8901d479b 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts @@ -7,6 +7,7 @@ import type { Int32, WithDefault, DirectEventHandler, + UnsafeMixed, } from 'react-native/Libraries/Types/CodegenTypes'; type DirectionType = 'rtl' | 'ltr'; @@ -16,6 +17,10 @@ type OnAttachedEvent = Readonly<{}>; // eslint-disable-next-line @typescript-eslint/ban-types type OnDetachedEvent = Readonly<{}>; +type OnPressHeaderBarButtonItemEvent = Readonly<{buttonId: string}>; +type OnPressHeaderBarButtonMenuItemEvent = Readonly<{menuId: string}>; + + type BackButtonDisplayMode = 'minimal' | 'default' | 'generic'; type BlurEffect = @@ -73,6 +78,10 @@ export interface NativeProps extends ViewProps { blurEffect?: WithDefault; // TODO: implement this props on iOS topInsetEnabled?: boolean; + headerLeftBarButtonItems?: UnsafeMixed[]; + headerRightBarButtonItems?: UnsafeMixed[]; + onPressHeaderBarButtonItem?: DirectEventHandler; + onPressHeaderBarButtonMenuItem?: DirectEventHandler; } export default codegenNativeComponent( From de5d0255d7d906f289f29ebefecdebaeea738b55 Mon Sep 17 00:00:00 2001 From: Johan Kasperi Date: Fri, 27 Jun 2025 16:10:24 +0200 Subject: [PATCH 12/99] proper examples of each feature --- apps/src/screens/BarButtonItems.tsx | 375 +++++++++++++++++++++++----- 1 file changed, 315 insertions(+), 60 deletions(-) diff --git a/apps/src/screens/BarButtonItems.tsx b/apps/src/screens/BarButtonItems.tsx index dc70322a2e..82d64e3fc6 100644 --- a/apps/src/screens/BarButtonItems.tsx +++ b/apps/src/screens/BarButtonItems.tsx @@ -1,91 +1,346 @@ // NOTE: The full native feature set (style, image, menu, etc.) is available, but the TS types in src/types.tsx need to be updated to match. This example uses only the currently typed props (title, icon, onPress, enabled). import React from 'react'; -import { View, Alert } from 'react-native'; -import { - createNativeStackNavigator, -} from '@react-navigation/native-stack'; +import { View, Alert, Button, Text } from 'react-native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { ScrollView } from 'react-native-gesture-handler'; -const Screen = () => { - return ( - - - - - - ); -}; - const Stack = createNativeStackNavigator(); +const demoScreens = [ + { name: 'PlainButtonDemo', title: 'Plain Button' }, + { name: 'IconButtonDemo', title: 'Icon Button' }, + { name: 'MenuButtonDemo', title: 'Menu Button' }, + { name: 'BadgeButtonDemo', title: 'Badge Button' }, + { name: 'DisabledButtonDemo', title: 'Disabled Button' }, + { name: 'CustomColorButtonDemo', title: 'Custom Color Button' }, + { name: 'ProminentStyleButtonDemo', title: 'Prominent Style Button' }, + { name: 'TitleStyleButtonDemo', title: 'Title Style Button' }, + { name: 'IconSharesBgButtonDemo', title: 'Icon SharesBackground' }, + { name: 'TextButtonWithWidthDemo', title: 'Text Button With Width' }, + { name: 'IconButtonsWithSpacingDemo', title: 'Icon Buttons With Spacing' }, + { name: 'HeaderTintColorDemo', title: 'Header Tint Color' }, + { name: 'DoneStyleButtonDemo', title: 'Done Style Button' }, + { name: 'AdvancedMenuButtonDemo', title: 'Advanced Menu Button' }, +]; + +const MainScreen = ({ navigation }: any) => ( + + + iOS only + + {demoScreens.map((screen) => ( +