[译] 如何实现隐藏元素的过渡效果?

原文连接:Transitioning Hidden Elements,Paul Hebertjavascript

若是你在设置了 hidden 属性或者 display: none 声明的元素上使用 CSS 过渡(transition),这是个很差实现的效果。我遇到这个问题不少次了,最后决定编写一个 npm 软件包 提供一个可重用的解决方案。css

问题

有不少种隐藏元素的方法。好比使用 visibilityopacitytransformposition 甚至是 clip-path,但这些属性有时并不能知足咱们的预期效果。元素使用 visibility: hidden 隐藏后,原来占据的空间显示为空白,并且上述这些属性的隐藏效果对屏幕阅读器而言都是可见的、可以解析到的。html

使用 hidden 属性display: none 声明隐藏的元素就没有这个问题。但这两种隐藏的元素方式还有一个明显的缺点:就是咱们不能对它们应用 transtion 过渡效果。那么,咱们该怎样解决这个问题呢?我最终编写了一个 npm 软件包 来处理这个问题,并在这个过程当中学到了很多关于文档流、transtion 事件方面的知识。下面跟你们分享下。java

显示和隐藏 .drawer

假设网页里有一个 .drawer 元素,它使用 CSS transform 属性设置偏移,移动到视口以外,随后使用 hidden 属性从文档流中移除。git

(本文里的全部例子都是使用 hidden 属性隐藏元素,这种隐藏方式与 display: none 效果同样。npm 包 同时支持这两种方式的设置。)github

<div class="drawer" hidden>Hello World!</div>

<style> .drawer { transform: translateX(100%); transition: transform ease-out 0.3s; } .drawer.is-open { transform: translateX(0); } </style>
复制代码

下面展现了咱们最终要达到的效果(点击这里 查看 demo):npm

GIF.gif

咱们如何实现这种方式的滑入滑出效果,同时还能将隐藏元素从文档流中移除呢?浏览器

继续看。ide

显示 .drawer

因为 hidden 元素不在文档流中,所以是不能对它使用过渡效果的。但能够这样作:删除元素 hidden 属性后,强制文档从新排版(reflow),这样就能够对 CSS 属性作 transition 效果了。同时,该须要一点 JS 代码协助完成这个工做。函数

const drawer = document.querySelector('.drawer');

function show() {
  drawer.removeAttribute('hidden');

  /** * 强制浏览器重绘(re-paint),而后浏览器就能知道 * 元素不是 `hidden` 的了,这样 transition 也就起做用了。 */
  const reflow = element.offsetHeight;

  // 触发 CSS transition 效果
  drawer.classList.add('is-open');
}
复制代码

隐藏 .drawer

到目前为止,咱们知道如何显示 .drawer 了,但怎样实现元素隐藏时的过渡效果呢?这就要保证隐藏元素再一次应用了 hidden 属性。但问题是,若是咱们在元素隐藏的那一时刻,就应用 hidden,是看不见过渡效果的。相反,咱们应该先等待过渡效果结束后,再 hidden 元素。有两种方式能够实现:transitionend 和 setTimeout

transitionend 事件

CSS 的 transition 效果结束时会触发 transitionend 事件。对应到咱们的示例中,经过删除 .is-open 这个类名,就能自动触发过渡效果发生。经过利用这个事件回调,咱们就能在过渡完成后,为元素添加 hidden 属性。

同时,还要确保过渡完成后要移除事件监听器,不然当元素再次显示时,还会触发一次以前绑定的事件回调,就有问题了。为此,咱们能够将监听器(listener)做为一个变量存储,并在过渡完成后将其移除:

const listener = () => {
  // 3) 为元素添加 hidden 属性
  drawer.setAttribute('hidden', true);
  // 4) 最后移除元素的事件监听器
  drawer.removeEventListener('transitionend', listener);
};
// 隐藏元素
function hide() {
  // 1) 先删除 .is-open 这个类名
  drawer.classList.remove('is-open');
  // 2) 在过渡效果结束后,进入 3)
  drawer.addEventListener('transitionend', listener);
}
复制代码

还要注意的是,中间可能还会发生这种操做:当前的 .drawer 正在隐藏、但尚未彻底隐藏的时候,又再次点击显示。对于这种状况,咱们还须要在上述的 show() 函数中手动再移除一次事件监听器:

function show() {
  drawer.removeEventListener('transitionend', listener);

  /* ... */
}
复制代码

transitionend 是冒泡事件

transitionend 是冒泡事件,所以使用 transitionend 事件时,有一个边缘状况须要考虑:在 DOM 中,事件冒泡是从子元素传递到祖先元素的。若是 .drawer 元素内部包含具备过渡效果的子元素,那么就会引起问题。

若是一个子元素的过渡效果先于 .drawer 元素完成,那么因为事件冒泡,该元素上的 transitionend 事件监听器会提早触发。

为了不这个问题的发生,能够在事件监听器中检查当前触发 transitionend 事件的目标元素是不是 .drawer。是的话,在再执行以前的处理逻辑:

const listener = e => {
  // 只有在目标元素是 .drawer 的状况下,在再执行以前的处理逻辑
  if(e.target === drawer) {
    drawer.setAttribute('hidden', true);
    drawer.removeEventListener('transitionend', listener);
  }
};
复制代码

使用 setTimeout 等待

使用 transitionend 事件有一个局限性,就是只能处理一个元素的过渡场景。若是要实现的是下面这种涉及到多个元素过渡效果的 交错动画 场景,就不适应了。

GIF.gif

点击这里 查看 demo

须要实现的功能:点击右上角按钮实现侧边菜单带有过渡效果的显示和隐藏;同时点击菜单子连接也会触发菜单的隐藏过渡效果。这须要咱们等待全部子元素的过渡效果完成,才能将 hidden 属性添加给 .drawer

最开始的处理方式,是强制等待全部子元素的 transitionend 事件都触发后,再作处理。可是,若是用户是在进行快速切换,那么就可能有问题了:有些子元素就没有发生过渡,那么这些元素也永远不会触发 transitionend 事件。

为了解决这个问题,咱们选择使用 setTimeout,而不是去监听 transitionend 事件:

function hide() {
  element.classList.remove(visibleClass);
  const timeoutDuration = 200;
  const timeout = setTimeout(() => {
    element.setAttribute('hidden', true);
  }, timeoutDuration);
}
复制代码

除此以外,为了不前面发生的(菜单正在隐藏时就点击显示)场景,不要忘记在 show() 中手动清除定时器:

function show() {
  if (this.timeout) {
    clearTimeout(this.timeout);
  }

  /* ... */
}
复制代码

我最终建立的 npm 包中,同时提供对 timeout  和 transitionend 选项的支持, 方便你们根据须要使用。

使用 npm 包

通过上面的分析,你们可以知道隐藏元素的过渡效果实现起来仍是稍微有点复杂的。个人 npm 包将本例所提到到两类场景所使用的代码,都包装到了 transitionHiddenElement 模块中。

使用方式以下:

import { transitionHiddenElement } from '@cloudfour/transition-hidden-element';

const drawerTransitioner = transitionHiddenElement({
  element: document.querySelector('.drawer'),
  visibleClass: 'is-open',
});

// 使用
drawerTransitioner.show();
drawerTransitioner.hide();
drawerTransitioner.toggle();
复制代码

我但愿这个方案对你们的项目有所帮助。你能够从 npm 获取或者在 GitHub 上查看源代码!

(正文完)


广告时间(长期有效)

我有一位好朋友开了一间猫舍,在此帮她宣传一下。如今猫舍里养的都是布偶猫。若是你也是个爱猫人士而且有须要的话,不妨扫一扫她的【闲鱼】二维码。不买也没关系,看看也行。

(完)

相关文章
相关标签/搜索