Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions packages/@headlessui-vue/src/components/popover/popover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<Popover :restoreFocus="false" v-slot="{ close }">
<PopoverButton>Trigger</PopoverButton>
<PopoverPanel>
<button @click="close()">Close me</button>
</PopoverPanel>
</Popover>
`)

// 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 () => {
Expand Down Expand Up @@ -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`
<Popover :restoreFocus="false">
<PopoverButton>Trigger</PopoverButton>
<PopoverPanel>Contents</PopoverPanel>
</Popover>
`)

// 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`
<Popover :restoreFocus="false">
<PopoverButton as="input" type="text" value="Trigger" data-trigger />
<PopoverPanel>Contents</PopoverPanel>
</Popover>
`)

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 () => {
Expand Down
25 changes: 17 additions & 8 deletions packages/@headlessui-vue/src/components/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface StateDefinition {
panelId: Ref<string | null>

isPortalled: Ref<boolean>
shouldRestoreFocus: Ref<boolean>

beforePanelSentinel: Ref<HTMLElement | null>
afterPanelSentinel: Ref<HTMLElement | null>
Expand All @@ -61,7 +62,8 @@ interface StateDefinition {
closePopover(): void

// Exposed functions
close(focusableElement: HTMLElement | Ref<HTMLElement | null>): void
close(focusableElement?: HTMLElement | Ref<HTMLElement | null>): void
focusButton(): void
}

let PopoverContext = Symbol('PopoverContext') as InjectionKey<StateDefinition>
Expand Down Expand Up @@ -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<HTMLElement | null>(null)
Expand Down Expand Up @@ -159,6 +162,7 @@ export let Popover = defineComponent({
panel,
button,
isPortalled,
shouldRestoreFocus: computed(() => props.restoreFocus),
beforePanelSentinel,
afterPanelSentinel,
togglePopover() {
Expand All @@ -171,11 +175,16 @@ export let Popover = defineComponent({
if (popoverState.value === PopoverStates.Closed) return
popoverState.value = PopoverStates.Closed
},
close(focusableElement: HTMLElement | Ref<HTMLElement | null>) {
focusButton() {
if (!api.shouldRestoreFocus.value) return
dom(api.button)?.focus()
},
close(focusableElement?: HTMLElement | Ref<HTMLElement | null>) {
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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -589,7 +598,7 @@ export let PopoverPanel = defineComponent({
event.preventDefault()
event.stopPropagation()
api.closePopover()
dom(api.button)?.focus()
api.focusButton()
break
}
}
Expand Down