diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/index.tsx b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/index.tsx
index e0f53f1abb..6b01e4b199 100644
--- a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/index.tsx
+++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createScenario } from '@apps/tests/shared/helpers';
import {
StackContainer,
@@ -9,24 +9,31 @@ import { Button, ScrollView } from 'react-native';
import LongText from '@apps/shared/LongText';
import { scenarioDescription } from './scenario-description';
import PressableWithFeedback from '@apps/shared/PressableWithFeedback';
+import { ToastProvider, useToast } from '@apps/shared';
+import { Colors } from '@apps/shared/styling';
const DEFAULT_TRAILING_ITEMS_COUNT = 2;
export function App() {
return (
-
+
+
+
);
}
-function buildHeaderConfig(trailingItemsCount: number): StackHeaderConfigProps {
+function buildHeaderConfig(
+ trailingItemsCount: number,
+ showToast: (text: string) => void,
+): StackHeaderConfigProps {
const trailingItems: NonNullable<
StackHeaderConfigProps['ios']
>['trailingItems'] = Array.from({ length: trailingItemsCount }).map(
@@ -42,15 +49,37 @@ function buildHeaderConfig(trailingItemsCount: number): StackHeaderConfigProps {
}),
menu: {
type: 'menu',
+ menuElementId: `menu-${i}`,
children: [
- { type: 'menuItem', title: `Item ${i}.1` },
- { type: 'menuItem', title: `Item ${i}.2` },
{
+ menuElementId: `subitem-${i}-1`,
+ type: 'menuItem',
+ title: `Item ${i}.1`,
+ onPress: () => showToast(`Clicked Item ${i}.1`),
+ },
+ {
+ menuElementId: `subitem-${i}-2`,
+ type: 'menuItem',
+ title: `Item ${i}.2`,
+ onPress: () => showToast(`Clicked Item ${i}.2`),
+ },
+ {
+ menuElementId: `submenu-${i}`,
type: 'menu',
title: `Submenu ${i}`,
children: [
- { type: 'menuItem', title: `Nested ${i}.1` },
- { type: 'menuItem', title: `Nested ${i}.2` },
+ {
+ menuElementId: `subsubitem-${i}-1`,
+ type: 'menuItem',
+ title: `Nested ${i}.1`,
+ onPress: () => showToast(`Clicked Nested ${i}.1`),
+ },
+ {
+ menuElementId: `subsubitem-${i}-2`,
+ type: 'menuItem',
+ title: `Nested ${i}.2`,
+ onPress: () => showToast(`Clicked Nested ${i}.2`),
+ },
],
},
],
@@ -68,14 +97,22 @@ function buildHeaderConfig(trailingItemsCount: number): StackHeaderConfigProps {
function ConfigScreen() {
const navigation = useStackNavigationContext();
+ const toast = useToast();
const [trailingItemsCount, setTrailingItemsCount] = useState(
DEFAULT_TRAILING_ITEMS_COUNT,
);
+ const showToast = useCallback(
+ (text: string) => {
+ toast.push({ backgroundColor: Colors.GreenDark120, message: text });
+ },
+ [toast],
+ );
+
const { setRouteOptions, routeKey } = navigation;
const headerConfig = useMemo(
- () => buildHeaderConfig(trailingItemsCount),
- [trailingItemsCount],
+ () => buildHeaderConfig(trailingItemsCount, showToast),
+ [trailingItemsCount, showToast],
);
useEffect(() => {
diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/scenario.md b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/scenario.md
index 873814bfbe..0c45b7f5bb 100644
--- a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/scenario.md
+++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-menu-ios/scenario.md
@@ -24,5 +24,18 @@ TBD
2. Reload the application (dev console causes some layout-related callbacks to trigger which may hide regressions)
3. Click on the Menu 1 item
- [ ] The bubble morphs into a menu with two items and a submenu
+ - [ ] Clicking on Item 1.1 triggers a Toast "Clicked Item 1.1"
+ - [ ] Clicking on Item 1.2 triggers a Toast "Clicked Item 1.2"
4. While the menu is opened, click on the Submenu 1
- [ ] A nested menu appears, containing two items
+ - [ ] Clicking on Nested 1.1 triggers a Toast "Clicked Nested 1.1"
+ - [ ] Clicking on Nested 1.2 triggers a Toast "Clicked Nested 1.2"
+5. Click on "Toggle trailing items count" 2 times to get 4 items
+6. Click on the Menu 3 item
+ - [ ] The bubble morphs into a menu with two items and a submenu
+ - [ ] Clicking on Item 3.1 triggers a Toast "Clicked Item 3.1"
+ - [ ] Clicking on Item 3.2 triggers a Toast "Clicked Item 3.2"
+7. While the menu is opened, click on the Submenu 3
+ - [ ] A nested menu appears, containing two items
+ - [ ] Clicking on Nested 3.1 triggers a Toast "Clicked Nested 3.1"
+ - [ ] Clicking on Nested 3.2 triggers a Toast "Clicked Nested 3.2"
diff --git a/ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm b/ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
index 2cf4d9355d..ecda54428a 100644
--- a/ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
+++ b/ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
@@ -1,11 +1,13 @@
#import "RNSStackHeaderConfigComponentView.h"
#import "RNSLog.h"
+#import "RNSStackHeaderConfigEventEmitter.h"
#import "RNSStackHeaderConfigShadowStateProxy.h"
#import "RNSStackHeaderContentFactory.h"
#import "RNSStackHeaderData.h"
#import "RNSStackHeaderItemComponentView.h"
#import "RNSStackHeaderItemInvalidationDelegate.h"
#import "RNSStackHeaderItemSpacerComponentView.h"
+#import "RNSStackHeaderMenuEventsDelegate.h"
#import "RNSStackNavigationController.h"
#import "RNSStackScreenComponentView.h"
#import "RNSStackScreenController.h"
@@ -28,7 +30,8 @@ static void RNSAssertIsValidHeaderChild(UIView *child)
RNSStackHeaderItemSpacerComponentView.class);
}
-@interface RNSStackHeaderConfigComponentView ()
+@interface RNSStackHeaderConfigComponentView ()
@end
@implementation RNSStackHeaderConfigComponentView {
@@ -43,6 +46,7 @@ @implementation RNSStackHeaderConfigComponentView {
std::shared_ptr _state;
RNSStackHeaderConfigShadowStateProxy *_Nonnull _shadowStateProxy;
+ RNSStackHeaderConfigEventEmitter *_Nonnull _reactEventEmitter;
}
- (instancetype)initWithFrame:(CGRect)frame
@@ -52,6 +56,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_props = defaultProps;
_children = [NSMutableArray new];
_shadowStateProxy = [[RNSStackHeaderConfigShadowStateProxy alloc] initWithHeaderConfigView:self];
+ _reactEventEmitter = [RNSStackHeaderConfigEventEmitter new];
[self resetProps];
}
return self;
@@ -111,13 +116,20 @@ - (void)unmountChildComponentView:(UIView *)childCompo
[self submitCurrentDataIfMounted];
}
-#pragma mark RNSStackHeaderItemInvalidationDelegate
+#pragma mark - RNSStackHeaderItemInvalidationDelegate
- (void)headerItemDidInvalidate
{
[self submitCurrentDataIfMounted];
}
+#pragma mark - RNSStackHeaderMenuEventsDelegate
+
+- (void)didPressMenuElement:(NSString *)menuElementId
+{
+ [_reactEventEmitter emitOnPressMenuItem:menuElementId];
+}
+
#pragma mark - RNSViewFrameChangeDelegate
- (void)viewFrameDidChange:(nonnull UINavigationBar *)navigationBar
@@ -193,6 +205,13 @@ + (BOOL)shouldBeRecycled
return NO;
}
+- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
+{
+ [super updateEventEmitter:eventEmitter];
+ [_reactEventEmitter
+ updateEventEmitter:std::static_pointer_cast(eventEmitter)];
+}
+
#pragma mark - Private
- (void)submitCurrentDataIfMounted
@@ -244,11 +263,13 @@ - (void)buildBarButtonItemsWithLeadingItems:(NSMutableArray *
switch (item.placement) {
case RNSHeaderItemPlacementLeading:
[leadingItems addObject:[RNSStackHeaderContentFactory barButtonItemForHeaderItem:item
- withFrameChangeDelegate:self]];
+ withFrameChangeDelegate:self
+ withMenuEventsDelegate:self]];
break;
case RNSHeaderItemPlacementTrailing:
[trailingItems addObject:[RNSStackHeaderContentFactory barButtonItemForHeaderItem:item
- withFrameChangeDelegate:self]];
+ withFrameChangeDelegate:self
+ withMenuEventsDelegate:self]];
break;
case RNSHeaderItemPlacementTitle:
if (item.customView != nil) {
diff --git a/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.h b/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.h
new file mode 100644
index 0000000000..391417621a
--- /dev/null
+++ b/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#import
+
+// Hide C++ symbols from C compiler used when building Swift module
+#if defined(__cplusplus)
+#import
+
+namespace react = facebook::react;
+#endif // __cplusplus
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface RNSStackHeaderConfigEventEmitter : NSObject
+
+- (BOOL)emitOnPressMenuItem:(NSString *)menuElementId;
+
+@end
+
+#pragma mark - Hidden from Swift
+
+#if defined(__cplusplus)
+
+@interface RNSStackHeaderConfigEventEmitter ()
+
+- (void)updateEventEmitter:(const std::shared_ptr &)emitter;
+
+@end
+
+#endif // __cplusplus
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.mm b/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.mm
new file mode 100644
index 0000000000..e081572353
--- /dev/null
+++ b/ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.mm
@@ -0,0 +1,24 @@
+#import "RNSStackHeaderConfigEventEmitter.h"
+#import
+
+@implementation RNSStackHeaderConfigEventEmitter {
+ std::shared_ptr _reactEventEmitter;
+}
+
+- (BOOL)emitOnPressMenuItem:(NSString *)menuElementId
+{
+ if (_reactEventEmitter != nullptr) {
+ _reactEventEmitter->onPressMenuItem({.menuElementId = menuElementId.cString});
+ return YES;
+ } else {
+ RCTLogWarn(@"[RNScreens] Skipped OnPressMenuItem event emission due to nullish emitter");
+ return NO;
+ }
+}
+
+- (void)updateEventEmitter:(const std::shared_ptr &)emitter
+{
+ _reactEventEmitter = emitter;
+}
+
+@end
diff --git a/ios/gamma/stack/header/RNSStackHeaderContentFactory.h b/ios/gamma/stack/header/RNSStackHeaderContentFactory.h
index fd31b68545..3dc24be664 100644
--- a/ios/gamma/stack/header/RNSStackHeaderContentFactory.h
+++ b/ios/gamma/stack/header/RNSStackHeaderContentFactory.h
@@ -4,6 +4,7 @@
#import "RNSStackHeaderItemDataProviding.h"
#import "RNSStackHeaderItemSpacerDataProviding.h"
+#import "RNSStackHeaderMenuEventsDelegate.h"
#import "RNSViewFrameChangeDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@@ -11,7 +12,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface RNSStackHeaderContentFactory : NSObject
+ (UIBarButtonItem *)barButtonItemForHeaderItem:(id)item
- withFrameChangeDelegate:(id)delegate;
+ withFrameChangeDelegate:(id)delegate
+ withMenuEventsDelegate:(id)menuEventsDelegate;
+ (UIView *)wrappedViewForHeaderItem:(id)item
frameChangeDelegate:(id)delegate;
diff --git a/ios/gamma/stack/header/RNSStackHeaderContentFactory.mm b/ios/gamma/stack/header/RNSStackHeaderContentFactory.mm
index 899c2cf56f..9435411a5f 100644
--- a/ios/gamma/stack/header/RNSStackHeaderContentFactory.mm
+++ b/ios/gamma/stack/header/RNSStackHeaderContentFactory.mm
@@ -7,11 +7,14 @@ @implementation RNSStackHeaderContentFactory
+ (UIBarButtonItem *)barButtonItemForHeaderItem:(id)item
withFrameChangeDelegate:(id)delegate
+ withMenuEventsDelegate:(id)menuEventsDelegate
{
UIBarButtonItem *barButtonItem = [RNSStackHeaderContentFactory internalBarButtonItemForHeaderItem:item
withFrameChangeDelegate:delegate];
if (item.menu != nil) {
- [RNSStackHeaderMenuCoordinator applyMenu:item.menu toBarButtonItem:barButtonItem];
+ [RNSStackHeaderMenuCoordinator applyMenu:item.menu
+ toBarButtonItem:barButtonItem
+ withMenuEventsDelegate:menuEventsDelegate];
}
return barButtonItem;
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h b/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h
index 17e62d2629..6a66adfcbf 100644
--- a/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h
@@ -2,12 +2,15 @@
#import
#import "RNSStackHeaderMenuData.h"
+#import "RNSStackHeaderMenuEventsDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface RNSStackHeaderMenuCoordinator : NSObject
-+ (void)applyMenu:(nonnull RNSStackHeaderMenuData *)data toBarButtonItem:(nonnull UIBarButtonItem *)item;
++ (void)applyMenu:(RNSStackHeaderMenuData *)data
+ toBarButtonItem:(UIBarButtonItem *)item
+ withMenuEventsDelegate:(id)delegate;
@end
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm b/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
index 840d8fd4a1..bdff6f4bef 100644
--- a/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
@@ -2,20 +2,23 @@
@implementation RNSStackHeaderMenuCoordinator
-+ (void)applyMenu:(nonnull RNSStackHeaderMenuData *)data toBarButtonItem:(nonnull UIBarButtonItem *)item
++ (void)applyMenu:(RNSStackHeaderMenuData *)data
+ toBarButtonItem:(UIBarButtonItem *)item
+ withMenuEventsDelegate:(id)delegate
{
#if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000
if (@available(tvOS 17.0, *)) {
- item.menu = [self buildMenuFromData:data];
+ item.menu = [self buildMenuFromData:data withMenuEventsDelegate:delegate];
}
#endif // !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000
}
+ (UIMenu *)buildMenuFromData:(RNSStackHeaderMenuData *)data
+ withMenuEventsDelegate:(id)delegate
{
NSMutableArray *elements = [NSMutableArray arrayWithCapacity:data.children.count];
for (id child in data.children) {
- UIMenuElement *element = [self buildElementFromData:child];
+ UIMenuElement *element = [self buildElementFromData:child withMenuEventsDelegate:delegate];
if (element != nil) {
[elements addObject:element];
}
@@ -25,18 +28,20 @@ + (UIMenu *)buildMenuFromData:(RNSStackHeaderMenuData *)data
}
+ (nullable UIMenuElement *)buildElementFromData:(id)element
+ withMenuEventsDelegate:(id)delegate
{
if ([element isKindOfClass:[RNSStackHeaderMenuData class]]) {
- return [self buildMenuFromData:(RNSStackHeaderMenuData *)element];
+ return [self buildMenuFromData:(RNSStackHeaderMenuData *)element withMenuEventsDelegate:delegate];
}
if ([element isKindOfClass:[RNSStackHeaderMenuItemData class]]) {
RNSStackHeaderMenuItemData *itemData = (RNSStackHeaderMenuItemData *)element;
+ __weak id weakDelegate = delegate;
return [UIAction actionWithTitle:itemData.title ?: @""
image:nil
identifier:nil
- handler:^(__kindof UIAction *_Nonnull action){
- // noop
+ handler:^(__kindof UIAction *_Nonnull action) {
+ [weakDelegate didPressMenuElement:itemData.menuElementId];
}];
}
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuData.h b/ios/gamma/stack/header/RNSStackHeaderMenuData.h
index 61f08ef7b9..d0aa434385 100644
--- a/ios/gamma/stack/header/RNSStackHeaderMenuData.h
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuData.h
@@ -5,13 +5,16 @@
NS_ASSUME_NONNULL_BEGIN
@protocol RNSStackHeaderMenuElement
+
+@property (nonatomic, copy, readonly) NSString *menuElementId;
+
@end
@interface RNSStackHeaderMenuItemData : NSObject
@property (nonatomic, copy, readonly, nullable) NSString *title;
-- (instancetype)initWithTitle:(nullable NSString *)title;
+- (instancetype)initWithId:(NSString *)menuElementId title:(nullable NSString *)title;
@end
@@ -20,7 +23,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, readonly, nullable) NSString *title;
@property (nonatomic, copy, readonly) NSArray> *children;
-- (instancetype)initWithTitle:(nullable NSString *)title children:(NSArray> *)children;
+- (instancetype)initWithId:(NSString *)menuElementId
+ title:(nullable NSString *)title
+ children:(NSArray> *)children;
@end
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuData.mm b/ios/gamma/stack/header/RNSStackHeaderMenuData.mm
index bbe2facd3a..33b54f385c 100644
--- a/ios/gamma/stack/header/RNSStackHeaderMenuData.mm
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuData.mm
@@ -2,9 +2,12 @@
@implementation RNSStackHeaderMenuItemData
-- (instancetype)initWithTitle:(nullable NSString *)title
+@synthesize menuElementId = _menuElementId;
+
+- (instancetype)initWithId:(NSString *)menuElementId title:(nullable NSString *)title
{
if (self = [super init]) {
+ _menuElementId = [menuElementId copy];
_title = [title copy];
}
return self;
@@ -14,9 +17,14 @@ - (instancetype)initWithTitle:(nullable NSString *)title
@implementation RNSStackHeaderMenuData
-- (instancetype)initWithTitle:(nullable NSString *)title children:(NSArray> *)children
+@synthesize menuElementId = _menuElementId;
+
+- (instancetype)initWithId:(NSString *)menuElementId
+ title:(nullable NSString *)title
+ children:(NSArray> *)children
{
if (self = [super init]) {
+ _menuElementId = [menuElementId copy];
_title = [title copy];
_children = [children copy];
}
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuEventsDelegate.h b/ios/gamma/stack/header/RNSStackHeaderMenuEventsDelegate.h
new file mode 100644
index 0000000000..829859191e
--- /dev/null
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuEventsDelegate.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol RNSStackHeaderMenuEventsDelegate
+
+- (void)didPressMenuElement:(NSString *)menuElementId;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm b/ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm
index 90d83d947e..151dcab1e8 100644
--- a/ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm
+++ b/ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm
@@ -29,7 +29,9 @@ + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
}
}
- return [[RNSStackHeaderMenuData alloc] initWithTitle:[self stringForKey:@"title" in:dict] children:children];
+ return [[RNSStackHeaderMenuData alloc] initWithId:[self stringForKey:@"menuElementId" in:dict]
+ title:[self stringForKey:@"title" in:dict]
+ children:children];
}
+ (nullable id)elementFromDictionary:(nullable id)dictionary
@@ -44,7 +46,8 @@ + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
return [self menuFromDictionary:dict];
} else if ([type isEqual:@"menuItem"]) {
[RNSStackHeaderMenuMapper validateMenuItemKeys:dict];
- return [[RNSStackHeaderMenuItemData alloc] initWithTitle:[self stringForKey:@"title" in:dict]];
+ return [[RNSStackHeaderMenuItemData alloc] initWithId:[self stringForKey:@"menuElementId" in:dict]
+ title:[self stringForKey:@"title" in:dict]];
}
return nil;
@@ -55,18 +58,22 @@ + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary
+ (void)validateMenuKeys:(NSDictionary *)dict
{
for (NSString *key in dict) {
- RCTAssert([key isEqualToString:@"type"] || [key isEqualToString:@"title"] || [key isEqualToString:@"children"],
+ RCTAssert([key isEqualToString:@"menuElementId"] || [key isEqualToString:@"type"] ||
+ [key isEqualToString:@"title"] || [key isEqualToString:@"children"],
@"Invalid key \"%@\" found in menu",
key);
}
+ RCTAssert(dict[@"menuElementId"], @"[RNScreens] missing menuElementId on one of menu elements");
}
+ (void)validateMenuItemKeys:(NSDictionary *)dict
{
for (NSString *key in dict) {
- RCTAssert(
- [key isEqualToString:@"type"] || [key isEqualToString:@"title"], @"Invalid key \"%@\" found in menu item", key);
+ RCTAssert([key isEqualToString:@"menuElementId"] || [key isEqualToString:@"type"] || [key isEqualToString:@"title"],
+ @"Invalid key \"%@\" found in menu item",
+ key);
}
+ RCTAssert(dict[@"menuElementId"], @"[RNScreens] missing menuElementId on one of menu elements");
}
+ (nullable NSString *)stringForKey:(NSString *)key in:(NSDictionary *)dict
diff --git a/src/components/gamma/stack/header/StackHeaderConfig.ios.tsx b/src/components/gamma/stack/header/StackHeaderConfig.ios.tsx
index 1e3e2ff31e..65ac02535f 100644
--- a/src/components/gamma/stack/header/StackHeaderConfig.ios.tsx
+++ b/src/components/gamma/stack/header/StackHeaderConfig.ios.tsx
@@ -1,17 +1,20 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import type { StackHeaderConfigProps } from './StackHeaderConfig.types';
-import StackHeaderConfigIOSNativeComponent from '../../../../fabric/gamma/stack/StackHeaderConfigIOSNativeComponent';
+import StackHeaderConfigIOSNativeComponent, {
+ PressMenuItemEvent,
+} from '../../../../fabric/gamma/stack/StackHeaderConfigIOSNativeComponent';
import type { StackHeaderItemPlacement } from './ios/StackHeaderItem.ios.types';
import { StackHeaderItemSpacerPlacement } from './ios/StackHeaderItemSpacer.ios.types';
import StackHeaderItemSpacer from './ios/StackHeaderItemSpacer.ios';
import StackHeaderItem from './ios/StackHeaderItem.ios';
-import { StyleSheet } from 'react-native';
+import { NativeSyntheticEvent, StyleSheet } from 'react-native';
import type {
StackHeaderInlineCustomItemIOS,
StackHeaderInlineItemIOS,
StackHeaderSpacerItemIOS,
StackHeaderTitleCustomItemIOS,
} from './StackHeaderConfig.ios.types';
+import { findMenuElementByIdInItems } from './utils';
/**
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
@@ -32,6 +35,23 @@ export default function StackHeaderConfig(props: StackHeaderConfigProps) {
largeTitleEnabled,
} = ios ?? {};
+ const handleMenuPress = useCallback(
+ (event: NativeSyntheticEvent) => {
+ const items = Array.of(
+ ...(leadingItems ?? []).filter(it => it && it.type === 'item'),
+ ...(trailingItems ?? []).filter(it => it && it.type === 'item'),
+ );
+ const menu = findMenuElementByIdInItems(
+ items,
+ event.nativeEvent.menuElementId,
+ );
+ if (menu && menu.type === 'menuItem') {
+ menu.onPress?.();
+ }
+ },
+ [leadingItems, trailingItems],
+ );
+
return (
+ style={styles.config}
+ onPressMenuItem={handleMenuPress}>
{leadingItems?.map(item => makeItemViewFromItem(item, 'leading'))}
{titleItem && makeItemViewFromItem(titleItem, 'title')}
{subtitleItem && makeItemViewFromItem(subtitleItem, 'subtitle')}
diff --git a/src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts b/src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts
index 4cc7dc07dd..936649c425 100644
--- a/src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts
+++ b/src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts
@@ -6,16 +6,20 @@ export interface StackHeaderBaseItemIOS {
label?: string | undefined;
}
-export interface StackHeaderInlineItemIOS extends StackHeaderBaseItemIOS {
- type: 'item';
+export interface SupportsMenuIOS {
menu?: StackHeaderMenu | undefined;
}
-export interface StackHeaderInlineCustomItemIOS {
+export interface StackHeaderInlineItemIOS
+ extends StackHeaderBaseItemIOS,
+ SupportsMenuIOS {
+ type: 'item';
+}
+
+export interface StackHeaderInlineCustomItemIOS extends SupportsMenuIOS {
key: string;
type: 'item';
render: () => ReactElement;
- menu?: StackHeaderMenu | undefined;
}
interface StackHeaderFixedSpacerItemIOS {
diff --git a/src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts b/src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts
index dafc908565..6d21e5b3ba 100644
--- a/src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts
+++ b/src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts
@@ -1,9 +1,12 @@
export interface StackHeaderMenuItem {
+ menuElementId: string;
type: 'menuItem';
title?: string | undefined;
+ onPress?: () => void;
}
export interface StackHeaderMenu {
+ menuElementId: string;
type: 'menu';
title?: string | undefined;
children: StackHeaderMenuElement[];
diff --git a/src/components/gamma/stack/header/utils.ts b/src/components/gamma/stack/header/utils.ts
new file mode 100644
index 0000000000..b5ee454b0b
--- /dev/null
+++ b/src/components/gamma/stack/header/utils.ts
@@ -0,0 +1,40 @@
+import { StackHeaderMenuElement } from './ios/StackHeaderMenu.ios.types';
+import { SupportsMenuIOS } from './StackHeaderConfig.ios.types';
+
+export function findMenuElementByIdInItems(
+ items: SupportsMenuIOS[],
+ menuElementId: string,
+): StackHeaderMenuElement | null {
+ for (const item of items) {
+ if (item.menu === undefined) {
+ continue;
+ }
+
+ const menu = findMenuElementById(item.menu, menuElementId);
+ if (menu !== null) {
+ return menu;
+ }
+ }
+
+ return null;
+}
+
+export function findMenuElementById(
+ menu: StackHeaderMenuElement,
+ menuElementId: string,
+): StackHeaderMenuElement | null {
+ if (menu.menuElementId === menuElementId) {
+ return menu;
+ }
+
+ if (menu.type === 'menu') {
+ for (const child of menu.children) {
+ const result = findMenuElementById(child, menuElementId);
+ if (result !== null) {
+ return result;
+ }
+ }
+ }
+
+ return null;
+}
diff --git a/src/fabric/gamma/stack/StackHeaderConfigIOSNativeComponent.ts b/src/fabric/gamma/stack/StackHeaderConfigIOSNativeComponent.ts
index b3c651c47d..f30b8783b2 100644
--- a/src/fabric/gamma/stack/StackHeaderConfigIOSNativeComponent.ts
+++ b/src/fabric/gamma/stack/StackHeaderConfigIOSNativeComponent.ts
@@ -3,6 +3,8 @@
import type { CodegenTypes as CT, ViewProps } from 'react-native';
import { codegenNativeComponent } from 'react-native';
+export type PressMenuItemEvent = Readonly<{ menuElementId: string }>;
+
export interface NativeProps extends ViewProps {
title?: string | undefined;
subtitle?: string | undefined;
@@ -14,6 +16,8 @@ export interface NativeProps extends ViewProps {
largeTitle?: string | undefined;
largeSubtitle?: string | undefined;
largeTitleEnabled?: CT.WithDefault;
+
+ onPressMenuItem?: CT.DirectEventHandler | undefined;
}
export default codegenNativeComponent('RNSStackHeaderConfigIOS', {