引言
徒手实现一个模态框看似简单,实则不然,一是需要处理滚动穿透问题,二是需要处理tabindex的问题,这两个问题都不好处理。
一般UI框架都会自带模态框组件,针对滚动穿透问题,它的做法一般是将外层容器的滚动条屏蔽掉。布局方面,一般是用固定定位+绝对定位。这里边有一个无法解决的问题,就是你永远无法确定模态框是否处于顶层,即便你zindex给的再大,都无法保证这一点。因为始终存在会比这个值更大的可能。
那有没有一种方式可以确保模态框始终处于顶层呢,其实是有的,即利用原生dialog的能力。
代码示例
html结构
html
<button onclick="dialog.show()">按钮</button>
<dialog class="dialog" id="dialog">
<div class="form">
<div class="form-title">登录</div>
<div class="form-control">
<label class="form-control-label">账号: </label>
<input class="form-control-value" />
</div>
<div class="form-control">
<label class="form-control-label">密码: </label>
<input type="password" class="form-control-value" />
</div>
<button class="btn-submit" @click="dialog.close()">登录</button>
</div>
</dialog>
效果如下:
简单给模态框里面的内容一些样式
css
.form{
width: 420px;
padding: 20px 30px;
}
.form-title{
display: flex;
justify-content: center;
padding: 20px;
font-size: 20px;
}
.form-control{
display: flex;
align-items: center;
padding: 12px 0;
width: 100%;
}
.form-control-label{
margin-right: 6px;
}
.form-control-value{
flex: 1;
height: 32px;
padding-left: 6px;
border: 1px solid #ccc;
}
.form-control-value:focus{
outline: none;
}
.btn-submit{
display: block;
margin: 20px auto;
background-color: rgb(93, 167, 93);
color: #fff;
width: 200px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
}
效果如下:
添加蒙层
html结构
html
<button onclick="dialog.showModal()">按钮</button>
// ...
</dialog>
只需将dialog.show改为dialog.showModal即可。
再给dialog和蒙层一些样式
css
.dialog{
border-width: 0;
border-radius: 6px;
}
dialog::backdrop{
background-color: #32586823; // 给蒙层一个灰色背景
backdrop-filter: blur(1px); // 添加毛玻璃效果
}
效果如下:
看起来好多了,但略显生硬。考虑是否可以给dialog添加一个过渡效果。
很遗憾,只通过样式是做不到的。
因为动画的本质是数字的变化,而dialog这里的显示和节点的创建和销毁,并非数字的变化。
细心的小伙伴已经发现了,这个dialog从层级上来看是处于当前文档节点之外的一个游离节点。
既然动画的本质是数字的变化,那我们是否可以通过改变这个dialog的透明度(opacity)来巧妙地做到过渡动画呢?
代码实现
ts
<script setup lang="ts">
function handleOpen() {
const dialog = document.getElementById('dialog')! as any
// 打开对话框时
dialog.showModal();
requestAnimationFrame(() => {
dialog.classList.add('open');
});
}
function handleSubmit() {
const dialog = document.getElementById('dialog')! as any
// 关闭对话框时
dialog.classList.remove('open');
setTimeout(() => {
dialog.close()
}, 200);
}
</script>
html
<template>
<button @click="handleOpen">按钮</button>
<dialog class="dialog" id="dialog">
<div class="form">
<div class="form-title">登录</div>
<div class="form-control">
<label class="form-control-label">账号: </label>
<input class="form-control-value" />
</div>
<div class="form-control">
<label class="form-control-label">密码: </label>
<input type="password" class="form-control-value" />
</div>
<button class="btn-submit" @click="handleSubmit">登录</button>
</div>
</dialog>
</template>
样式稍作调整
css
.dialog{
border-width: 0;
border-radius: 6px;
transition-duration: .5s;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s ease, transform 0.3s ease;
}
dialog.open {
opacity: 1;
transform: scale(1);
}
dialog.open::backdrop{
opacity: 1;
transform: scale(1);
background-color: #32586823;
backdrop-filter: blur(1px);
}
.dialog::backdrop{
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s ease, transform 0.3s ease;
}
效果如下:
这样看起来是不是好多了。
总结
通过原生实现模态框,一方面可以解决zindex层级覆盖的问题,二来也能规避掉滚动穿透和tabindex跑到蒙层下面的问题。由于是浏览器原生支持的,所以性能也会更好。