Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function TestStackHeaderMenuIOS() {
function buildHeaderConfig(
trailingItemsCount: number,
showToast: (text: string) => void,
keepsMenuPresented: boolean,
): StackHeaderConfigProps {
const trailingItems: NonNullable<
StackHeaderConfigProps['ios']
Expand All @@ -58,25 +59,29 @@ function buildHeaderConfig(
type: 'menuItem',
itemType: 'action',
title: `Action ${i}-1`,
keepsMenuPresented,
onPress: () => showToast(`Clicked Action ${i}-1`),
},
{
id: `toggle-${i}-1`,
type: 'menuItem',
itemType: 'toggle',
title: `Toggle ${i}-1`,
keepsMenuPresented,
},
{
id: `toggle-${i}-2`,
type: 'menuItem',
itemType: 'toggle',
title: `Toggle ${i}-2`,
keepsMenuPresented,
},
{
id: `toggle-${i}-3`,
type: 'menuItem',
itemType: 'toggle',
title: `Toggle ${i}-3`,
keepsMenuPresented,
},
{
id: `submenu-${i}`,
Expand Down Expand Up @@ -130,6 +135,7 @@ function ConfigScreen() {
const [trailingItemsCount, setTrailingItemsCount] = useState<number>(
DEFAULT_TRAILING_ITEMS_COUNT,
);
const [keepsMenuPresented, setKeepsMenuPresented] = useState(false);

const showToast = useCallback(
(text: string) => {
Expand All @@ -140,8 +146,8 @@ function ConfigScreen() {

const { setRouteOptions, routeKey } = navigation;
const headerConfig = useMemo(
() => buildHeaderConfig(trailingItemsCount, showToast),
[trailingItemsCount, showToast],
() => buildHeaderConfig(trailingItemsCount, showToast, keepsMenuPresented),
[trailingItemsCount, showToast, keepsMenuPresented],
);

useLayoutEffect(() => {
Expand All @@ -156,6 +162,10 @@ function ConfigScreen() {
title={`Toggle trailing items count (${trailingItemsCount}/4)`}
onPress={() => setTrailingItemsCount(count => (count + 1) % 5)}
/>
<Button
title={`keepsMenuPresented: ${keepsMenuPresented}`}
onPress={() => setKeepsMenuPresented(prev => !prev)}
/>
<LongText />
</ScrollView>
);
Expand Down
30 changes: 24 additions & 6 deletions ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#import "RNSStackHeaderMenuCoordinator.h"
#import "RNSDefines.h"

#import <React/RCTAssert.h>

Expand Down Expand Up @@ -169,6 +170,8 @@ + (nullable UIMenuElement *)buildToggleFromData:(RNSStackHeaderMenuItemData *)da
}];
toggleAction.state = isItemToggledOn ? UIMenuElementStateOn : UIMenuElementStateOff;

[self decorateActionKeepsMenuPresented:toggleAction withData:data];

return toggleAction;
}

Expand All @@ -177,12 +180,16 @@ + (nullable UIMenuElement *)buildActionFromData:(RNSStackHeaderMenuItemData *)da
{
__weak id<RNSStackHeaderEventsDelegate> weakDelegate = delegate;

return [UIAction actionWithTitle:data.title
image:nil
identifier:nil
handler:^(__kindof UIAction *_Nonnull action) {
[weakDelegate didPressMenuItem:data.menuElementId];
}];
UIAction *action = [UIAction actionWithTitle:data.title
image:nil
identifier:nil
handler:^(__kindof UIAction *_Nonnull action) {
[weakDelegate didPressMenuItem:data.menuElementId];
}];

[self decorateActionKeepsMenuPresented:action withData:data];

return action;
}

#pragma mark - Helpers
Expand Down Expand Up @@ -229,4 +236,15 @@ + (void)collectToggleItemsIdsFromMenu:(RNSStackHeaderMenuData *)menu
}
}

+ (void)decorateActionKeepsMenuPresented:(UIAction *)action withData:(RNSStackHeaderMenuItemData *)data
{
if (data.keepsMenuPresented) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0) || (TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= 160000)
if (@available(iOS 16.0, tvOS 16.0, *)) {
action.attributes |= UIMenuElementAttributesKeepsMenuPresented;
}
#endif
}
}

@end
4 changes: 3 additions & 1 deletion ios/gamma/stack/header/RNSStackHeaderMenuData.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ typedef NS_ENUM(NSInteger, RNSMenuItemType) {
@property (nonatomic, copy, readonly, nullable) NSString *title;
@property (nonatomic, readonly) RNSMenuItemType itemType;
@property (nonatomic, readonly) BOOL initialToggleState;
@property (nonatomic, readonly) BOOL keepsMenuPresented;

- (instancetype)initWithId:(NSString *)menuElementId
title:(nullable NSString *)title
itemType:(RNSMenuItemType)itemType
initialToggleState:(BOOL)initialToggleState;
initialToggleState:(BOOL)initialToggleState
keepsMenuPresented:(BOOL)keepsMenuPresented;

@end

Expand Down
2 changes: 2 additions & 0 deletions ios/gamma/stack/header/RNSStackHeaderMenuData.mm
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ - (instancetype)initWithId:(NSString *)menuElementId
title:(nullable NSString *)title
itemType:(RNSMenuItemType)itemType
initialToggleState:(BOOL)initialToggleState
keepsMenuPresented:(BOOL)keepsMenuPresented
{
if (self = [super init]) {
_menuElementId = [menuElementId copy];
_title = [title copy];
_itemType = itemType;
_initialToggleState = initialToggleState;
_keepsMenuPresented = keepsMenuPresented;
}
return self;
}
Expand Down
18 changes: 9 additions & 9 deletions ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
#import <React/RCTAssert.h>
#import <React/RCTLog.h>

static NSSet<NSString *> *const kRNSAllowedMenuKeys =
[NSSet setWithObjects:@"id", @"type", @"title", @"children", @"singleSelection", nil];
static NSSet<NSString *> *const kRNSAllowedMenuItemKeys =
[NSSet setWithObjects:@"id", @"type", @"title", @"itemType", @"initialToggleState", @"keepsMenuPresented", nil];

@implementation RNSStackHeaderMenuMapper

+ (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
Expand Down Expand Up @@ -54,7 +59,8 @@ + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
title:[self stringForKey:@"title" in:dict]
itemType:[self itemTypeFromString:[self stringForKey:@"itemType"
in:dict]]
initialToggleState:[self boolForKey:@"initialToggleState" in:dict]];
initialToggleState:[self boolForKey:@"initialToggleState" in:dict]
keepsMenuPresented:[self boolForKey:@"keepsMenuPresented" in:dict]];
}

return nil;
Expand All @@ -65,10 +71,7 @@ + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
+ (void)validateMenuKeys:(NSDictionary *)dict
{
for (NSString *key in dict) {
RCTAssert([key isEqualToString:@"id"] || [key isEqualToString:@"type"] || [key isEqualToString:@"title"] ||
[key isEqualToString:@"children"] || [key isEqualToString:@"singleSelection"],
@"[RNScreens] Invalid key \"%@\" found in menu",
key);
RCTAssert([kRNSAllowedMenuKeys containsObject:key], @"[RNScreens] Invalid key \"%@\" found in menu", key);
}
RCTAssert(dict[@"children"], @"[RNScreens] missing key \"children\" in menu");
RCTAssert(dict[@"id"], @"[RNScreens] missing id on one of menu elements");
Expand All @@ -77,10 +80,7 @@ + (void)validateMenuKeys:(NSDictionary *)dict
+ (void)validateMenuItemKeys:(NSDictionary *)dict
{
for (NSString *key in dict) {
RCTAssert([key isEqualToString:@"id"] || [key isEqualToString:@"type"] || [key isEqualToString:@"title"] ||
[key isEqualToString:@"itemType"] || [key isEqualToString:@"initialToggleState"],
@"[RNScreens] Invalid key \"%@\" found in menu item",
key);
RCTAssert([kRNSAllowedMenuItemKeys containsObject:key], @"[RNScreens] Invalid key \"%@\" found in menu item", key);
}
RCTAssert(dict[@"id"], @"[RNScreens] missing id on one of menu elements");
}
Expand Down
15 changes: 15 additions & 0 deletions src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ export interface StackHeaderMenuItemIOS {
* @platform ios
*/
onPress?: () => void | undefined;
/**
* @summary Keeps the menu presented after this item is tapped.
*
* @description
* When enabled, selecting this item will not dismiss the menu,
* allowing the user to continue interacting with other items.
*
* @remarks
* This prop should only be used for items in top-level menus. Requires iOS 16.0 or later.
*
* @default false
*
* @platform ios
*/
keepsMenuPresented?: boolean | undefined;
Comment thread
kmichalikk marked this conversation as resolved.
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type StackHeaderMenuItemIOS = {
id: string;
type: 'menuItem';
title?: string | undefined;
keepsMenuPresented?: boolean | undefined;
};

export type StackHeaderMenuIOS = {
Expand Down
Loading