-
-
Notifications
You must be signed in to change notification settings - Fork 664
feat: Initial code for menu in header items #4138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import React, { useEffect, useMemo, useState } from 'react'; | ||
| import { createScenario } from '@apps/tests/shared/helpers'; | ||
| import { | ||
| StackContainer, | ||
| useStackNavigationContext, | ||
| } from '@apps/shared/gamma/containers/stack'; | ||
| import { StackHeaderConfigProps } from 'react-native-screens/components/gamma/stack/header'; | ||
| import { Button, ScrollView } from 'react-native'; | ||
| import LongText from '@apps/shared/LongText'; | ||
| import { scenarioDescription } from './scenario-description'; | ||
| import PressableWithFeedback from '@apps/shared/PressableWithFeedback'; | ||
|
|
||
| const DEFAULT_TRAILING_ITEMS_COUNT = 2; | ||
|
|
||
| export function App() { | ||
| return ( | ||
| <StackContainer | ||
| routeConfigs={[ | ||
| { | ||
| name: 'Home', | ||
| Component: ConfigScreen, | ||
| options: {}, | ||
| }, | ||
| ]} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function buildHeaderConfig(trailingItemsCount: number): StackHeaderConfigProps { | ||
| const trailingItems: NonNullable< | ||
| StackHeaderConfigProps['ios'] | ||
| >['trailingItems'] = Array.from({ length: trailingItemsCount }).map( | ||
| (_, i) => ({ | ||
| type: 'item', | ||
| key: `trailing-${i}`, | ||
| label: `Menu ${i}`, | ||
| // every second item is custom | ||
| ...(i % 2 === 0 && { | ||
| render: () => ( | ||
| <PressableWithFeedback style={{ width: 30, height: 30 }} /> | ||
| ), | ||
| }), | ||
| menu: { | ||
| type: 'menu', | ||
| children: [ | ||
| { type: 'menuItem', title: `Item ${i}.1` }, | ||
| { type: 'menuItem', title: `Item ${i}.2` }, | ||
| { | ||
| type: 'menu', | ||
| title: `Submenu ${i}`, | ||
| children: [ | ||
| { type: 'menuItem', title: `Nested ${i}.1` }, | ||
| { type: 'menuItem', title: `Nested ${i}.2` }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| return { | ||
| title: 'Header Menu', | ||
| ios: { | ||
| trailingItems, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function ConfigScreen() { | ||
| const navigation = useStackNavigationContext(); | ||
| const [trailingItemsCount, setTrailingItemsCount] = useState<number>( | ||
| DEFAULT_TRAILING_ITEMS_COUNT, | ||
| ); | ||
|
|
||
| const { setRouteOptions, routeKey } = navigation; | ||
| const headerConfig = useMemo( | ||
| () => buildHeaderConfig(trailingItemsCount), | ||
| [trailingItemsCount], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| setRouteOptions(routeKey, { | ||
| headerConfig, | ||
| }); | ||
| }, [headerConfig, setRouteOptions, routeKey]); | ||
|
|
||
| return ( | ||
| <ScrollView contentInsetAdjustmentBehavior="automatic"> | ||
| <Button | ||
| title={`Toggle trailing items count (${trailingItemsCount}/4)`} | ||
| onPress={() => setTrailingItemsCount(count => (count + 1) % 5)} | ||
| /> | ||
| <LongText /> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| export default createScenario(App, scenarioDescription); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import type { ScenarioDescription } from '@apps/tests/shared/helpers'; | ||
|
|
||
| export const scenarioDescription: ScenarioDescription = { | ||
| name: 'Stack Header Menu (iOS)', | ||
| key: 'test-stack-header-menu-ios', | ||
| details: 'Tests header item menus with nesting.', | ||
| platforms: ['ios'], | ||
| e2eCoverage: 'tbd', | ||
| smokeTest: false, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Test Scenario: Stack Header Menu (iOS) | ||
|
|
||
| ## Details | ||
|
|
||
| **Description:** This test focuses on handling menus attached to items in the header on iOS. | ||
|
|
||
| **OS test creation version:** iOS 26.4, iPadOS 26.4 | ||
|
|
||
| ## E2E test | ||
|
|
||
| TBD | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - iOS / iPadOS emulator | ||
|
|
||
| ## Note (Optional) | ||
|
|
||
| - For now, menus don't appear on items with custom views | ||
|
|
||
| ## Steps on iPhone | ||
|
|
||
| 1. Open Dev Console | ||
| 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 | ||
| 4. While the menu is opened, click on the Submenu 1 | ||
| - [ ] A nested menu appears, containing two items |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| #pragma once | ||
|
|
||
| #import <UIKit/UIKit.h> | ||
| #import "RNSStackHeaderMenuData.h" | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| @interface RNSStackHeaderMenuCoordinator : NSObject | ||
|
|
||
| + (void)applyMenu:(nonnull RNSStackHeaderMenuData *)data toBarButtonItem:(nonnull UIBarButtonItem *)item; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| #import "RNSStackHeaderMenuCoordinator.h" | ||
|
|
||
| @implementation RNSStackHeaderMenuCoordinator | ||
|
|
||
| + (void)applyMenu:(nonnull RNSStackHeaderMenuData *)data toBarButtonItem:(nonnull UIBarButtonItem *)item | ||
| { | ||
| #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we should create a macro for tvOS version, similar to |
||
| if (@available(tvOS 17.0, *)) { | ||
| item.menu = [self buildMenuFromData:data]; | ||
| } | ||
| #endif // !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 | ||
| } | ||
|
|
||
| + (UIMenu *)buildMenuFromData:(RNSStackHeaderMenuData *)data | ||
| { | ||
| NSMutableArray<UIMenuElement *> *elements = [NSMutableArray arrayWithCapacity:data.children.count]; | ||
| for (id<RNSStackHeaderMenuElement> child in data.children) { | ||
| UIMenuElement *element = [self buildElementFromData:child]; | ||
| if (element != nil) { | ||
| [elements addObject:element]; | ||
| } | ||
| } | ||
|
|
||
| return [UIMenu menuWithTitle:data.title ?: @"" children:elements]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a difference between
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same below |
||
| } | ||
|
|
||
| + (nullable UIMenuElement *)buildElementFromData:(id<RNSStackHeaderMenuElement>)element | ||
| { | ||
| if ([element isKindOfClass:[RNSStackHeaderMenuData class]]) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: you may consider having a method on the protocol to avoid selecting the implementation based on the class type here |
||
| return [self buildMenuFromData:(RNSStackHeaderMenuData *)element]; | ||
| } | ||
|
|
||
| if ([element isKindOfClass:[RNSStackHeaderMenuItemData class]]) { | ||
| RNSStackHeaderMenuItemData *itemData = (RNSStackHeaderMenuItemData *)element; | ||
| return [UIAction actionWithTitle:itemData.title ?: @"" | ||
| image:nil | ||
| identifier:nil | ||
| handler:^(__kindof UIAction *_Nonnull action){ | ||
| // noop | ||
| }]; | ||
| } | ||
|
|
||
| return nil; | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| #pragma once | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| @protocol RNSStackHeaderMenuElement <NSObject> | ||
| @end | ||
|
|
||
| @interface RNSStackHeaderMenuItemData : NSObject <RNSStackHeaderMenuElement> | ||
|
|
||
| @property (nonatomic, copy, readonly, nullable) NSString *title; | ||
|
|
||
| - (instancetype)initWithTitle:(nullable NSString *)title; | ||
|
|
||
| @end | ||
|
|
||
| @interface RNSStackHeaderMenuData : NSObject <RNSStackHeaderMenuElement> | ||
|
|
||
| @property (nonatomic, copy, readonly, nullable) NSString *title; | ||
| @property (nonatomic, copy, readonly) NSArray<id<RNSStackHeaderMenuElement>> *children; | ||
|
|
||
| - (instancetype)initWithTitle:(nullable NSString *)title children:(NSArray<id<RNSStackHeaderMenuElement>> *)children; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #import "RNSStackHeaderMenuData.h" | ||
|
|
||
| @implementation RNSStackHeaderMenuItemData | ||
|
|
||
| - (instancetype)initWithTitle:(nullable NSString *)title | ||
| { | ||
| if (self = [super init]) { | ||
| _title = [title copy]; | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| @end | ||
|
|
||
| @implementation RNSStackHeaderMenuData | ||
|
|
||
| - (instancetype)initWithTitle:(nullable NSString *)title children:(NSArray<id<RNSStackHeaderMenuElement>> *)children | ||
| { | ||
| if (self = [super init]) { | ||
| _title = [title copy]; | ||
| _children = [children copy]; | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #pragma once | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| #import "RNSStackHeaderMenuData.h" | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| @interface RNSStackHeaderMenuMapper : NSObject | ||
|
|
||
| + (nullable RNSStackHeaderMenuData *)menuFromDictionary:(nullable id)dictionary; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coordinator or applicator or factory?