Vue3/Nuxt3复用HeadlessUI Dialog多实例触发FocusTrap报错
问题描述
在Vue3/Nuxt3应用中基于HeadlessUI开发了可复用的Dialog模态框组件,单独使用时功能正常,但在页面中引入多个实例后,点击打开模态框会触发错误:There are no focusable elements inside the <FocusTrap />。
组件代码(ExtensionModal.vue):
<template> <div class="flex-col items-center"> <div class="mb-2"> <button type="button" @click="openModal" class="block rounded-full text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-0 focus:outline-none focus:ring-blue-300 font-medium text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:hover:bg-blue-600 dark:focus:ring-blue-800" > {{ buttonLabel }} </button> </div> </div> <TransitionRoot appear :show="showExtensionModal" as="template"> <Dialog as="div" @close="closeModal" class="relative z-10"> <TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0" enter-to="opacity-100" leave="duration-200 ease-in" leave-from="opacity-100" leave-to="opacity-0" > <div class="fixed inset-0 bg-black bg-opacity-25" /> </TransitionChild> <div class="fixed h-fit inset-0 overflow-y-auto flex items-center justify-center" > <div class="fixed inset-0 overflow-y-auto flex items-center justify-center" > <TransitionChild as="template" enter="duration-300 ease-out" enter-from="opacity-0 scale-95" enter-to="opacity-100 scale-100" leave="duration-200 ease-in" leave-from="opacity-100 scale-100" leave-to="opacity-0 scale-95" > <DialogPanel class="flex-grow w-full h-fit transform overflow-visible rounded-2xl bg-gray-200 dark:bg-slate-800 p-6 text-left align-middle shadow-xl transition-all max-w-[90vw] sm:max-w-[80vw] md:max-w-[70vw] lg:max-w-[60vw] xl:max-w-[50vw] 2xl:max-w-[40vw]" > <DialogTitle as="h3" class="flex text-lg font-medium leading-6 text-gray-900 justify-center dark:text-white" > {{ buttonTitle }} </DialogTitle> <form @submit="onSubmit($event)"> <div class="flex mt-5"> <div class="w-full z-40"> <select v-model="element" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" > <option v-for="option in extensionTypes" :value="option.value" :key="option.value" > {{ option.text }} </option> </select> </div> </div> <!-- Submit/Cancel button for the modal --> <FormSubmit @submit="onSubmit" @close="closeModal" /> </form> </DialogPanel> </TransitionChild> </div> </div> </Dialog> </TransitionRoot> </template> <script setup> import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle, Switch, } from "@headlessui/vue"; const props = defineProps({ buttonTitle: { type: String, required: false, }, buttonLabel: { type: String, required: false, }, }); const emits = defineEmits(["onSubmit"]); const extension = ref({}); const showExtensionModal = useState("showExtensionModal", () => false); const extensionTypes = useState("extensionTypes", () => [ { value: "extensionElement", text: "Element" }, ]); const dataTypes = useState("dataTypes", () => [ { value: "string", text: "String" }, { value: "complex", text: "Complex" }, ]); function openModal() { showExtensionModal.value = true; } function closeModal() { showExtensionModal.value = false; } function onItemChange(fieldName, value) { extension.value = {}; extension.value[fieldName] = value.value; } function onSubmit(event) { event.preventDefault(); console.log("Sumitted : " + JSON.stringify(extension.value, null, 4)); emits("onSubmit", extension.value); closeModal(); } </script> <style> </style>
页面中两次使用组件(pages/view.vue):
<template> <div> <div class="my-2 mx-2 px-2 pb-0"> <div class="flex gap-2 h-16 float-right"> <ExtensionModal :buttonTitle="$t('pages.modal.userExtension-btt-title')" :buttonLabel="$t('pages.modal.userExtension-btt-label')" /> <ExtensionModal :buttonTitle="$t('pages.modal.ilmd-btt-title')" :buttonLabel="$t('pages.modal.ilmd-btt-label')" /> </div> </div> </div> </template> <script setup> import { Icon } from "@iconify/vue"; </script> <style> </style>
错误原因
核心问题是使用Nuxt的useState管理组件内部模态框状态。useState创建的是全局共享状态,所有组件实例共用同一个showExtensionModal变量。点击任意按钮打开模态框时,所有ExtensionModal的模态框都会被激活,多个Dialog同时存在导致HeadlessUI的FocusTrap焦点陷阱逻辑混乱,触发错误。
此外还有两个小问题:
- 模板中
v-model="element"绑定的变量未在脚本中声明 extensionTypes和dataTypes是组件固定数据,无需用全局状态useState
解决方案
1. 替换全局状态为组件内部状态
将useState改为Vue的ref,确保每个组件实例拥有独立状态:
// 替换原来的useState声明 const showExtensionModal = ref(false); const extensionTypes = [ { value: "extensionElement", text: "Element" }, ]; const dataTypes = [ { value: "string", text: "String" }, { value: "complex", text: "Complex" }, ];
2. 声明缺失的element变量
在脚本中添加响应式变量声明:
const element = ref('');
3. 同步表单数据到提交对象
在onSubmit中同步element的值到extension对象,确保提交数据完整:
function onSubmit(event) { event.preventDefault(); extension.value.element = element.value; console.log("Sumitted : " + JSON.stringify(extension.value, null, 4)); emits("onSubmit", extension.value); closeModal(); }
修改后的完整脚本部分
<script setup> import { ref } from 'vue'; import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle, Switch, } from "@headlessui/vue"; const props = defineProps({ buttonTitle: { type: String, required: false, }, buttonLabel: { type: String, required: false, }, }); const emits = defineEmits(["onSubmit"]); const extension = ref({}); const showExtensionModal = ref(false); const element = ref(''); const extensionTypes = [ { value: "extensionElement", text: "Element" }, ]; const dataTypes = [ { value: "string", text: "String" }, { value: "complex", text: "Complex" }, ]; function openModal() { showExtensionModal.value = true; } function closeModal() { showExtensionModal.value = false; } function onItemChange(fieldName, value) { extension.value = {}; extension.value[fieldName] = value.value; } function onSubmit(event) { event.preventDefault(); extension.value.element = element.value; console.log("Sumitted : " + JSON.stringify(extension.value, null, 4)); emits("onSubmit", extension.value); closeModal(); } </script>
修改后每个ExtensionModal实例拥有独立的显示状态,打开一个模态框不会影响其他实例,FocusTrap可正常识别当前模态框内的可聚焦元素(select、提交按钮等),错误即可解决。
内容的提问来源于stack exchange,提问作者BATMAN_2008
相关产品推荐
相关产品推荐

