CSS & JS Effect – Dialog Modal
效果
参考:
Youtube – Create a Simple Popup Modal
Youtube – Create a Modal (Popup) with HTML/CSS and JavaScript
重点
1. modal 就是一个 position: fixed 的大 overlay 黑影,同时里面有居中的 content box。
2. modal 原本是 hide 起来的,点击后 show 就可以了。
HTML
<body> <button class="open-modal-btn">Open Modal</button> <div class="modal"> <button class="close-modal-btn">X</button> <div class="content"> <h1>Titanic</h1> <p>I'm the king of the world!!!</p> </div> </div> </body>
modal 负责 backdrop 黑影,content 则是中间白色的内容区域。
Styles
看注释的部分就可以了,其它的只是点缀。
.modal {
visibility: hidden; /* show & hide control */
position: fixed; /* 定位 */
inset: 0; /* full viewport */
background-color: hsl(0deg 0% 0% / 20%); /* backdrop color */
/* 居中 content */
display: flex;
justify-content: center;
align-items: center;
.close-modal-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 64px;
border-radius: 999px;
border: 0;
background-color: black;
color: white;
width: 64px;
height: 64px;
padding: 12px;
font-size: 32px;
cursor: pointer;
}
.content {
padding: 32px 48px;
background-color: white; /* 因为 modal 是透明黑, 所以这里要 set 回白色 */
box-shadow: 0 2px 4px hsl(0deg 0% 0% / 20%); /* 影子 */
h1 {
margin-bottom: 16px;
font-size: 48px;
}
p {
font-size: 20px;
line-height: 1.5;
}
}
&.shown {
visibility: unset; /* clear visibility to show */
}
}
JavaScript
const modal = document.querySelector('.modal')!;
const openModalBtn = document.querySelector('.open-modal-btn')!;
const closeModalBtn = document.querySelector('.close-modal-btn')!;
openModalBtn.addEventListener('click', () => modal.classList.add('shown'));
closeModalBtn.addEventListener('click', () => modal.classList.remove('shown'));
只是简单的点击 add class 而已。
Body Scroll IOS Safari Problem
modal 开启后,通常体验是不允许 body scroll,一般的做法是给 body overflow: hidden。
但是这个在 Safari 有 bug,目前 status 是说已经 fixed 了,但是我没用最新的 safari 测试,所以不确定。
解决方法是让 body position fixed,然后修改它的 top 到当前的 scroll top。
下面是我封装的方法
// note: 来龙去脉
// safari 没有办法通过 overflow hide 去阻止 body scroll
// 所以只能把 body 定位变成没有 scroll
// 这个 safari 问题已经很多年的了
// tesla, angular, Stack Overflow 也是用这个方案去破
// bootstrap 倒没有处理这个, 比较 noob
// refer:
// https://bugs.webkit.org/show_bug.cgi?id=153852
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block
const showedModals: string[] = [];
export function toggleModal(modalName: string): void {
showedModals.includes(modalName) ? closeModal(modalName) : openModal(modalName);
}
export function openModal(modalName: string): void {
if (showedModals.length === 0) {
const currentScrollTop = (document.scrollingElement ?? document.documentElement).scrollTop;
document.body.style.position = 'fixed';
document.body.style.width = '100%';
document.body.style.top = `-${currentScrollTop}px`;
}
showedModals.push(modalName);
}
export function closeModal(modalName: string): void {
if (!showedModals.includes(modalName)) {
throw new Error(`Showed modals doesn't contains modal name: ${modalName}`);
}
if (showedModals.length === 1) {
const recoveryScrollTop = Math.abs(parseFloat(document.body.style.top));
document.body.style.removeProperty('position');
document.body.style.removeProperty('width');
document.body.style.removeProperty('top');
(document.scrollingElement ?? document.documentElement).scrollTop = recoveryScrollTop;
}
const indexOf = showedModals.indexOf(modalName);
showedModals.splice(indexOf, 1);
}
Mobile Back Button Close Dialog
大家习惯按手机的 back button 来试图关闭 Modal。
其实这是一个不正确的操作,因为 back 会直接跳去上一页而不是关闭 Modal。
为了防止这样的错误体验,我们可以做一点手脚。
-
在打开 Modal的同时 history.push 放入 query param?modal=show (这样之后就可以 back 了)
-
把关闭的操作改成 history.back (统一关闭的方式)
-
监听 onpopstate,一旦发生就关闭 Modal。
-
如果用户 refresh 那我们需要在 page load 的时候把 ?modal=show 用 history.replace 移除掉
以上的方法思路只是 for 简单场景的,如果有多层 modal 或者更复杂的情况就 cover 不了了。
JS 代码:
const modal = document.querySelector<HTMLElement>('.modal')!;
const openModalBtn = document.querySelector<HTMLButtonElement>('.open-modal-btn')!;
const closeModalBtn = document.querySelector<HTMLButtonElement>('.close-modal-btn')!;
// 处理 page load
const { pathname, search, hash } = window.location;
const queryParams = new URLSearchParams(search);
if (queryParams.has('whatsAppDialog')) {
queryParams.delete('whatsAppDialog');
const newSearch = queryParams.toString() !== '' ? `?${queryParams.toString()}` : '';
history.replaceState(null, '', `${pathname}${newSearch}${hash}`);
}
openModalBtn.addEventListener('click', () => {
modal.classList.add('shown');
// pushState
const { pathname, search, hash } = window.location;
const params = new URLSearchParams(search);
params.set('whatsAppDialog', 'open');
history.pushState(null, '', `${pathname}?${params.toString()}${hash}`);
// listen to 'back button'
window.addEventListener(
'popstate',
() => {
modal.classList.remove('shown');
},
{ once: true },
);
});
closeModalBtn.addEventListener('click', () => {
history.back(); // back
});
另外,未来或许可以用 Navigation API 实现,可能会更方便。
以后才研究,毕竟现在许多 browser 还不支持。
其它没有提到的
-
backdrop click, Keyboard Esc 关闭。
-
show 的 animation, backdrop fadein, content scale in
-
z-index 问题。要确保 z-index 够高最好是把 modal 放到 body 最下方 (Angular Material 就这么做的)。
但放出去后要注意 CSS Style,element 结构在外面了就不可以 depend on ancestor 了。