diff --git a/packages/@headlessui-vue/src/components/popover/popover.test.ts b/packages/@headlessui-vue/src/components/popover/popover.test.ts
index 3a208e206e..19e872ee0a 100644
--- a/packages/@headlessui-vue/src/components/popover/popover.test.ts
+++ b/packages/@headlessui-vue/src/components/popover/popover.test.ts
@@ -180,6 +180,32 @@ describe('Rendering', () => {
})
)
+ it(
+ 'should expose a close function that closes the popover and does not restore focus when restoreFocus is false',
+ suppressConsoleLogs(async () => {
+ renderTemplate(html`
+
+ Trigger
+
+
+
+
+ `)
+
+ // Open the popover
+ await click(getPopoverButton())
+
+ // Ensure we can click the close button
+ await click(getByText('Close me'))
+
+ // Ensure the popover is closed
+ assertPopoverPanel({ state: PopoverState.InvisibleUnmounted })
+
+ // Ensure focus did not get restored to the trigger
+ expect(document.activeElement).not.toBe(getPopoverButton())
+ })
+ )
+
it(
'should expose a close function that closes the popover and restores to a specific element',
suppressConsoleLogs(async () => {
@@ -2188,6 +2214,63 @@ describe('Mouse interactions', () => {
})
)
+ it(
+ 'should be possible to close the popover without restoring focus when we click outside on the body element and restoreFocus is false',
+ suppressConsoleLogs(async () => {
+ renderTemplate(html`
+
+ Trigger
+ Contents
+
+ `)
+
+ // Open popover
+ await click(getPopoverButton())
+
+ // Verify it is open
+ assertPopoverButton({ state: PopoverState.Visible })
+
+ // Click the body to close
+ await click(document.body)
+
+ // Verify it is closed
+ assertPopoverButton({ state: PopoverState.InvisibleUnmounted })
+
+ // Verify focus was not restored to the trigger
+ expect(document.activeElement).not.toBe(getPopoverButton())
+ })
+ )
+
+ it(
+ 'should not restore focus to an input trigger when we click outside and restoreFocus is false',
+ suppressConsoleLogs(async () => {
+ renderTemplate(html`
+
+
+ Contents
+
+ `)
+
+ let input = document.querySelector('[data-trigger]') as HTMLInputElement
+ expect(input).not.toBeNull()
+
+ // Open popover from the input trigger
+ await click(input)
+
+ // Verify it is open
+ assertPopoverPanel({ state: PopoverState.Visible })
+
+ // Click outside to close
+ await click(document.body)
+
+ // Verify it is closed
+ assertPopoverPanel({ state: PopoverState.InvisibleUnmounted })
+
+ // Verify focus was not restored to the input trigger
+ expect(document.activeElement).not.toBe(input)
+ })
+ )
+
it(
'should be possible to close the popover, and re-focus the button when we click outside on a non-focusable element',
suppressConsoleLogs(async () => {
diff --git a/packages/@headlessui-vue/src/components/popover/popover.ts b/packages/@headlessui-vue/src/components/popover/popover.ts
index 564c228fbc..3b4171c20a 100644
--- a/packages/@headlessui-vue/src/components/popover/popover.ts
+++ b/packages/@headlessui-vue/src/components/popover/popover.ts
@@ -52,6 +52,7 @@ interface StateDefinition {
panelId: Ref
isPortalled: Ref
+ shouldRestoreFocus: Ref
beforePanelSentinel: Ref
afterPanelSentinel: Ref
@@ -61,7 +62,8 @@ interface StateDefinition {
closePopover(): void
// Exposed functions
- close(focusableElement: HTMLElement | Ref): void
+ close(focusableElement?: HTMLElement | Ref): void
+ focusButton(): void
}
let PopoverContext = Symbol('PopoverContext') as InjectionKey
@@ -105,6 +107,7 @@ export let Popover = defineComponent({
inheritAttrs: false,
props: {
as: { type: [Object, String], default: 'div' },
+ restoreFocus: { type: Boolean, default: true },
},
setup(props, { slots, attrs, expose }) {
let internalPopoverRef = ref(null)
@@ -159,6 +162,7 @@ export let Popover = defineComponent({
panel,
button,
isPortalled,
+ shouldRestoreFocus: computed(() => props.restoreFocus),
beforePanelSentinel,
afterPanelSentinel,
togglePopover() {
@@ -171,11 +175,16 @@ export let Popover = defineComponent({
if (popoverState.value === PopoverStates.Closed) return
popoverState.value = PopoverStates.Closed
},
- close(focusableElement: HTMLElement | Ref) {
+ focusButton() {
+ if (!api.shouldRestoreFocus.value) return
+ dom(api.button)?.focus()
+ },
+ close(focusableElement?: HTMLElement | Ref) {
api.closePopover()
+ if (focusableElement === undefined && !api.shouldRestoreFocus.value) return
let restoreElement = (() => {
- if (!focusableElement) return dom(api.button)
+ if (focusableElement === undefined) return dom(api.button)
if (focusableElement instanceof HTMLElement) return focusableElement
if (focusableElement.value instanceof HTMLElement) return dom(focusableElement)
@@ -251,9 +260,9 @@ export let Popover = defineComponent({
(event, target) => {
api.closePopover()
- if (!isFocusableElement(target, FocusableMode.Loose)) {
+ if (api.shouldRestoreFocus.value && !isFocusableElement(target, FocusableMode.Loose)) {
event.preventDefault()
- dom(button)?.focus()
+ api.focusButton()
}
},
computed(() => popoverState.value === PopoverStates.Open)
@@ -339,7 +348,7 @@ export let PopoverButton = defineComponent({
// @ts-expect-error
event.target.click?.()
api.closePopover()
- dom(api.button)?.focus() // Re-focus the original opening Button
+ api.focusButton() // Re-focus the original opening Button
break
}
} else {
@@ -383,7 +392,7 @@ export let PopoverButton = defineComponent({
if (props.disabled) return
if (isWithinPanel.value) {
api.closePopover()
- dom(api.button)?.focus() // Re-focus the original opening Button
+ api.focusButton() // Re-focus the original opening Button
} else {
event.preventDefault()
event.stopPropagation()
@@ -589,7 +598,7 @@ export let PopoverPanel = defineComponent({
event.preventDefault()
event.stopPropagation()
api.closePopover()
- dom(api.button)?.focus()
+ api.focusButton()
break
}
}