Skip to content
Open
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
@@ -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,
Expand All @@ -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 (
<StackContainer
routeConfigs={[
{
name: 'Home',
Component: ConfigScreen,
options: {},
},
]}
/>
<ToastProvider>
<StackContainer
routeConfigs={[
{
name: 'Home',
Component: ConfigScreen,
options: {},
},
]}
/>
</ToastProvider>
);
}

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(
Expand All @@ -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`),
},
],
},
],
Expand All @@ -68,14 +97,22 @@ function buildHeaderConfig(trailingItemsCount: number): StackHeaderConfigProps {

function ConfigScreen() {
const navigation = useStackNavigationContext();
const toast = useToast();
const [trailingItemsCount, setTrailingItemsCount] = useState<number>(
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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
29 changes: 25 additions & 4 deletions ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -28,7 +30,8 @@ static void RNSAssertIsValidHeaderChild(UIView *child)
RNSStackHeaderItemSpacerComponentView.class);
}

@interface RNSStackHeaderConfigComponentView () <RNSStackHeaderItemInvalidationDelegate>
@interface RNSStackHeaderConfigComponentView () <RNSStackHeaderItemInvalidationDelegate,
RNSStackHeaderMenuEventsDelegate>
@end

@implementation RNSStackHeaderConfigComponentView {
Expand All @@ -43,6 +46,7 @@ @implementation RNSStackHeaderConfigComponentView {

std::shared_ptr<const react::RNSStackHeaderConfigShadowNode::ConcreteState> _state;
RNSStackHeaderConfigShadowStateProxy *_Nonnull _shadowStateProxy;
RNSStackHeaderConfigEventEmitter *_Nonnull _reactEventEmitter;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand All @@ -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;
Expand Down Expand Up @@ -111,13 +116,20 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)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
Expand Down Expand Up @@ -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<const react::RNSStackHeaderConfigIOSEventEmitter>(eventEmitter)];
}

#pragma mark - Private

- (void)submitCurrentDataIfMounted
Expand Down Expand Up @@ -244,11 +263,13 @@ - (void)buildBarButtonItemsWithLeadingItems:(NSMutableArray<UIBarButtonItem *> *
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) {
Expand Down
32 changes: 32 additions & 0 deletions ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#import <Foundation/Foundation.h>

// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus)
#import <react/renderer/components/rnscreens/EventEmitters.h>

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<const react::RNSStackHeaderConfigIOSEventEmitter> &)emitter;

@end

#endif // __cplusplus

NS_ASSUME_NONNULL_END
24 changes: 24 additions & 0 deletions ios/gamma/stack/header/RNSStackHeaderConfigEventEmitter.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#import "RNSStackHeaderConfigEventEmitter.h"
#import <React/RCTLog.h>

@implementation RNSStackHeaderConfigEventEmitter {
std::shared_ptr<const react::RNSStackHeaderConfigIOSEventEmitter> _reactEventEmitter;
}

- (BOOL)emitOnPressMenuItem:(NSString *)menuElementId
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onPressMenuItem({.menuElementId = menuElementId.cString});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. in tabs we use RCTStringFromNSString, maybe we should do so here as well

{.selectedScreenKey = RCTStringFromNSString(payload.selectedScreenKey),

return YES;
Comment thread
kmichalikk marked this conversation as resolved.
} else {
RCTLogWarn(@"[RNScreens] Skipped OnPressMenuItem event emission due to nullish emitter");
return NO;
}
}

- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSStackHeaderConfigIOSEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}

@end
4 changes: 3 additions & 1 deletion ios/gamma/stack/header/RNSStackHeaderContentFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

#import "RNSStackHeaderItemDataProviding.h"
#import "RNSStackHeaderItemSpacerDataProviding.h"
#import "RNSStackHeaderMenuEventsDelegate.h"
#import "RNSViewFrameChangeDelegate.h"

NS_ASSUME_NONNULL_BEGIN

@interface RNSStackHeaderContentFactory : NSObject

+ (UIBarButtonItem *)barButtonItemForHeaderItem:(id<RNSStackHeaderItemDataProviding>)item
withFrameChangeDelegate:(id<RNSViewFrameChangeDelegate>)delegate;
withFrameChangeDelegate:(id<RNSViewFrameChangeDelegate>)delegate
withMenuEventsDelegate:(id<RNSStackHeaderMenuEventsDelegate>)menuEventsDelegate;

+ (UIView *)wrappedViewForHeaderItem:(id<RNSStackHeaderItemDataProviding>)item
frameChangeDelegate:(id<RNSViewFrameChangeDelegate>)delegate;
Expand Down
5 changes: 4 additions & 1 deletion ios/gamma/stack/header/RNSStackHeaderContentFactory.mm
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ @implementation RNSStackHeaderContentFactory

+ (UIBarButtonItem *)barButtonItemForHeaderItem:(id<RNSStackHeaderItemDataProviding>)item
withFrameChangeDelegate:(id<RNSViewFrameChangeDelegate>)delegate
withMenuEventsDelegate:(id<RNSStackHeaderMenuEventsDelegate>)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;
Expand Down
5 changes: 4 additions & 1 deletion ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

#import <UIKit/UIKit.h>
#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<RNSStackHeaderMenuEventsDelegate>)delegate;

@end

Expand Down
17 changes: 11 additions & 6 deletions ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

@implementation RNSStackHeaderMenuCoordinator

+ (void)applyMenu:(nonnull RNSStackHeaderMenuData *)data toBarButtonItem:(nonnull UIBarButtonItem *)item
+ (void)applyMenu:(RNSStackHeaderMenuData *)data
toBarButtonItem:(UIBarButtonItem *)item
withMenuEventsDelegate:(id<RNSStackHeaderMenuEventsDelegate>)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<RNSStackHeaderMenuEventsDelegate>)delegate
{
NSMutableArray<UIMenuElement *> *elements = [NSMutableArray arrayWithCapacity:data.children.count];
for (id<RNSStackHeaderMenuElement> child in data.children) {
UIMenuElement *element = [self buildElementFromData:child];
UIMenuElement *element = [self buildElementFromData:child withMenuEventsDelegate:delegate];
if (element != nil) {
[elements addObject:element];
}
Expand All @@ -25,18 +28,20 @@ + (UIMenu *)buildMenuFromData:(RNSStackHeaderMenuData *)data
}

+ (nullable UIMenuElement *)buildElementFromData:(id<RNSStackHeaderMenuElement>)element
withMenuEventsDelegate:(id<RNSStackHeaderMenuEventsDelegate>)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<RNSStackHeaderMenuEventsDelegate> 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];
}];
}

Expand Down
Loading