microsoft / react-native-macos

A framework for building native macOS apps with React.
https://microsoft.github.io/react-native-windows/
MIT License
3.39k stars 130 forks source link

Right click context menu #2085

Closed AdrianFahrbach closed 2 months ago

AdrianFahrbach commented 5 months ago

Summary

The deprecated react-native-macos package from ptmt has the option to add a custom context menu to a view: https://github.com/ptmt/react-native-macos/issues/237

It basically works like this:

const contextMenu = [
  { key: 'foo', title: 'Foo' },
  { isSeparator: true },
  { key: 'bar', title: 'Bar' },
]

<View
  contextMenu={contextMenu}
  onContextMenuItemClick={event => {
    console.log(event.nativeEvent)
  }} />

Maybe porting that feature to this package is a viable option.

Motivation

The context menu would give an easy way to hide more complex functions.

In my use case I got a list of workload entries from Jira. All of those entries can be edited and delete through a separate edit screen but that requires a bit of navigation. "Pro" users could access shortcuts to specific functions (like "delete entry") through that right click menu. Other useful actions that aren't important enough for a separate button could also be hidden there. In my example that could be something like "Copy issue id" or "Open issue in browser".

Basic Example

No response

Open Questions

Porting the code from the other packages seems like a nice shortcut to a useful feature for me. I have no idea if this is possible though, so this should probably be checked first. If porting the code is not an option, then other features may be more important for now.

Saadnajmi commented 5 months ago

I've been meaning to add support for macOS to https://github.com/react-native-menu/menu but haven't gotten around to it. Meanwhile, I ended up writing a fully custom replacement for NSMenu with FluentUI React Native Menu. That one had to be fully custom (A custom NSWindow holding RN Views instead of just rendering a native NSMenu) because we wanted more customizable menu items (I.E, you can put any view in there instead of just text). That might be worth checking out. The spec page I linked doesn't have macOS screenshots but I encourage you to try the test app out

Saadnajmi commented 5 months ago

I think when I've thought about this before, I've resisted the approach of just adding props to <View> because menus can get secretly complex when you add in images, checkboxes, multi-select, submenus, etc. Ultimately I think a "wrapper "component (like @react-native-menu/menu) or a component that lets you write up the menu in JSX (like FluentUI React Native's Menu) is the better extensible approach.

AdrianFahrbach commented 5 months ago

I tried the FluentUI React Native Menu and can't get it to work. I also tested this on windows and force opening the menu with <Menu open> but it just doesn't show up. Maybe the theme provider is mandatory?

https://github.com/microsoft/react-native-macos/assets/45072099/0c187be9-95e2-4cfd-a995-76d6a157e0da

Ultimately I think a "wrapper "component (like @react-native-menu/menu) or a component that lets you write up the menu in JSX (like FluentUI React Native's Menu) is the better extensible approach.

My initial thought was a separate library as well, so I was actually looking for something like your FluentUI menu. What I really like about the View prop approach though is its simplicity. I didn't test it, but it sounds like that way I also don't have to worry about making it feel 100% native and/or placing the popup in the right location. I also don't need to handle the open/close state or nest multiple components, I can just throw an object into that prop. I'm going for a very native look & feel though, so in my case I don't want to place any custom views inside my menu.

Saadnajmi commented 4 months ago

@AdrianFahrbach there's another option you could use for a native NSmenu: ActionSheetIOS. We actually ported that to fire an NSMenu. I found an old internal component that gives you an idea of how it could work:

'use strict';
import * as React from 'react';
import { findNodeHandle, ActionSheetIOSOptions, ActionSheetIOS } from 'react-native';
import { IContextualMenuStatic } from '../../common/ContextualMenu/ContextualMenu.types';
import { IContextualMenuProps } from '../../common/ContextualMenu/ContextualMenu.Props';

declare module 'react-native' {
  // The TypeScript type definition at
  // https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-native
  // is missing anchor on ActionSheetIOSOptions.
  // Use declaration merging to add it here.
  interface ActionSheetIOSOptions {
    anchor?: number;
  }
}

class ContextualMenuImpl extends React.Component<IContextualMenuProps, {}> {
  public static showContextualMenu(menu: IContextualMenuProps, target: React.ReactNode) {
    const buttonTitles: Array<string> = [];

    for (const button of menu.items) {
      buttonTitles.push(button.name);
    }

    /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
    const reactTag = findNodeHandle(target as any);
    const options: ActionSheetIOSOptions = {
      options: buttonTitles,
      cancelButtonIndex: menu.cancelButtonIndex,
      destructiveButtonIndex: menu.destructiveButtonIndex,
      anchor: reactTag
    };

    ActionSheetIOS.showActionSheetWithOptions(options, buttonIndex => {
      menu.items[buttonIndex].onClick({ key: menu.items[buttonIndex].key });
    });
  }
}

export const ContextualMenu: IContextualMenuStatic = ContextualMenuImpl;

Note that while findNodeHandle works in Fabric, it's not recommended, and we're still looking for a good alternative.

AdrianFahrbach commented 2 months ago

This works perfectly! If anyone reading this needs a stripped down version:

import { ActionSheetIOS, ActionSheetIOSOptions, GestureResponderEvent } from 'react-native';

interface MenuItem {
  name: string;
  onClick: () => void;
}

export function isRightClick(e: GestureResponderEvent) {
  return (e.nativeEvent as any).button === 2;
}

export function showContextualMenu(menuItems: MenuItem[], target: React.ReactNode) {
  const options: ActionSheetIOSOptions = {
    options: menuItems.map(item => item.name),
    anchor: target ? findNodeHandle(target as any) ?? undefined : undefined,
  };

  ActionSheetIOS.showActionSheetWithOptions(options, buttonIndex => {
    menuItems[buttonIndex].onClick();
  });
}

I think this issue can therefore be closed. It would be nice to have a list of things that have been ported though. Maybe that is something to add to the documentation in the future?