diff --git a/packages/@headlessui-vue/src/components/listbox/listbox.ts b/packages/@headlessui-vue/src/components/listbox/listbox.ts index b1421ff9c2..ce9659ed85 100644 --- a/packages/@headlessui-vue/src/components/listbox/listbox.ts +++ b/packages/@headlessui-vue/src/components/listbox/listbox.ts @@ -20,6 +20,7 @@ import { useId } from '../../hooks/use-id' import { Keys } from '../../keyboard' import { calculateActiveIndex, Focus } from '../../utils/calculate-active-index' import { resolvePropValue } from '../../utils/resolve-prop-value' +import { dom } from '../../utils/dom' enum ListboxStates { Open, @@ -184,11 +185,11 @@ export let Listbox = defineComponent({ let active = document.activeElement if (listboxState.value !== ListboxStates.Open) return - if (buttonRef.value?.contains(target)) return + if (dom(buttonRef)?.contains(target)) return - if (!optionsRef.value?.contains(target)) api.closeListbox() + if (!dom(optionsRef)?.contains(target)) api.closeListbox() if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.value?.focus({ preventScroll: true }) + if (!event.defaultPrevented) dom(buttonRef)?.focus({ preventScroll: true }) } window.addEventListener('mousedown', handler) @@ -231,7 +232,7 @@ export let ListboxLabel = defineComponent({ id, el: api.labelRef, handleClick() { - api.buttonRef.value?.focus({ preventScroll: true }) + dom(api.buttonRef)?.focus({ preventScroll: true }) }, } }, @@ -253,10 +254,10 @@ export let ListboxButton = defineComponent({ id: this.id, type: 'button', 'aria-haspopup': true, - 'aria-controls': api.optionsRef.value?.id, + 'aria-controls': dom(api.optionsRef)?.id, 'aria-expanded': api.listboxState.value === ListboxStates.Open ? true : undefined, 'aria-labelledby': api.labelRef.value - ? [api.labelRef.value.id, this.id].join(' ') + ? [dom(api.labelRef)?.id, this.id].join(' ') : undefined, disabled: api.disabled, onKeyDown: this.handleKeyDown, @@ -284,7 +285,7 @@ export let ListboxButton = defineComponent({ event.preventDefault() api.openListbox() nextTick(() => { - api.optionsRef.value?.focus({ preventScroll: true }) + dom(api.optionsRef)?.focus({ preventScroll: true }) if (!api.value.value) api.goToOption(Focus.First) }) break @@ -293,7 +294,7 @@ export let ListboxButton = defineComponent({ event.preventDefault() api.openListbox() nextTick(() => { - api.optionsRef.value?.focus({ preventScroll: true }) + dom(api.optionsRef)?.focus({ preventScroll: true }) if (!api.value.value) api.goToOption(Focus.Last) }) break @@ -304,11 +305,11 @@ export let ListboxButton = defineComponent({ if (api.disabled) return if (api.listboxState.value === ListboxStates.Open) { api.closeListbox() - nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) + nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) } else { event.preventDefault() api.openListbox() - nextFrame(() => api.optionsRef.value?.focus({ preventScroll: true })) + nextFrame(() => dom(api.optionsRef)?.focus({ preventScroll: true })) } } @@ -334,7 +335,7 @@ export let ListboxOptions = defineComponent({ api.activeOptionIndex.value === null ? undefined : api.options.value[api.activeOptionIndex.value]?.id, - 'aria-labelledby': api.labelRef.value?.id ?? api.buttonRef.value?.id, + 'aria-labelledby': dom(api.labelRef)?.id ?? dom(api.buttonRef)?.id, id: this.id, onKeyDown: this.handleKeyDown, role: 'listbox', @@ -377,7 +378,7 @@ export let ListboxOptions = defineComponent({ api.select(dataRef.value) } api.closeListbox() - nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) + nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) break case Keys.ArrowDown: @@ -401,7 +402,7 @@ export let ListboxOptions = defineComponent({ case Keys.Escape: event.preventDefault() api.closeListbox() - nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) + nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) break case Keys.Tab: @@ -477,7 +478,7 @@ export let ListboxOption = defineComponent({ if (disabled) return event.preventDefault() api.select(value) api.closeListbox() - nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) + nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) } function handleFocus() { diff --git a/packages/@headlessui-vue/src/components/menu/menu.test.tsx b/packages/@headlessui-vue/src/components/menu/menu.test.tsx index 993eb041c7..84199e6547 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.test.tsx +++ b/packages/@headlessui-vue/src/components/menu/menu.test.tsx @@ -231,6 +231,46 @@ describe('Rendering', () => { assertMenu({ state: MenuState.Visible }) }) + it('should be possible to render a MenuButton using a template `as` prop and a custom element', async () => { + renderTemplate({ + template: ` +
+ `, + components: { + MyCustomButton: defineComponent({ + setup(_, { slots }) { + return () => { + return h('button', slots.default?.()) + } + }, + }), + }, + }) + + assertMenuButton({ + state: MenuState.InvisibleUnmounted, + attributes: { id: 'headlessui-menu-button-1', 'data-open': 'false' }, + }) + assertMenu({ state: MenuState.InvisibleUnmounted }) + + await click(getMenuButton()) + + assertMenuButton({ + state: MenuState.Visible, + attributes: { id: 'headlessui-menu-button-1', 'data-open': 'true' }, + }) + assertMenu({ state: MenuState.Visible }) + }) + it( 'should yell when we render a MenuButton using a template `as` prop that contains multiple children', suppressConsoleLogs(() => { diff --git a/packages/@headlessui-vue/src/components/menu/menu.ts b/packages/@headlessui-vue/src/components/menu/menu.ts index 68c73505b9..1beb6545bb 100644 --- a/packages/@headlessui-vue/src/components/menu/menu.ts +++ b/packages/@headlessui-vue/src/components/menu/menu.ts @@ -16,6 +16,7 @@ import { useId } from '../../hooks/use-id' import { Keys } from '../../keyboard' import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index' import { resolvePropValue } from '../../utils/resolve-prop-value' +import { dom } from '../../utils/dom' enum MenuStates { Open, @@ -141,11 +142,11 @@ export let Menu = defineComponent({ let active = document.activeElement if (menuState.value !== MenuStates.Open) return - if (buttonRef.value?.contains(target)) return + if (dom(buttonRef)?.contains(target)) return - if (!itemsRef.value?.contains(target)) api.closeMenu() + if (!dom(itemsRef)?.contains(target)) api.closeMenu() if (active !== document.body && active?.contains(target)) return // Keep focus on newly clicked/focused element - if (!event.defaultPrevented) buttonRef.value?.focus({ preventScroll: true }) + if (!event.defaultPrevented) dom(buttonRef)?.focus({ preventScroll: true }) } window.addEventListener('mousedown', handler) @@ -176,7 +177,7 @@ export let MenuButton = defineComponent({ id: this.id, type: 'button', 'aria-haspopup': true, - 'aria-controls': api.itemsRef.value?.id, + 'aria-controls': dom(api.itemsRef)?.id, 'aria-expanded': api.menuState.value === MenuStates.Open ? true : undefined, onKeyDown: this.handleKeyDown, onClick: this.handleClick, @@ -203,7 +204,7 @@ export let MenuButton = defineComponent({ event.preventDefault() api.openMenu() nextTick(() => { - api.itemsRef.value?.focus({ preventScroll: true }) + dom(api.itemsRef)?.focus({ preventScroll: true }) api.goToItem(Focus.First) }) break @@ -212,7 +213,7 @@ export let MenuButton = defineComponent({ event.preventDefault() api.openMenu() nextTick(() => { - api.itemsRef.value?.focus({ preventScroll: true }) + dom(api.itemsRef)?.focus({ preventScroll: true }) api.goToItem(Focus.Last) }) break @@ -223,11 +224,11 @@ export let MenuButton = defineComponent({ if (props.disabled) return if (api.menuState.value === MenuStates.Open) { api.closeMenu() - nextTick(() => api.buttonRef.value?.focus({ preventScroll: true })) + nextTick(() => dom(api.buttonRef)?.focus({ preventScroll: true })) } else { event.preventDefault() api.openMenu() - nextFrame(() => api.itemsRef.value?.focus({ preventScroll: true })) + nextFrame(() => dom(api.itemsRef)?.focus({ preventScroll: true })) } } @@ -255,7 +256,7 @@ export let MenuItems = defineComponent({ api.activeItemIndex.value === null ? undefined : api.items.value[api.activeItemIndex.value]?.id, - 'aria-labelledby': api.buttonRef.value?.id, + 'aria-labelledby': dom(api.buttonRef)?.id, id: this.id, onKeyDown: this.handleKeyDown, role: 'menu', @@ -279,7 +280,7 @@ export let MenuItems = defineComponent({ let searchDebounce = ref