LAN 聊天系统改造手记:从原生 confirm 到自定义模态框
约 1 分钟· 719 字 63 次阅读
聊天的项目里之前到处是 confirm(),用户反馈"不同浏览器弹窗长得不一样""点快了不知道点了哪个按钮"。于是系统性替换成自定义模态框,这篇文章记录设计与实现。
为什么不用原生 confirm
原生 confirm()/alert() 有三个硬伤:
- 样式不可控:每个浏览器/操作系统长得都不一样,和产品 UI 割裂;
- 阻塞主线程:弹窗期间页面完全冻结,连 CSS 动画都停了;
- 无返回值状态:只能拿到 true/false,拿不到"用户点了哪个按钮"的上下文。
自定义模态框的设计
基础结构
<div class="modal-backdrop" role="presentation">
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<h2 id="modal-title">确认删除?</h2>
<p id="modal-desc">删除后不可恢复。</p>
<div class="modal-actions">
<button data-action="cancel">取消</button>
<button data-action="confirm" class="danger">删除</button>
</div>
</div>
</div>
关键细节
焦点管理:打开时把焦点移到对话框内的确认按钮;关闭后把焦点还给触发按钮。否则键盘用户会"丢焦点"。
export function showConfirm({ title, message, onConfirm }) {
const trigger = document.activeElement;
// ...创建 DOM、挂载...
confirmBtn.focus();
return {
close(result) {
// 卸载 DOM 后把焦点还给触发元素
trigger?.focus();
if (result) onConfirm?.();
},
};
}
Esc 与遮罩点击:Esc 等于取消,点遮罩也等于取消(但要有明确视觉反馈)。
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") modal.close(false);
});
动画与过渡:模态框打开用淡入 + 轻微上移,关闭时要有"正在关闭"的状态,避免瞬间消失的生硬感。用 display 切换配合 requestAnimationFrame 触发过渡,而不是直接隐藏。
跨浏览器一致性
自定义模态框天然解决了"不同浏览器长得不一样"的问题——所有浏览器渲染同一套 HTML/CSS。需要额外注意的是:
navigator.clipboard等 API 的兼容性差异,模态框内容里的交互要用降级方案;- 移动端
<select>等原生控件在模态框内的表现; - 长文本时模态框最大高度与滚动(
max-height: 80dvh; overflow-y: auto)。
替换策略
不一次性全换,按风险从低到高分三批:
- 无副作用的确认框(删除、清空)—— 直接替换;
- 带输入的提示框 —— 换成自定义表单模态框;
- 异步操作结果反馈 —— 换成 toast 而非弹窗。
每批替换后跑一遍冒烟测试,重点是"取消/确认后焦点位置正确"和"Esc 行为一致"。
小结
自定义模态框不是"为了好看",它同时解决了一致性、可访问性和交互反馈三个问题。核心工作量不在样式,而在焦点管理、Esc 处理、动画过渡这三个细节——做对了,体验和原生一样顺手,但样式完全可控。