tailwindlabs / headlessui

Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.
https://headlessui.com
MIT License
25.81k stars 1.07k forks source link

Dialog component's portal root element gets lost when navigating between routes in a Vue 3 app #3116

Closed DaRosenberg closed 5 months ago

DaRosenberg commented 5 months ago

What package within Headless UI are you using?

@headlessui/vue

What version of that package are you using?

1.7.20

What browser are you using?

Chrome and Safari (reproduces in both).

Reproduction URL

I've boiled this down to a bare minimum repro.

Describe your issue

When using vue-router it seems the Dialog component only works in components that are loaded on the initial route (i.e. on page load). When navigating to a different route, thus loading different components, instances of Dialog inside those newly visible components fail to open and the dialog component logs a console warning:

There are no focusable elements inside the <FocusTrap />

While troubleshooting I've noticed that this coincides with the headlessui-portal-root element getting lost on navigation between routes (i.e. removed from the DOM and then not restored when the new route is loaded).

The repro I put together illustrates this.

To reproduce the issue, use the links to navigate between the first and second view.

Notice how the headlessui-portal-root element is present in the DOM on the view of the initial page load, and dialog opens fine on this view. When navigating to the other view, the headlessui-portal-root element disappears, the dialog does not open.

When navigating back to the initial route, the headlessui-portal-root element is restored and the dialog once again opens fine on the first view.

Note that the exact same TestDialog component is used on both views, and the issue has nothing to do with missing focusable elements in the dialog itself.

sk31park commented 5 months ago

I have a same problem..

Danita commented 5 months ago

Oh god, I've been battling the last 2 hours with this thinking it was a problem on my application. It happens here too. I haven't found a workaround yet.

Danita commented 5 months ago

I found that using v-if to show and hide the dialog forces it to recreate the portal and work around the error for now. You lose any transitions of course.

DaRosenberg commented 5 months ago

@Danita I would kill for a workaround, can you show yours? What are you using as the condition in your v-if?

Danita commented 5 months ago

@DaRosenberg It's nothing fancy, only a matter of conditionally drawing the

element on the page that uses it, not only setting its open prop like it says on the documentation. For example, I was creating a reusable custom component for my app that looks like this:

<template>
    <Dialog :open="show" @close="$emit('cancel')">
    ...
    </Dialog>
</template>
<script setup>
    import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue';
    defineProps({
        show: { type: Boolean, default: false },
        title: { type: String, default: 'Dialog title' },
    })
    const emit = defineEmits(["cancel", "save"]);
</script>

So when I use that component in my app, to show the modal I should set the show prop to true. My workaround was to also insert the modal conditionally with v-if.

<template>
    ...
    <button @click="showModal=true">Edit</button>
    ...
    <modal v-if="showModal" :show="showModal" @cancel="showModal=false" @save="handleSave">
        ...
    </modal>
</template>
<script setup>
    import Modal from "../../components/Modal.vue";
    import { ref } from 'vue';
    const showModal = ref(false);
</script>

Hope it helps.

DaRosenberg commented 5 months ago

@Danita that works like a charm, thank you! 🙏🏻

thecrypticace commented 5 months ago

Sorry about that! One of our changes cause a timing issue when switching dialogs in the same "tick". Fixed in @headlessui/vue v1.7.21

npm install @headlessui/vue@latest
Danita commented 4 months ago

@thecrypticace thank you!! <3