Control whether Menu closes on Menu.Item click #1122
Replies: 38 comments 7 replies
-
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 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>
</>
)
} |
Beta Was this translation helpful? Give feedback.
-
Thanks @bytehala. This is what I'll have to do for now. |
Beta Was this translation helpful? Give feedback.
-
the click outside is the worst, i'm strugling trying implement it myself. no luck, a lot of bugs |
Beta Was this translation helpful? Give feedback.
-
@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. |
Beta Was this translation helpful? Give feedback.
-
Hey! Thank you for your suggestion! @employee451 could you talk more about this use case you have? A 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 |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
I am running into a similar situation and while 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! |
Beta Was this translation helpful? Give feedback.
-
@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 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%? |
Beta Was this translation helpful? Give feedback.
-
@Mad-Kat That's an issue with |
Beta Was this translation helpful? Give feedback.
-
I think there is the same issue with |
Beta Was this translation helpful? Give feedback.
-
This issue is about the opposite problem, as the Menu component is closing in situations where we might not want it to. |
Beta Was this translation helpful? Give feedback.
-
One use case if you have a |
Beta Was this translation helpful? Give feedback.
-
@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
If you have many internal divs, you may also need to toggle the open state on those if the events don't bubble up. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
I think what makes more sense is to invoke |
Beta Was this translation helpful? Give feedback.
-
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>
);
}; |
Beta Was this translation helpful? Give feedback.
-
Hey all! I've moved this to a discussion instead because while we don't have time to look at it right now, we will look at it in the future. Once we dedicate some time to this problem we will get back to this discussion. |
Beta Was this translation helpful? Give feedback.
-
As an alternative, https://www.radix-ui.com/docs/primitives/components/dropdown-menu#item allows developers to prevent the default behavior of closing the menu on select via
(hover over the 🛈)
|
Beta Was this translation helpful? Give feedback.
-
Here https://github.com/tailwindlabs/headlessui/blob/main/packages/%40headlessui-react/src/components/menu/menu.tsx#L612 is a check for the disabled prop. If present, it will not close the menu. Maybe we can supply react prop "closeMenuOnClick=trueOrFalse" to the Menu.Item to return there as well? |
Beta Was this translation helpful? Give feedback.
-
@livthomas, I have a similar problem. My menu has items like |
Beta Was this translation helpful? Give feedback.
-
a more complete example handling keyboard events as well. Way too hacky for my liking. import { Menu } from "@headlessui/react";
import { useRef } from "react";
export const MenuWithAsyncActions = () => {
const refMenuButton = useRef<HTMLButtonElement>(null);
const refIsOpen = useRef<boolean>();
const refActiveItemId = useRef<string>();
function closeMenu() {
if (refIsOpen.current) refMenuButton.current?.click();
}
async function apiCall() {
// async logic
// if (error) maybe not close the menu
closeMenu();
}
return (
<Menu>
{({ open }) => {
refIsOpen.current = open;
return (
<>
<Menu.Button ref={refMenuButton}> More</Menu.Button>
<Menu.Items
onKeyDown={(event: any) => {
if (event.key === "Enter" || event.key === "Space") {
if (refActiveItemId.current === "asyncItem") {
event.preventDefault(); // prevent menu close on keyboard select
void apiCall();
}
}
}}
>
<Menu.Item
as={"button"}
onClick={(event: any) => {
event.preventDefault();
void apiCall();
}}
>
{({ active }) => {
// hacky workaround for Menu not exposing in any other way the item active status on keyboard navigation
if (active) refActiveItemId.current = "asyncItem";
return <span>Async Menu Item</span>;
}}
</Menu.Item>
<Menu.Item as={"button"}>
{({ active }) => {
if (active) refActiveItemId.current = "syncItem";
return <span>Sync Menu Item</span>;
}}
</Menu.Item>
</Menu.Items>
</>
);
}}
</Menu>
);
}; |
Beta Was this translation helpful? Give feedback.
-
So, here is a pretty nasty hack, inspired by this comment (above).
import { Menu } from "@headlessui/react";
export const MenuThatDoesntClose = () => {
return (
<Menu>
<Menu.Button>More</Menu.Button>
<Menu.Items>
{/* doesn't close on click - but click away still works ;) */}
<Menu.Item as="button" closeOnSelect={false}>
will not close
</Menu.Item>
{/* does close on click */}
<Menu.Item as="button">Will close</Menu.Item>
</Menu.Items>
</Menu>
);
}; The nice thing about this solution is that there are no refs, timeout hacks, open/re-open flashes, extra state, etc. just a simple boolean prop to set. On the flip side, this is clearly a bit brittle and this little trick might break on new releases ... |
Beta Was this translation helpful? Give feedback.
-
here is my workaround for those who wonder. <template>
<Menu v-slot="{ open }" as="div" class="relative inline-block text-left">
<MenuButton @click="isMenuOpen = !isMenuOpen">
<slot :open="isStatic ? isMenuOpen : open" name="button">
<n-button
class="h-8 flex items-center md:w-auto w-full"
size="sm"
variant="white"
v-bind="buttonProps">
<span class="flex items-center gap-2">
{{ buttonProps.label }}
<ChevronDown v-if="iconEnabled" :class="open ? 'rotate-180' : 'rotate-0'" class="h-3 w-3 transition-transform" />
</span>
</n-button>
</slot>
</MenuButton>
<Transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0">
<div v-if="isStatic ? isMenuOpen : open">
<MenuItems
:static="isStatic"
class="absolute z-30 p-4 left-0 mt-2 min-w-[130px] origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<slot name="items" />
</MenuItems>
</div>
</Transition>
</Menu>
</template>
<script setup>
import { Menu, MenuButton, MenuItems } from '@headlessui/vue';
import ChevronDown from '@assets/icons/ui/chevron-down.svg';
import { ref } from 'vue';
const isMenuOpen = ref(false);
defineProps({
buttonProps: {
type: Object,
default: () => ({})
},
iconEnabled: {
type: Boolean,
default: true
},
isStatic: {
type: Boolean,
default: false
}
})
</script> |
Beta Was this translation helpful? Give feedback.
-
You can create a wrapper around Something like: import { Menu } from '@headlessui/react';
const Component = ({
children,
onClick,
closeOnClick = false,
}) => {
return (
<Menu.Item
onClick={(e) => {
if (onClick) onClick();
if (!closeOnClick) {
e.preventDefault();
e.stopPropagation();
}
}}
>
{children}
</Menu.Item>
}
export default Component; |
Beta Was this translation helpful? Give feedback.
-
This is what I did to solve this issue. Not the cleanest because you have to use some vanilla JS to find the active item and add a prop to both the menu and the menu items but seems reasonably clean. I would still prefer to expose an Below is a simplified example but should give folks the general idea 🤞 // keyboard.ts
// From https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
export const KEY_VALUES = {
Space: " ",
Enter: "Enter",
Escape: "Escape",
Backspace: "Backspace",
Delete: "Delete",
ArrowLeft: "ArrowLeft",
ArrowUp: "ArrowUp",
ArrowRight: "ArrowRight",
ArrowDown: "ArrowDown",
Home: "Home",
End: "End",
PageUp: "PageUp",
PageDown: "PageDown",
Tab: "Tab",
}; // Dropdown.tsx
const Dropdown = ({
preventCloseWhenItemSelected = false,
children
}) => {
const menuItemsRef = useRef<HTMLDivElement>(null);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (!preventCloseWhenItemSelected) {
return;
}
if (event.key === KEY_VALUES.Enter) {
const activeItemId = menuItemsRef?.current?.getAttribute(
"aria-activedescendant"
);
if (!activeItemId) {
return;
}
const activeItem = document.getElementById(activeItemId);
if (!activeItem) {
return;
}
event.preventDefault();
event.stopPropagation();
activeItem.click();
}
};
return (
<Menu as="div">
<Menu.Button>Open</Menu.Button>
<Menu.Items
ref={menuItemsRef}
onKeyDown={handleKeyDown}
>
{children}
</Menu.Items>
</Menu>
);
}; // DropdownItem.tsx
const DropdownItem = (
{
preventCloseWhenSelected = false,
children,
},
) => {
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
if (!preventCloseWhenSelected) {
return;
}
event.preventDefault();
event.stopPropagation();
};
return (
<Menu.Item>
<button onClick={handleClick}>{children}</button>
</Menu.Item>
);
}; Example usage <Dropdown preventCloseWhenItemSelected>
<DropdownItem preventCloseWhenSelected>Foo bar</DropdownItem>
</Dropdown> |
Beta Was this translation helpful? Give feedback.
-
Suppose you are looking for a menu panel with some other nest structures (divs, buttons, items, etc.), such as a calendar date picker. In that case, you can use a Disclosure and a Disclosure Panel with the static flag set within the Menu Items, and all of your dreams will come true. |
Beta Was this translation helpful? Give feedback.
-
Since 1.7.4, The Menu component exposes a |
Beta Was this translation helpful? Give feedback.
-
close the menu after click:
|
Beta Was this translation helpful? Give feedback.
-
This Worked for me using a useRef and triggering the menu button event and it closes the menu function ToolMenu() {
const buttonARef = useRef(null);
const handleButtonClickB = () => {
if (buttonARef.current) {
buttonARef.current.click();
}
}
return (
<div>
<Menu as="div" className="inline-block text-left">
<div>
<Menu.Button ref={buttonARef} className="link flex items-center gap-[2px] ">
<CgMenuGridR size={24}/>
<p className=" hidden sm:block">Tools</p>
</Menu.Button>
</div>
<Menu.Items className="absolute right-0 top-[56px] bg-white w-full border z-20">
<div className=" p-6">
<div onClick={handleButtonClickB}className="max-w-7xl mx-auto ">
<Tools />
</div>
</div>
</Menu.Items>
</Menu>
</div>
)
} |
Beta Was this translation helpful? Give feedback.
-
To prevent the click action from closing the dropdown, as others have suggested, you can apply a preventDefault() on the MenuItem click event. In Vue, I apply the @click.prevent directly to the MenuItem as follows:
|
Beta Was this translation helpful? Give feedback.
-
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.
Beta Was this translation helpful? Give feedback.
All reactions