Closed employee451 closed 2 years ago
While I agree that there should be a way to do this with less boilerplate, I looked at the docs and found one way to do it without having to wait for that feature:
You can omit the open
destructuring, as this variable is internally tracked by the Menu component and you basically just ignore it and have full control of when you want your Menu to open/close based on customOpen
.
function MyDropdown() {
const [display, setDisplay] = useState('display here');
const [customOpen, setCustomOpen] = useState(false);
function buttonClicked() {
setCustomOpen(prev => !prev);
}
return (
<>
<Menu>
{({open}) => (
<>
<Menu.Button onClick={buttonClicked}>More</Menu.Button>
{customOpen && (
<Menu.Items static>
<Menu.Item>
{({ active }) => (
<a className={`${active && 'bg-blue-500'}`} onClick={() => setDisplay('Account Settings')}>
Account settings
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a className={`${active && 'bg-blue-500'}`} onClick={() => setDisplay('Documentation')}>
Documentation
</a>
)}
</Menu.Item>
<Menu.Item disabled>
<span className="opacity-75">Invite a friend (coming soon!)</span>
</Menu.Item>
</Menu.Items>)
}
</>
)
}
</Menu>
<br/><br/>
<div>{display} was clicked</div>
</>
)
}
Thanks @bytehala. This is what I'll have to do for now. The annoying thing about doing it that way is that I now have to implement that the menu closes on 'escape' press, when you click outside the menu, etc. myself.
the click outside is the worst, i'm strugling trying implement it myself. no luck, a lot of bugs
@bytehala Is there a good way to use something like this to allow adding form fields / text areas / submit button to Menu Items like this?
I am clearly in over my head in understanding how this might work.
Hey! Thank you for your suggestion! Much appreciated! 🙏
@employee451 could you talk more about this use case you have? A Menu
typically contains items that let you invoke some action or let you navigate to another page. Think about it as your native OS menu bar. As far as I know, they all close the menu once the "action" is invoked. Here is a screenshot of the macOS Menu for example:
The reason that I ask about your use case is because I'm trying to understand what you try to achieve. Because initially I am thinking about that you might be "abusing" the Menu
for a different use case than it is intended for.
Of course!🙏 @RobinMalfait
One use case where this functionality would be useful is when the "action" is asynchronous, e.g. you have to wait for an API call to complete before closing the menu.
In my particular use case, the Menu
acts as an option menu for a calendar event. One of the options in the menu should cancel the event. When this option is selected, I would like to show a loading spinner on the menu item, then a check icon once the action has been completed. Currently, I'm showing the loading state elsewhere in the UI.
I am running into a similar situation and while <Disclosure />
mostly fits the bill, there is one thing lacking: a way to detect an outside click and the escape key being pressed.
Feel free to chip in but I think having access to this would solve this issue!
react-use has useClickAway and useKey but it'd be nice to have it integrated here!
@RobinMalfait great job on this library. I really like it :)
I have exactly the same problem that the menu doesn't close automatically with the <Link></Link>
tag from NextJS. When I want to have this behavior I would have to implement the open/close functionality again and clicking outside detection is not so easy :laughing:
Do you know why the menu doesn't close with NextJS? Could it be that when we have client side routing, that the closing mechanism doesn't work 100%?
@Mad-Kat That's an issue with next/link
, checkout: https://github.com/tailwindlabs/headlessui/issues/120#issuecomment-717174190
I think there is the same issue with Popover
. I would expect when users click on one of the items inside Popover.Panel
, then the panel will close.
I think there is the same issue with
Popover
. I would expect when users click on one of the items insidePopover.Panel
, then the panel will close.
This issue is about the opposite problem, as the Menu component is closing in situations where we might not want it to.
As far as I know, they all close the menu once the "action" is invoked. Here is a screenshot of the macOS Menu for example:
One use case if you have a Switch
button in your menu, then you'd want the user to be able to see the toggle go through before closing.
@bytehala's solution didn't completely work for me – I had to expand it slightly by observing the change to the internal open state (for my complex layout, opening the menu didn't work reliably) by using a useEffect
function MyDropdown() {
const [open, setOpen] = useState(true);
return (
<Menu>
{( { open: internalOpen }) => {
useEffect(() => {
if (internalOpen && !open) {
setOpen(true)
}
}, [internalOpen])
return (
<Menu.Button onClick={buttonClicked}>More</Menu.Button>
{customOpen && (
<Menu.Items static>
<Menu.Item>
{({ active }) => (
<a className={`${active && 'bg-blue-500'}`} onClick={() => setDisplay('Account Settings')}>
Account settings
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a className={`${active && 'bg-blue-500'}`} onClick={() => setDisplay('Documentation')}>
Documentation
</a>
)}
</Menu.Item>
<Menu.Item disabled>
<span className="opacity-75">Invite a friend (coming soon!)</span>
</Menu.Item>
</Menu.Items>)
)
}
If you have many internal divs, you may also need to toggle the open state on those if the events don't bubble up.
For anyone suffering to keep the dropdown opened if an item clicked, here is my workaround:
const [menuOpened, setMenuOpened] = useState(false)
const CustomMenuButton = function ({ children }) {
return <button onClick={() => setMenuOpened(!menuOpened)}>{children}</button>
}
function myComponent () {
return (
<Menu>
<Menu.Button as={CustomMenuButton}>Button</Menu.Button>
<Menu.Items static>
{menuOpened && (
<Menu.Item>This is item<MenuItem>
)}
</Menu.Items>
</Menu>
)
}
TL;DR: just create a custom component for button and do what ever you want (and ofc set static to true for Menu.Items
)
I think what makes more sense is to invoke event.stopProgatation()
from within the Menu.Item
component. In this way, the dropdown still closes when clicking outside the dialog and there's no need to reimplement any logic.
When this is going to be fixed ? It's really annoying to do so much just to stop closing the menu there should be a prop 😞
For anyone suffering to keep the dropdown opened if an item clicked, here is my workaround:
const [menuOpened, setMenuOpened] = useState(false) const CustomMenuButton = function ({ children }) { return <button onClick={() => setMenuOpened(!menuOpened)}>{children}</button> } function myComponent () { return ( <Menu> <Menu.Button as={CustomMenuButton}>Button</Menu.Button> <Menu.Items static> {menuOpened && ( <Menu.Item>This is item<MenuItem> )} </Menu.Items> </Menu> ) }
TL;DR: just create a custom component for button and do what ever you want (and ofc set static to true for
Menu.Items
)
The this will throw a forward ref error
Wrap it in forwardRef
const CustomMenuButton = forwardRef(({ children }, ref) => (
<button onClick={() => setMenuOpened(!menuOpened)} ref={ref}>
{children}
</button>
));
I've found that the popover element https://headlessui.dev/react/popover provides a close()
prop that you need to invoke manually otherwise the menu doesn't close. Perfect to my use case. I resorted to that and got rid of the dropdown.
@luciodale can you share some boiler code on how u implemented it on menu click and click away?
If anyone encounters this today in Vue, here's a template to get you started on a "Dropdown" popover:
<template>
<Popover class="relative">
<PopoverButton class="inline-flex items-center font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 rounded-md shadow-sm text-gray-700 border border-gray-300 bg-white hover:bg-gray-50 focus:ring-blue-500">
Dropdown Title
<ChevronDownIcon class="w-5 h-5 ml-2" />
</PopoverButton>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-1 opacity-0"
>
<PopoverPanel
class="absolute w-56 py-1 mt-2 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<slot />
</PopoverPanel>
</transition>
</Popover>
</template>
<script>
import { ChevronDownIcon } from '@heroicons/vue/solid';
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue';
export default {
components: { Popover, PopoverButton, PopoverPanel, ChevronDownIcon },
};
</script>
I just ran into the same problem with Menu
and had to read the docs very carefully. The solution is pretty easy!
In React, I had this problem when rendering a <Link to="/foo/bar/" ... />
element. The page transition did happen, but the menu did not close.
The fix was to use the <Menu.Item as={Link} to="/foo/bar" ... />
, as this handles the click event properly.
This should work for next.js navigation as well. Not sure about vue, though.
Hey! Thank you for your suggestion! Much appreciated! 🙏
@employee451 could you talk more about this use case you have? A
Menu
typically contains items that let you invoke some action or let you navigate to another page. Think about it as your native OS menu bar. As far as I know, they all close the menu once the "action" is invoked. Here is a screenshot of the macOS Menu for example:The reason that I ask about your use case is because I'm trying to understand what you try to achieve. Because initially I am thinking about that you might be "abusing" the
Menu
for a different use case than it is intended for.
Hm, one question related to that image is how would you build that "share" submenu? Since right now, when you press enter, it closes the menu; but for the submenu, you'd expect it to open a second dropdown menu on the right.
I would also need this feature in Headless UI. My use case is simple: I have a menu item which copies a link to clipboard. When it is clicked, the menu needs to stay open because the menu item text changes to "Link Copied!" for a few seconds. This provides a visual feedback to users so they know that the action has been successfully executed.
So what I really need is a prop on a menu item which would control whether the menu should be closed when the menu item is clicked. I have tried to use event.stopPropagation()
or event.preventDefault()
as a workaround but the menu is still being closed on click.
Kinda gross, but here's a workaround. Use a ref to the Menu.Button and force a click event to reopen it immediately afterwards (in a timeout so it is queued up immediately after the current event is handled)
import { Menu } from "@headlessui/react";
import { useRef } from "react";
export const MenuThatDoesntClose = () => {
const ref = useRef(null);
return (
<Menu>
<Menu.Button ref={ref}> More</Menu.Button>
<Menu.Items>
<Menu.Item
as={"button"}
onClick={() => {
setTimeout(() => {
ref.current?.click();
}, 0);
}}
>
will not close
</Menu.Item>
<Menu.Item as={"button"}>Will close</Menu.Item>
</Menu.Items>
</Menu>
);
};
Hi there! I have a use case for the Menu component where I don't necessarily want the Menu to close when I click on a Menu.Item. Or, in other words, I would like to be able to control when the menu closes in some way.