radix-ui / primitives

Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos.
https://radix-ui.com/primitives
MIT License
14.86k stars 722 forks source link

[ContextMenu] Add ability to control #1307

Open benoitgrelard opened 2 years ago

benoitgrelard commented 2 years ago

A few users have needed to control the ContextMenu in some scenarios (typically canvas based apps) so it would be good if we could provide a way to do so.

mrudowski commented 2 years ago

i think this approach is very handy - just a anchorPoint to work with onShapeContextMenu

Andarist commented 1 year ago

What kind of control are we talking about here? Just the open state? Or something else too? My use case is that I'd like to conditionally disable the context menu - which could be faked through open={isDisabled ? false : undefined} (as long as the component would handle switching between controlled and uncontrolled "modes" gracefully)

benoitgrelard commented 1 year ago

What kind of control are we talking about here? Just the open state?

This issue is about controlling the open state/position.

My use case is that I'd like to conditionally disable the context menu - which could be faked through open={isDisabled ? false : undefined} (as long as the component would handle switching between controlled and uncontrolled "modes" gracefully)

This sounds like a good case for adding a disabled prop maybe? That should be fairly trivial I think. Wanna create a separate issue @Andarist?

Andarist commented 1 year ago

Sure, I've created a feature request here: https://github.com/radix-ui/primitives/issues/1745

paperstick commented 1 year ago

Made a PR which adds a disabled prop to <ContextMenu /> trigger and data-disabled prop which could be helpful in case of custom styling of the trigger area. @Andarist, I hope that this will help in your case!

dbrxnds commented 1 year ago

Our case may be a little odd, but still wanting to put it out there.

We have a table where users can open a context menu on each row, but we also still have an actions column for our "less technical" users to allow them to open that same menu by clicking an icon. This is currently not possible without some workarounds / reuse of code between these two things

c8se commented 11 months ago

I want to open context menu by keyboard shortcut in my case, and as mentioned by @dbrxnds , It would be nice if I can reuse the same menu between a dropdown menu and a context menu. Maybe the Menu component inside this project can be exposed as a separated component?

siyao-polarr commented 10 months ago

Same here. We want to support opening context menu on right click but also when user taps a "..." button on the item in case they can't figure out the right click

canadaduane commented 10 months ago

A short-term hack:

document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))

This will trigger the DismissibleLayer's onDismiss function, which will close the ContextMenu.

AlbaOngaro commented 10 months ago

As a way to open the context menu when clicking on another element I came up with something similar:

trigger.current.dispatchEvent(
  new MouseEvent("contextmenu", {
    bubbles: true,
    clientX: button.current.getBoundingClientRect().x,
    clientY: button.current.getBoundingClientRect().y,
  }),
);

where trigger is the ContextMenu.Trigger element, and button is the element that should trigger the context menu on click.

flbn commented 8 months ago

Our case may be a little odd, but still wanting to put it out there.

We have a table where users can open a context menu on each row, but we also still have an actions column for our "less technical" users to allow them to open that same menu by clicking an icon. This is currently not possible without some workarounds / reuse of code between these two things

no literally same issue here, we can't have this table rendering a menu for each cell. should be on a row level...

misha-erm commented 8 months ago

no literally same issue here, we can't have this table rendering a menu for each cell. should be on a row level...

Just wrap the whole table with ContextMenu and render content depending on a selected cell. This sandbox might give you an idea how to achieve that

flbn commented 8 months ago

Just wrap the whole table with ContextMenu and render content depending on a selected cell. This sandbox might give you an idea how to achieve that

the issue isn't really that, cells will be rendering different context menus depending on the content inside. it's... weird, but i think alba's comment might be the workaround i reach for. or maybe moving the root of the menu up and keeping each cell as a trigger. would be much easier with a primitive like open in <Popover/>

edit: ahh yeah, i've seen you've done what i was thinking of and moved the root up. cheers :)

piszczu4 commented 4 months ago

Would be nice to can use event to customize or prevent showing custom context menu

ben-xD commented 2 months ago

Solution for canvas-based apps

TLDR: Just wrap your map in ContextMenuTrigger

I thought toggling open/close state imperatively was an instrumental feature. But after getting stuck with issues styling @szhsin/react-menu consistently with other shadcn/ui components using tailwind, I tried the shadcn/radix-ui Context Menu again.

I was dealing with a canvas/map based application. I use maplibre-gl-js, deck.gl and nebula.gl for mapping, and shadcn/ui/radix-ui for components. Turns out, I just needed to wrap the entire map in a <ContextMenuTrigger>. Everything else works really nicely.

Much simpler than @szhsin/react-menu

This is a lot simpler than my original code using @szhsin/react-menu, which involved using the boundingBoxRef and `anchorPoint props, storing pointer positions, and calculating workarounds/offsets for wrong menu position. I needed to use.getCanvas()?.getBoundingClientRect()` and change the anchor points. For example:

  const containerRef = useRef<HTMLDivElement | null>(null);
  const [contextMenuAnchorPoint, setContextMenuAnchorPoint] = useState<{
    x: number;
    y: number;
  }>({ x: 0, y: 0 });
  const deckGlRef = useRef<DeckGLRef>(null);
  const canvasPosition = deckGlRef.current?.deck
    ?.getCanvas()
    ?.getBoundingClientRect();
  const panelCorrectedContextMenuAnchorPoint: typeof contextMenuAnchorPoint = {
    x: contextMenuAnchorPoint.x + (canvasPosition?.x ?? 0),
    y: contextMenuAnchorPoint.y + (canvasPosition?.y ?? 0),
  };

// In a callback e.g. onClick from the map library, I call `setContextMenuAnchorPoint`

return (
    <div
        ref={containerRef}
        ...

In both radix and szhsin/react-menu, I still have a contextMenuMode state which tells what context menu to show. e.g. If the user right clicks a cat, ill setContextMenuMode("cat"), and my ContextMenu component will conditionally render (e.g. disable/hide options).

Just sharing my thoughts with anyone who might need it after a good few hours spent on this :).

lui7henrique commented 1 month ago

UP! 🆙

Extremely necessary! I'm building a context-menu with options and I want it to work on mobile and just by controlling the opening state I can make it work.

image

aroc commented 2 weeks ago

The solution from @AlbaOngaro works perfectly! Would still be good to have official support but not a blocker given how easy that solution is. Could be good to include that approach in the docs.

kunal-singh commented 2 weeks ago

I was struggling to display the context menu over a canvas(react force graph in my case). Thanks to the answers above I was able to solve it. Here is a barebones working example on how to do the same. Hopefully it would be of help to someone stuck in a similar problem.