CSS & JS Effect – Dialog Modal

 效果

 

参考:

Youtube – Create a Simple Popup Modal

Youtube – Create a Modal (Popup) with HTML/CSS and JavaScript

HTML – Native Dialog Modal

Boostrap Modal

Angular Material Dialog

 

重点

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。

为了防止这样的错误体验,我们可以做一点手脚。

  1. 在打开 Modal的同时 history.push 放入 query param?modal=show (这样之后就可以 back 了)

  2. 把关闭的操作改成 history.back (统一关闭的方式)

  3. 监听 onpopstate,一旦发生就关闭 Modal。

  4. 如果用户 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 还不支持

 

其它没有提到的

  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 了。

 

posted @ 2022-04-17 19:33  兴杰  阅读(77)  评论(0)    收藏  举报