From 020020f532930487f6c49bb9b581e80152e6ddac Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Tue, 28 Apr 2026 12:59:08 +0200 Subject: [PATCH 1/5] add commands skeleton --- .../config/StackHeaderConfigViewManager.kt | 14 +++++++ .../header/StackHeaderConfig.android.tsx | 31 +++++++++++++- ...StackHeaderConfigAndroidNativeComponent.ts | 40 ++++++++++++++++++- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt index aa7ac413a7..0adcf0d634 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt @@ -2,6 +2,7 @@ package com.swmansion.rnscreens.gamma.stack.header.config import android.view.View import com.facebook.react.bridge.JSApplicationIllegalArgumentException +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ReactStylesDiffMap @@ -186,6 +187,19 @@ open class StackHeaderConfigViewManager : view.scrollFlagSnap = value } + override fun setToolbarMenuItems( + view: StackHeaderConfig, + value: ReadableArray?, + ) = Unit + + override fun setToolbarMenuItemOptions( + view: StackHeaderConfig, + id: String, + options: ReadableArray, + ) { + println("[StackHeaderConfigViewManager] " + options.getMap(0)) + } + companion object { const val REACT_CLASS = "RNSStackHeaderConfigAndroid" } diff --git a/src/components/gamma/stack/header/StackHeaderConfig.android.tsx b/src/components/gamma/stack/header/StackHeaderConfig.android.tsx index 4b7954a11d..7cac35d4a4 100644 --- a/src/components/gamma/stack/header/StackHeaderConfig.android.tsx +++ b/src/components/gamma/stack/header/StackHeaderConfig.android.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { ComponentRef, useEffect, useRef } from 'react'; import { Image, StyleSheet } from 'react-native'; import type { StackHeaderConfigProps } from './StackHeaderConfig.types'; -import StackHeaderConfigAndroidNativeComponent from '../../../../fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent'; +import StackHeaderConfigAndroidNativeComponent, { + Commands as StackHeaderConfigAndroidNativeCommands, +} from '../../../../fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent'; import type { NativeProps as StackHeaderConfigAndroidNativeComponentProps } from '../../../../fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent'; import StackHeaderSubview from './android/StackHeaderSubview.android'; import type { @@ -17,6 +19,30 @@ function StackHeaderConfig(props: StackHeaderConfigProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { android, ios, ...baseProps } = props; + const ref = useRef | null>(null); + + useEffect(() => { + const handle = setTimeout(() => { + if (!ref.current) { + return; + } + + StackHeaderConfigAndroidNativeCommands.setToolbarMenuItemOptions( + ref.current, + 'someItemId', + [ + { + title: 'Hello!', + }, + ], + ); + }, 5000); + + return () => clearTimeout(handle); + }, []); + const { backgroundSubview, leadingSubview, @@ -42,6 +68,7 @@ function StackHeaderConfig(props: StackHeaderConfigProps) { return ( ; + +export interface ToolbarMenuItemAndroid { + id: string; + title: string; + hidden?: CT.WithDefault; + + // TODO: add Menu +} + export interface NativeProps extends ViewProps { title?: string | undefined; hidden?: CT.WithDefault; @@ -28,8 +41,33 @@ export interface NativeProps extends ViewProps { scrollFlagEnterAlwaysCollapsed?: CT.WithDefault; scrollFlagExitUntilCollapsed?: CT.WithDefault; scrollFlagSnap?: CT.WithDefault; + + toolbarMenuItems?: ToolbarMenuItemAndroid[] | undefined; + onToolbarMenuItemClicked?: + | CT.DirectEventHandler + | undefined; } +type ComponentType = HostComponent; + +export type ToolbarMenuItemOptionsAndroid = Partial< + Omit +>; + +export interface NativeCommands { + setToolbarMenuItemOptions: ( + viewRef: React.ComponentRef, + id: string, + // We use the array here only due to codegen limitation. We're using only + // the first index of the array. + options: ToolbarMenuItemOptionsAndroid[], + ) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['setToolbarMenuItemOptions'], +}); + export default codegenNativeComponent( 'RNSStackHeaderConfigAndroid', { From af0b92d6b79afd7673be12d2e11c4776a88bfbd5 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Tue, 28 Apr 2026 15:22:35 +0200 Subject: [PATCH 2/5] add sft skeleton --- .../single-feature-tests/stack-v5/index.ts | 2 + .../index.tsx | 161 ++++++++++++++++++ .../scenario.md | 1 + 3 files changed, 164 insertions(+) create mode 100644 apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-commands/index.tsx create mode 100644 apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-commands/scenario.md diff --git a/apps/src/tests/single-feature-tests/stack-v5/index.ts b/apps/src/tests/single-feature-tests/stack-v5/index.ts index 5c6f505d04..dfd06528ae 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/index.ts +++ b/apps/src/tests/single-feature-tests/stack-v5/index.ts @@ -5,6 +5,7 @@ import AnimationAndroid from './test-animation-android'; import TestStackSimpleNav from './test-stack-simple-nav'; import TestStackSubviews from './test-stack-subviews-android'; import TestStackBackButton from './test-stack-back-button-android'; +import TestStackToolbarMenuCommands from './test-stack-toolbar-menu-commands'; const scenarios = { PreventNativeDismissSingleStack, @@ -13,6 +14,7 @@ const scenarios = { TestStackSimpleNav, TestStackSubviews, TestStackBackButton, + TestStackToolbarMenuCommands, }; const StackScenarioGroup: ScenarioGroup = { diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-commands/index.tsx b/apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-commands/index.tsx new file mode 100644 index 0000000000..cd01e2ec82 --- /dev/null +++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-commands/index.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Button, ScrollView, StyleSheet, Text } from 'react-native'; +import { + createScenario, + ScenarioDescription, +} from '@apps/tests/shared/helpers'; +import { + StackContainer, + useStackNavigationContext, +} from '@apps/shared/gamma/containers/stack'; +import { Colors } from '@apps/shared/styling'; +import type { StackHeaderConfigProps } from 'react-native-screens/experimental'; + +const scenarioDescription: ScenarioDescription = { + name: 'Stack Toolbar Menu Commands', + key: 'test-stack-toolbar-menu-commands', + details: 'Tests changes to toolbar menu in runtime.', + platforms: ['android'], +}; + +interface Config { + // backButtonHidden: boolean; + // tintColor: TintColorOption; + // icon: IconOption; +} + +const DEFAULT_CONFIG: Config = { + // backButtonHidden: false, + // tintColor: 'default', + // icon: 'default', +}; + +const ConfigContext = React.createContext<{ + config: Config; + updateConfig: (key: K, value: Config[K]) => void; +}>({ + config: DEFAULT_CONFIG, + updateConfig: () => {}, +}); + +function buildHeaderConfig(config: Config): StackHeaderConfigProps { + return { + title: 'Toolbar Menu Commands Test', + // backButtonHidden: config.backButtonHidden, + // android: { + // backButtonTintColor: resolveTintColor(config.tintColor), + // backButtonIcon: resolveIcon(config.icon), + // }, + }; +} + +export function App() { + const [config, setConfig] = useState(DEFAULT_CONFIG); + + const updateConfig = useCallback( + (key: K, value: Config[K]) => { + setConfig(prev => ({ ...prev, [key]: value })); + }, + [], + ); + + return ( + + + + ); +} + +function ConfigControls() { + const { config, updateConfig } = React.useContext(ConfigContext); + + return ( + <> + Toolbar Menu + {/* updateConfig('backButtonHidden', v)} + /> + + label="tintColor" + value={config.tintColor} + onValueChange={v => updateConfig('tintColor', v)} + items={TINT_COLOR_OPTIONS} + /> + + label="icon" + value={config.icon} + onValueChange={v => updateConfig('icon', v)} + items={ICON_OPTIONS} + />*/} + + ); +} + +function useApplyHeaderConfig() { + const { config } = React.useContext(ConfigContext); + const { setRouteOptions, routeKey } = useStackNavigationContext(); + const headerConfig = useMemo(() => buildHeaderConfig(config), [config]); + + useEffect(() => { + setRouteOptions(routeKey, { headerConfig }); + }, [headerConfig, setRouteOptions, routeKey]); +} + +function RootScreen() { + const { push } = useStackNavigationContext(); + useApplyHeaderConfig(); + + return ( + + + Navigation +