primitives icon indicating copy to clipboard operation
primitives copied to clipboard

[NavigationMenu] Option to open navigation menu on click instead of pointer enter

Open hannahcancode opened this issue 3 years ago • 30 comments

Feature request

Overview

The ability to optionally have the navigation menu trigger on click instead of on hover.

Who does this impact? Who is this for?

Would love for this to be a beginner friendly boolean prop, that turns on/off click in preference to hover.

For users building more complicated navigation menus, the ability to click to open the menu and have the menu stay open can provide a better UX, as they can move their mouse into the menu more easily, without the possibility of the menu closing accidentally. In our testing it also seems that some users will automatically go to click on the trigger and close the menu without realising.

As the button primitive already has an onClick prop being passed through it could be relatively straightforward to implement with a conditional.

Additional context

For example, vercel.com (logged out) mega menus open on click. Smashing magazine article on click vs hover: https://www.smashingmagazine.com/2021/05/frustrating-design-patterns-mega-dropdown-hover-menus/#designing-a-better-mega-dropdown-tap-click-menu

hannahcancode avatar Aug 26 '22 01:08 hannahcancode

Thanks for raising @hannahcancode, this is possible by preventing the relevant pointer events on trigger and content

https://codesandbox.io/s/navigation-menu-on-click-duwvgn?file=/App.js

however, this is not perfect as it relies on understanding the implementation details (which may change in the future).

I'll mark this as an improvement to explore.

andy-hook avatar Sep 05 '22 14:09 andy-hook

Oh that's great information, thanks @andy-hook. I did dive a little into whether I could "hack" it in such a way but as you say, I didn't have the knowledge of the implementation and failed. I'll see if that helps! Would love to see it as an enhancement 😁

hannahcancode avatar Sep 05 '22 23:09 hannahcancode

@andy-hook thank you so much for that code sample.

My use case: I have a horizontal menu that basically folds into an vertical accordion on smaller screens. The hover events really messed up the navigation on devices with small screens/window sizes that use an input device with hover.

Solved with something like:


export const preventHover = (event: any) => {
  const e = event as Event
  if (window.innerWidth < 1024) e.preventDefault()
}
…
<NavigationMenu.Trigger
  onPointerMove={preventHover}
  onPointerLeave={preventHover}
> 
…
<NavigationMenu.Content
  onPointerEnter={preventHover}
  onPointerLeave={preventHover}
>

elbotho avatar Sep 08 '22 18:09 elbotho

@andy-hook thank you so much for that code sample.

My use case: I have a horizontal menu that basically folds into an vertical accordion on smaller screens. The hover events really messed up the navigation on devices with small screens/window sizes that use an input device with hover.

Solved with something like:

export const preventHover = (event: any) => {
  const e = event as Event
  if (window.innerWidth < 1024) e.preventDefault()
}
…
<NavigationMenu.Trigger
  onPointerMove={preventHover}
  onPointerLeave={preventHover}
> 
…
<NavigationMenu.Content
  onPointerEnter={preventHover}
  onPointerLeave={preventHover}
>

Based on my testing, only onPointerLeave closes the menu so it's not necessary to preventHover for onPointerEnter. This should be sufficient

<NavigationMenu.Content
  onPointerLeave={preventHover}
>

longzheng avatar Mar 31 '23 04:03 longzheng

Hi @andy-hook! I tried using onPointerEnter and onPointerLeave and on fresh page load the navigation menu opens once with hover, then only open/close on click. How can I make it to only open on click so the behavior is consistent? Has something changed on the component behavior since this was discussed?

justkahdri avatar Apr 10 '23 04:04 justkahdri

Hey @justkahdri, I don't think anything has changed https://codesandbox.io/p/sandbox/pensive-shadow-j8m3lm

Can you provide a sandbox?

joaom00 avatar Apr 11 '23 02:04 joaom00

You're right, nothing has changed. I was using onPointerEnter instead of onPointerMove and that caused the weird behavior. Realized after reading the sandbox 😅 Thanks!

justkahdri avatar Apr 11 '23 18:04 justkahdri

Just a note, on vercel.com (which was mentioned in the OP), the "Features" menu is now opened both on hover and on click. I'm wondering if they did any sort of use study and determined this was better than opening the menu only on click.

multiwebinc avatar Apr 21 '23 20:04 multiwebinc

I've found that a lot of mouse users will instinctively click the menu triggers to open them, which will close the menu as their "hover" state has already triggered the menu to open.

Here is Theo exhibiting this behaviour a few months ago.

https://github.com/radix-ui/primitives/assets/17420741/829546c6-0d92-4587-9063-4ce0fb933db3

A hack is to create mutation observers which listen to the data-state attributes of the trigger elements and prevent any onClick events within a given interval.

https://codesandbox.io/p/sandbox/peaceful-nobel-gmlbyy?file=%2FApp.tsx (codesandbox typescript is weird so this example isnt fully typesafe)

While it's a bit messy this should give the best of both click and hover worlds. If anyone can foresee any problems with this let me know!

Cheers

JohnGemstone avatar May 12 '23 16:05 JohnGemstone

I've found that a lot of mouse users will instinctively click the menu triggers to open them

I find myself constantly doing that with this library.

multiwebinc avatar May 12 '23 19:05 multiwebinc

Hi all, how do you manually control the open and close state of the navigation menu in react? Do we just set the data-state attribute to open or closed on the NavigationMenu.Trigger component? It doesn't seem to be working for me at the moment.

jpizzle34 avatar Jun 01 '23 06:06 jpizzle34

Hi all, how do you manually control the open and close state of the navigation menu in react? Do we just set the data-state attribute to open or closed on the NavigationMenu.Trigger component? It doesn't seem to be working for me at the moment.

oh nvm, i found the forceMount prop

jpizzle34 avatar Jun 01 '23 10:06 jpizzle34

Hi all, how do you manually control the open and close state of the navigation menu in react? Do we just set the data-state attribute to open or closed on the NavigationMenu.Trigger component? It doesn't seem to be working for me at the moment.

oh nvm, i found the forceMount prop

forceMount isn't how you control the open state (it's there for JS animation purpose). To control the open state, you use value and onValueChange.

benoitgrelard avatar Jun 01 '23 10:06 benoitgrelard

@jpizzle34 @benoitgrelard can you provide an example to force an open state on a navigation item?

coreybruyere avatar Oct 30 '23 23:10 coreybruyere

@jpizzle34 @benoitgrelard can you provide an example to force an open state on a navigation item?

<NavigationMenu.Root defaultValue="some-value-item">
  ...
</NavigationMenu.Root>

// or

const [value, setValue] = React.useState('some-value-item')

<NavigationMenu.Root value={value} onValueChange={setValue}>
  ...
</NavigationMenu.Root>

joaom00 avatar Oct 30 '23 23:10 joaom00

So there is no official way to make the menu open on the click instead of horrible hover?

AurelianSpodarec avatar Dec 13 '23 11:12 AurelianSpodarec

Thanks for the workarounds!

I've also had to override onPointer events on the NavigationMenu.Viewport component to achieve the desired results.

GuiSim avatar Dec 13 '23 17:12 GuiSim

Not sure why this still isn't an option without workarounds. Appreciate the tips! 👍

I tried adding these props:

onPointerEnter={preventHover}
onPointerLeave={preventHover}

On my Trigger, Content and Viewport but it still triggers the menu unfortunately. Oddly it only triggers it on the first event. If I close the menu, it will no longer open on hover.

jwmann avatar Jan 29 '24 21:01 jwmann

@jwmann by now I prevent default the following:

  • on Trigger: onPointerEnter, onPointerLeave and onPointerMove
  • on Content: onPointerEnter, onPointerLeave

The onPointerMove could make the difference. Good luck 🤞

elbotho avatar Jan 30 '24 22:01 elbotho

@elbotho That definitely made the difference for me! Cheers 👍

jwmann avatar Feb 02 '24 14:02 jwmann

I you as me are using a viewport element outside of the regular "flow", also add these handlers over there:

<NavigationMenu.Viewport
    onPointerEnter={event => event.preventDefault()}
    onPointerLeave={event => event.preventDefault()}
/>

Jdruwe avatar Feb 12 '24 20:02 Jdruwe

Thanks for the workarounds!

I've also had to override onPointer events on the NavigationMenu.Viewport component to achieve the desired results.

how? Can you point us to it?

I you as me are using a viewport element outside of the regular "flow", also add these handlers over there:

<NavigationMenu.Viewport
    onPointerEnter={event => event.preventDefault()}
    onPointerLeave={event => event.preventDefault()}
/>

What os the regular "flow"?

SalahAdDin avatar Mar 09 '24 04:03 SalahAdDin

@SalahAdDin

Like this

<NavigationMenu.Viewport
    onPointerEnter={event => event.preventDefault()}
    onPointerLeave={event => event.preventDefault()}
/>

GuiSim avatar Mar 09 '24 04:03 GuiSim

Glad this got added in thanks for the help!!

EastTexasElectronics avatar May 13 '24 16:05 EastTexasElectronics

Hi @andy-hook! I tried using onPointerEnter and onPointerLeave and on fresh page load the navigation menu opens once with hover, then only open/close on click. How can I make it to only open on click so the behavior is consistent? Has something changed on the component behavior since this was discussed?

This worked for me, hope this gets added as a prop (disableHover) or something like that

AChangXD avatar Jun 03 '24 01:06 AChangXD

<NavigationMenu.Root value={value} onValueChange={setValue}>

Can anyone explain this further? It seems like you should be able to pass in an isOpen prop, but the value prop only accepts strings...do I pass in "closed"/"open"?

kdevay avatar Jun 11 '24 22:06 kdevay

Hey @kdevay, It looks like you're trying to pass isOpen as a boolean to the value prop, correct? Unfortunately, this won't work with this component. The value prop requires a string because the NavigationMenu can contain multiple items, and each needs a unique string identifier to determine which item to display.

I created a codesandbox demo hope this helps.

daprieu avatar Jun 12 '24 21:06 daprieu