移动端键盘开启时关闭Dialog组件无法卸载的技术问题
解决HeadlessUI Dialog移动端键盘开启时关闭后未卸载的问题
问题复现
- 移动端打开弹窗
- 聚焦输入框唤起虚拟键盘
- 保持键盘开启状态,点击弹窗背景关闭弹窗
- 弹窗完成过渡消失后,组件未卸载,阻挡下层所有元素交互
核心原因
移动端键盘唤起会改变视窗高度,导致HeadlessUI的Transition组件在处理退场动画时,无法正确同步Dialog的open状态与过渡完成后的卸载逻辑;同时输入框未失焦也可能引发DOM状态异常,使得弹窗底层元素仍保留在页面中阻挡交互。
解决方案
1. 强制过渡结束后卸载Dialog
确保Transition组件设置unmount={true},让组件在过渡动画完全结束后自动卸载:
import { Dialog, Transition } from '@headlessui/react'; import { useState, Fragment } from 'react'; function AppModal({ open, setOpen }) { return ( <Transition appear show={open} as={Fragment} unmount={true}> <Dialog as="div" className="relative z-10" onClose={setOpen}> {/* 背景遮罩过渡 */} <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="fixed inset-0 bg-black/25" /> </Transition.Child> {/* 弹窗内容容器 */} <div className="fixed inset-0 overflow-y-auto"> <div className="flex min-h-full items-center justify-center p-4"> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > <Dialog.Panel className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl transition-all"> <Dialog.Title className="text-lg font-medium text-gray-900">示例弹窗</Dialog.Title> <div className="mt-4"> <input type="text" className="w-full border rounded p-2" placeholder="输入内容" /> </div> <div className="mt-4"> <button className="rounded-md bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200" onClick={() => setOpen(false)} > 关闭弹窗 </button> </div> </Dialog.Panel> </Transition.Child> </div> </div> </Dialog> </Transition> ); }
2. 关闭弹窗时强制失焦输入框
在Dialog的关闭回调中,手动让输入框失焦,避免键盘唤起状态下的DOM异常:
import { Dialog, Transition } from '@headlessui/react'; import { useState, Fragment, useRef } from 'react'; function AppModal({ open, setOpen }) { const inputRef = useRef<HTMLInputElement>(null); const handleClose = () => { // 强制输入框失焦 inputRef.current?.blur(); setOpen(false); }; return ( <Transition appear show={open} as={Fragment} unmount={true}> <Dialog as="div" className="relative z-10" onClose={handleClose}> {/* ... 省略遮罩与容器代码 ... */} <Dialog.Panel className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl transition-all"> <Dialog.Title className="text-lg font-medium text-gray-900">示例弹窗</Dialog.Title> <div className="mt-4"> <input ref={inputRef} type="text" className="w-full border rounded p-2" placeholder="输入内容" /> </div> <div className="mt-4"> <button className="rounded-md bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200" onClick={handleClose} > 关闭弹窗 </button> </div> </Dialog.Panel> </Dialog> </Transition> ); }
3. 监听视窗高度变化同步状态
移动端键盘唤起会改变window.innerHeight,监听该变化,确保弹窗在关闭状态下完全卸载:
import { Dialog, Transition } from '@headlessui/react'; import { useState, Fragment, useEffect } from 'react'; function AppModal({ open, setOpen }) { useEffect(() => { const handleResize = () => { if (!open) { // 手动清理残留的弹窗DOM元素 document.querySelectorAll('[role="dialog"]').forEach(el => { if (el.style.display !== 'none') el.remove(); }); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [open]); return ( {/* ... 省略弹窗主体代码 ... */} ); }
关键注意事项
Transition组件的unmount={true}是实现过渡后卸载的核心配置,必须开启- 处理输入框焦点可避免键盘唤起带来的DOM状态异常
- 移动端视窗变化可能干扰HeadlessUI内部状态,需额外监听同步
内容的提问来源于stack exchange,提问作者Eric Wang
相关产品推荐
相关产品推荐

