Skip to content

feat(Android, Stack v5): toolbar menu item icon and icon tint color#4105

Open
kligarski wants to merge 18 commits into
mainfrom
@kligarski/stack-v5-android-toolbar-menu-icon
Open

feat(Android, Stack v5): toolbar menu item icon and icon tint color#4105
kligarski wants to merge 18 commits into
mainfrom
@kligarski/stack-v5-android-toolbar-menu-icon

Conversation

@kligarski

@kligarski kligarski commented May 27, 2026

Copy link
Copy Markdown
Contributor

Description

Adds support for icons and icon tinting for toolbar menu items in Stack v5 on Android.

Closes https://github.com/software-mansion/react-native-screens-labs/issues/1207.

Details

  1. I tried not to use any additional state for color state list, that's why we have some code to read the current values from menu item state.
  2. Setting tint color without normal leads to transparent icon - that's platform limitation. If you specify any tint, we can't use original appearance of the icon. You must specify normal if you want to apply any tint color.
  3. Applying tint color to back button will be added in separate PR.
  4. Currently, we don't support making the toolbar menu item disabled - this will be added in separate PR.
  5. For view command update, if we need to load the image asynchronously, we delay applying the command until the image is loaded. I think it's reasonable - we want to avoid a flash e.g. if you change both colors and icon. This however differs from regular prop behavior where we apply all props as soon as possible. When you add multiple icons, each will force the update - I did it to simplify the logic, the runtime looks reasonable as well. If it turns out to be problematic, we can consider batching these updates but we would need to refactor image loading to ensure that something is always returned.
  6. To test "focused" state you need to enable "Hardware input" in your emulator. Select something with keyboard focus (use arrows) and then click Ctrl+Tab - this will move the focus to the toolbar.

Changes

  • move parseIconToNativeProps to components/shared
  • add new props and process them for native props (change undefined to null, parseColor/resolveAssetSource)
  • add StackHeaderToolbarUpdate to handle props where null is the default value and "no change" needs different representation from "reset to default"
  • create IconResolver to handle icon-related logic (diffing, handling async updates)
  • handle icon and color changes for both prop and view command paths
  • ensure drawable is resized to 24 dp height (default icon size according to M3 spec)
  • add SFT

Before & after - visual documentation

toolbar_menu_icons.mp4

Test plan

Run test-stack-toolbar-menu-icon-android. Check for regressions in test-stack-subviews-android and test-stack-back-button-android.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

@kligarski kligarski changed the base branch from main to @kligarski/stack-v5-android-toolbar-menu-show-as-action May 27, 2026 14:33
Base automatically changed from @kligarski/stack-v5-android-toolbar-menu-show-as-action to main June 2, 2026 11:20
@kligarski kligarski force-pushed the @kligarski/stack-v5-android-toolbar-menu-icon branch from 92b5302 to 8105b22 Compare June 2, 2026 14:48
@kligarski kligarski requested a review from Copilot June 2, 2026 15:02
@kligarski kligarski marked this pull request as ready for review June 2, 2026 15:02

Copilot AI left a comment

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.

Pull request overview

Adds Android Stack v5 support for toolbar menu item icons (drawable resources or image sources) and per-state icon tinting, including correct “no change” vs “reset to default” semantics for view-command updates. This integrates new JS-to-native props, native icon/tint application in MaterialToolbar, and a Single Feature Test scenario to validate behavior.

Changes:

  • Extend Stack v5 toolbar menu item API with icon and iconTintColor* props, and plumb them through Fabric + view-command update paths.
  • Introduce native update/value wrappers (StackHeaderToolbarUpdate) and icon resolution infrastructure (IconResolver, icon source maps) to support async image loading and diffing.
  • Add a new Android SFT scenario for toolbar menu icons/tints and wire it into the stack-v5 scenario group.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent.ts Adds native prop fields for menu item icon sources and processed tint colors.
src/components/tabs/screen/TabsScreen.android.tsx Switches tabs icon parsing to use shared parseIconToNativeProps.
src/components/shared/index.ts Adds shared parseIconToNativeProps helper (moved from tabs).
src/components/gamma/stack/header/StackHeaderConfig.android.types.ts Extends public TS types/docs with menu item icon and iconTintColor* props.
src/components/gamma/stack/header/StackHeaderConfig.android.tsx Converts toolbar menu item icon/tint props to native props; updates view-command option serialization.
apps/src/tests/single-feature-tests/tabs/test-tabs-item-title-ios/index.tsx Formatting/line wrapping updates in an iOS SFT description component.
apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-icon-android/scenario.md New manual test scenario documenting expected behavior for icon + tint props/commands.
apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-icon-android/scenario-descriptions.ts Registers the new SFT scenario metadata.
apps/src/tests/single-feature-tests/stack-v5/test-stack-toolbar-menu-icon-android/index.tsx Implements the interactive SFT screen for icon/tint prop + command flows.
apps/src/tests/single-feature-tests/stack-v5/index.ts Adds the new scenario to the stack-v5 scenario group.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarUpdate.kt Adds reset-vs-set wrapper to distinguish “reset default” from “no change”.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuItemOptions.kt Extends menu item option updates to include icon + per-state tint updates.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuItemIconSource.kt Adds a compact icon-source model (drawable name / image uri).
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuItemDefaults.kt Adds defaults for icon/tint-related fields.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuItemConfig.kt Extends config model with resolved icon drawable and tint colors.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/toolbar/StackHeaderToolbarMenuCoordinator.kt Applies icon + tint updates to toolbar MenuItems and resizes drawables to 24dp.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/StackHeaderCoordinator.kt Updates coordinator call signature for menu item updates.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfigViewManager.kt Parses new icon/tint props/options and stages icon sources for async resolution.
android/src/main/java/com/swmansion/rnscreens/gamma/stack/header/config/StackHeaderConfig.kt Adds icon resolution for back button + toolbar items and atomic command application.
android/src/main/java/com/swmansion/rnscreens/gamma/helpers/IconResolver.kt Adds reusable icon resolver for drawable-name vs image-uri sources with diffing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/shared/index.ts Outdated

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Comment on lines +43 to +60
if (drawableIconResourceName == lastEmittedDrawableName &&
imageIconUri == lastEmittedImageUri
) {
onResult(IconResolution.Unchanged)
return
}
lastEmittedDrawableName = drawableIconResourceName
lastEmittedImageUri = imageIconUri
when {
drawableIconResourceName != null ->
onResult(IconResolution.Resolved(getSystemDrawableResource(context, drawableIconResourceName)))
imageIconUri != null ->
loadImage(context, imageIconUri) { drawable ->
if (imageIconUri == lastImageUri && lastDrawableName == null) {
onResult(IconResolution.Resolved(drawable))
}
}
else -> onResult(IconResolution.Resolved(null))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think this is an issue now. Let me know if you think we should add it now.

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.

at least we should note that down for further work, but this PR contains many changes, and as the initial version, I'm okay with merging it in current shape

@kligarski kligarski requested review from kkafar and kmichalikk June 2, 2026 15:56
@kligarski kligarski requested review from sgaczol and t0maboro June 2, 2026 15:56
Comment on lines +132 to +134
return drawable
.toBitmap(width = targetWidthPx, height = targetHeightPx)
.toDrawable(toolbar.resources)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This doesn't resize drawable resources correctly. It will be fixed in follow-up PR with resizing & tint for back button.

// Helpers for view commands:
// - not defined -> null (means "no update")
// - null -> default (means "reset to default value")
private fun ReadableMap.readColor(

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.

wondering whether we shouldn't have a general helper file with all extensions rather than private parsers in the manager's source code (I did the same for TabsScreenManager) , unrelated to this PR, but do you think we should add a ticket to unify the approach or at least have a single file definitions for prop parsing?

* @summary Specifies the icon for the menu item.
*
* Supported values:
* - `{ type: 'imageSource', imageSource }`

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.

nit: shouldn't we mention what type imageSource is?

import { Image, type ImageResolvedAssetSource } from 'react-native';
import type { PlatformIconAndroid } from '../../types';

export function parseIconToNativeProps(icon: PlatformIconAndroid | undefined): {

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.

nit: parseAndroidIconToNativeProps

Comment on lines +43 to +60
if (drawableIconResourceName == lastEmittedDrawableName &&
imageIconUri == lastEmittedImageUri
) {
onResult(IconResolution.Unchanged)
return
}
lastEmittedDrawableName = drawableIconResourceName
lastEmittedImageUri = imageIconUri
when {
drawableIconResourceName != null ->
onResult(IconResolution.Resolved(getSystemDrawableResource(context, drawableIconResourceName)))
imageIconUri != null ->
loadImage(context, imageIconUri) { drawable ->
if (imageIconUri == lastImageUri && lastDrawableName == null) {
onResult(IconResolution.Resolved(drawable))
}
}
else -> onResult(IconResolution.Resolved(null))

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.

at least we should note that down for further work, but this PR contains many changes, and as the initial version, I'm okay with merging it in current shape

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants