Skip to content

feat(menu): add subtitle, destructive and theme support for menu items#8

Merged
sbaiahmed1 merged 3 commits intomainfrom
feat/add-dark-mode-support
Dec 6, 2025
Merged

feat(menu): add subtitle, destructive and theme support for menu items#8
sbaiahmed1 merged 3 commits intomainfrom
feat/add-dark-mode-support

Conversation

@sbaiahmed1
Copy link
Copy Markdown
Owner

@sbaiahmed1 sbaiahmed1 commented Dec 6, 2025

  • Add subtitle and destructive properties to menu items for both iOS and Android
  • Implement themeVariant support for dark/light/system themes
  • Add title support for menu dialogs
  • Update example app to showcase new features

Summary by Sourcery

Add support for titled, themable menus with richer item metadata across iOS and Android, and update the example app to showcase these capabilities.

New Features:

  • Support subtitles and destructive flags on menu items on both iOS and Android.
  • Allow configuring a title for the menu dialog.
  • Add themeVariant support to control dark, light, or system-based menu appearance.

Enhancements:

  • Improve Android dialog layout to support rich menu rows with title, subtitle, destructive styling, and theme-aware colors.
  • Update the example app with a complex menu example demonstrating titles, subtitles, destructive items, and dark theme styling.

- Add subtitle and destructive properties to menu items for both iOS and Android
- Implement themeVariant support for dark/light/system themes
- Add title support for menu dialogs
- Update example app to showcase new features
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Dec 6, 2025

Reviewer's Guide

Adds subtitle, destructive, themeVariant and title support to menu items on both iOS and Android and updates the example app to demonstrate these features.

Sequence diagram for updated menu rendering and selection flow with themeVariant and destructive items

sequenceDiagram
    actor User
    participant App as ReactApp
    participant JSMenu as MenuViewNativeComponent
    participant AndroidView as AndroidMenuView
    participant IOSView as IOSMenuView

    User->>App: Open screen with complex menu
    App->>JSMenu: Render MenuView(title, themeVariant="dark", menuItems[subtitle, destructive])

    JSMenu-->>AndroidView: setTitle(title)
    JSMenu-->>AndroidView: setThemeVariant("dark")
    JSMenu-->>AndroidView: setMenuItems(menuItems)

    JSMenu-->>IOSView: updateProps(title, themeVariant, menuItems)

    AndroidView->>AndroidView: getBackgroundColor()/getTextColor() based on themeVariant
    AndroidView->>AndroidView: Build dialog with header, subtitles, destructive styling

    IOSView->>IOSView: Apply overrideUserInterfaceStyle from themeVariant
    IOSView->>IOSView: Map menuItems to UIActions
    IOSView->>IOSView: Set action.subtitle and destructive attributes

    User-->>AndroidView: Tap destructive item (Android)
    AndroidView->>AndroidView: selectMenuItem(identifier, title)
    AndroidView-->>JSMenu: onMenuSelect(nativeEvent)
    JSMenu-->>App: handleMenuSelect(event)

    User-->>IOSView: Tap destructive item (iOS)
    IOSView->>IOSView: selectMenuItem(identifier, title)
    IOSView-->>JSMenu: onMenuSelect(nativeEvent)
    JSMenu-->>App: handleMenuSelect(event)
Loading

Class diagram for updated MenuView native components and props

classDiagram
    class MenuItem {
      +string identifier
      +string title
      +string subtitle
      +bool destructive
      +string iosSymbol
    }

    class NativeProps {
      +string title
      +string themeVariant
      +string color
      +string checkedColor
      +string uncheckedColor
      +MenuItem[] menuItems
    }

    class MenuViewNativeComponent {
      +render(props NativeProps) void
    }

    class AndroidMenuView {
      -String themeVariant
      -String title
      -List~Map~ menuItems
      -String selectedItemIdentifier
      -String checkedColor
      -String uncheckedColor
      +setTitle(title String) void
      +setThemeVariant(themeVariant String) void
      +setMenuItems(menuItems ReadableArray) void
      -getBackgroundColor() int
      -getTextColor() int
      -selectMenuItem(identifier String, title String) void
    }

    class AndroidMenuViewManager {
      +createViewInstance(reactContext ReactContext) AndroidMenuView
      +setTitle(view AndroidMenuView, title String) void
      +setThemeVariant(view AndroidMenuView, themeVariant String) void
      +setColor(view AndroidMenuView, color String) void
    }

    class IOSMenuView {
      -NSArray menuItems
      -NSString selectedIdentifier
      +updateProps(props MenuViewProps, oldProps MenuViewProps) void
      +updateMenuItems(menuItems NSArray, selectedIdentifier NSString) void
    }

    class MenuViewProps {
      +string title
      +string themeVariant
      +MenuItem[] menuItems
    }

    MenuViewNativeComponent --> NativeProps : uses
    NativeProps "*" --> MenuItem : contains

    AndroidMenuViewManager --> AndroidMenuView : creates
    AndroidMenuViewManager --> AndroidMenuView : setsTitle/themeVariant

    IOSMenuView --> MenuViewProps : reads
    IOSMenuView --> MenuItem : buildsFrom

    MenuViewProps --> MenuItem : contains
Loading

File-Level Changes

Change Details Files
Extend native menu item model and props to support subtitles and destructive actions on both platforms.
  • Broaden Android menuItems storage to Map<String, Any> and read optional subtitle and destructive fields from the React ReadableArray.
  • Propagate subtitle and destructive for each item into the internal iOS _menuItems dictionary, tracking them in diffing logic so menus rebuild when they change.
  • On iOS, configure UIAction attributes and subtitle/discoverabilityTitle based on destructive and subtitle values when constructing the UIMenu.
android/src/main/java/com/menu/MenuView.kt
ios/MenuView.mm
src/MenuViewNativeComponent.ts
Add themeVariant and optional title support for menu dialogs on Android and themeVariant support on iOS.
  • Introduce themeVariant and title fields and setters in Android MenuView and MenuViewManager, and use themeVariant to compute background/text colors and divider colors.
  • Render an optional header title and separator in the Android dialog when a non-empty title is provided.
  • On iOS, react to themeVariant prop in updateProps and set overrideUserInterfaceStyle to dark, light or unspecified accordingly; plumb title through props when building the UIMenu title.
android/src/main/java/com/menu/MenuView.kt
android/src/main/java/com/menu/MenuViewManager.kt
ios/MenuView.mm
src/MenuViewNativeComponent.ts
Rework Android dialog item rendering to support rich rows (title, subtitle, destructive) while still using RadioButton as the selection indicator.
  • Replace plain RadioButtons in the Android dialog RadioGroup with a horizontal LinearLayout row containing a vertical text container and a RadioButton.
  • Apply destructive styling to titles, render optional subtitle TextView, and compute colors via themeVariant helpers.
  • Make the row container handle selection and dialog dismissal while the RadioButton only reflects checked state and uses themed tint colors.
android/src/main/java/com/menu/MenuView.kt
Update example app to showcase complex menu features instead of previous long list and disabled menu examples.
  • Remove the isDisabled state and disabled menu example from App.tsx along with the long scrollable options example.
  • Add a new "Complex Menu" example using MenuView title, themeVariant="dark", and items with subtitle and destructive properties.
  • Style the complex menu trigger button to visually match a dark-themed menu.
example/src/App.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In MenuViewNativeComponent.ts, consider narrowing themeVariant?: string to a string union type (e.g. 'light' | 'dark' | 'system') so that JS/TS callers get compile-time validation and autocompletion instead of passing arbitrary strings.
  • On Android, getBackgroundColor() and getTextColor() duplicate the same UI_MODE_NIGHT_* branching logic; you could factor this into a small helper to avoid divergence if the theme handling needs to change later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MenuViewNativeComponent.ts`, consider narrowing `themeVariant?: string` to a string union type (e.g. `'light' | 'dark' | 'system'`) so that JS/TS callers get compile-time validation and autocompletion instead of passing arbitrary strings.
- On Android, `getBackgroundColor()` and `getTextColor()` duplicate the same `UI_MODE_NIGHT_*` branching logic; you could factor this into a small helper to avoid divergence if the theme handling needs to change later.

## Individual Comments

### Comment 1
<location> `ios/MenuView.mm:341-348` </location>
<code_context>
+    // For now, since title was removed from props based on user request, we use empty string
+    // But if we wanted to support title later:
+    NSString *menuTitle = @"";
+    if (_props) {
+        const auto &viewProps = *std::static_pointer_cast<MenuViewProps const>(_props);
+        if (!viewProps.title.empty()) {
+             menuTitle = [[NSString alloc] initWithUTF8String:viewProps.title.c_str()];
+        }
+    }
+    
+    menu = [UIMenu menuWithTitle:menuTitle children:actions];
     _menuButton.menu = menu;
 }
</code_context>

<issue_to_address>
**issue (bug_risk):** Menu title changes won’t trigger a menu rebuild, so updated titles may not appear.

The menu title is taken from `viewProps.title` only when the menu is created, but `menuItemsChanged` only reacts to item-level changes. If only `title` changes, `_menuButton.menu` is not rebuilt and the new title is never shown. Please either incorporate `viewProps.title` into the change detection or explicitly trigger a rebuild when the title changes.
</issue_to_address>

### Comment 2
<location> `android/src/main/java/com/menu/MenuView.kt:256` </location>
<code_context>
+                    LinearLayout.LayoutParams.MATCH_PARENT,
+                    (0.5 * context.resources.displayMetrics.density).toInt()
+                )
+                setBackgroundColor(if (themeVariant == "dark") Color.parseColor("#38383A") else Color.parseColor("#E0E0E0"))
+            }
+            container.addView(separator)
</code_context>

<issue_to_address>
**suggestion:** Divider color ignores system dark mode when `themeVariant` is `system`, causing a potential visual mismatch.

Here the separator color is based only on `themeVariant == "dark"`. When `themeVariant` is `system` and the device is in dark mode, `getBackgroundColor()` becomes dark but separators still use the light color, causing a mismatch. Consider deriving the divider color from `getBackgroundColor()`/`getTextColor()`, or reusing the same `uiMode` logic so system dark mode stays visually consistent.
</issue_to_address>

### Comment 3
<location> `android/src/main/java/com/menu/MenuView.kt:317-326` </location>
<code_context>
+            // Let's try the container approach where the RadioButton is the "checkmark" 
+            // and the whole row is clickable.
+            
+            val itemContainer = LinearLayout(context).apply {
+                orientation = LinearLayout.HORIZONTAL
+                layoutParams = LinearLayout.LayoutParams(
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Wrapping the RadioButton in a clickable container may affect accessibility and RadioGroup semantics.

Using the `LinearLayout` as the clickable target and disabling clicks on the `RadioButton` can break expected `RadioGroup` behavior and accessibility (focus handling, TalkBack announcements, etc.). Consider either forwarding `itemContainer` clicks to `radioButton.performClick()` so accessibility services still see a standard radio interaction, and/or giving `itemContainer` an appropriate accessibility role/label so the whole row is exposed as a single selectable option.
</issue_to_address>

### Comment 4
<location> `src/MenuViewNativeComponent.ts:21` </location>
<code_context>

 export interface NativeProps extends ViewProps {
+  title?: string;
+  themeVariant?: string;
   color?: string;
   checkedColor?: string;
</code_context>

<issue_to_address>
**suggestion:** Consider tightening the `themeVariant` type to the supported values instead of plain `string`.

On native platforms this is treated as `'light' | 'dark' | 'system'` with a default of `system`. Keeping it as `string` allows unsupported values that fall back unexpectedly. Consider defining `type ThemeVariant = 'light' | 'dark' | 'system';` and using `themeVariant?: ThemeVariant;` to enforce valid values at compile time.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@sbaiahmed1 sbaiahmed1 merged commit 2328c2d into main Dec 6, 2025
6 checks passed
@sbaiahmed1 sbaiahmed1 deleted the feat/add-dark-mode-support branch December 6, 2025 01:12
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.

1 participant