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 id="js-open-modal-btn" class="open-modal-btn">Open Modal</button> <div id="js-modal" class="modal"> <div class="content"> <div class="header"> <h1>You are The Winner!</h1> <button id="js-close-modal-btn" class="close-modal-btn"><i class="fa-solid fa-xmark"></i></button> </div> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellat id, rem veniam ratione modi laboriosam consectetur vitae illum perspiciatis recusandae!</p> </div> </div> </body>
modal 负责 backdrop 黑影
content 则是中间白色的内容区域
CSS Style
.open-modal-btn{ margin-top: 17rem; margin-inline: auto; display: block; } .modal { visibility: hidden; // show / hide control position: fixed; // 定位 inset: 0; // full viewport background-color: rgb(0 0 0 / 0.2); // backdrop color // 居中 content display: flex; justify-content: center; align-items: center; .content { padding: 2rem 3rem; max-width: 1024px; background-color: white; // 因为 modal 是透明黑, 所以这里要 set 回白色 box-shadow: 0 2px 4px rgb(0 0 0 / 0.2); // 影子 .header{ display: flex; justify-content: space-between; align-items: center; font-size: 3rem; .close-modal-btn { background-color: white; font-size: inherit; color: black; border-width: 0; cursor: pointer; } } p{ margin-top: 2rem; font-size: 2rem; } } &.show { visibility: initial; // clear visibility to show } }
看注释的地方解释就可以了, 其它是点缀而已
JavaScript
const modal = document.querySelector<HTMLElement>('#js-modal')!; const openModalBtn = document.querySelector<HTMLButtonElement>('#js-open-modal-btn')!; const closeModalBtn = document.querySelector<HTMLButtonElement>('#js-close-modal-btn')!; openModalBtn.addEventListener('click', () => { modal.classList.add('show'); }); closeModalBtn.addEventListener('click', () => { modal.classList.remove('show'); });
只是简单的点击 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, stackoverflow 也是用这个方案去破 // 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.
为了防止这样的错误体验. 我们可以做一点手脚.
step 1: 在打开 Modal的同时 history.push 放入 query param ?modal="show" (这样之后就可以 back 了)
step 2: 把关闭的操作 改成 history.back (统一关闭的方式)
step 3: 监听 onpopstate, 一旦发生就关闭 Modal.
step 4: 如果用户 refresh 那我们需要在 page load 的时候把 ?modal=show 用 history.replace 移除掉
以上的方法思路只是 for 简单场景的. 如果有多层 modal 或者更复杂的情况就 cover 不了了.
JS 代码:
const modal = document.querySelector<HTMLElement>('#js-modal')!; const openModalBtn = document.querySelector<HTMLButtonElement>('#js-open-modal-btn')!; const closeModalBtn = document.querySelector<HTMLButtonElement>('#js-close-modal-btn')!; // 处理 page load const { pathname, search, hash } = 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('show'); // pushState const { pathname, search, hash } = 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('show'); }, { once: true } ); }); closeModalBtn.addEventListener('click', () => { history.back(); // back });
另外, 未来或许可以用 Navigation API 实现, 可能会更方便. 以后才研究. 毕竟现在许多 browser 还不支持.
其它没有提到的
1. backdrop click, Keyboard Esc 关闭.
2. show 的 animation, backdrop fadein, content scale in
3. z-index 问题. 要确保 z-index 够高最好是把 modal 放到 body 最下方 (Angular Material 就这么做的)
但放出去后要注意 CSS Style 哦. element 结构在外面了就不可以 depend on ancestor 了.