原文连接:Transitioning Hidden Elements,Paul Hebertjavascript
若是你在设置了 hidden
属性或者 display: none
声明的元素上使用 CSS 过渡(transition
),这是个很差实现的效果。我遇到这个问题不少次了,最后决定编写一个 npm 软件包 提供一个可重用的解决方案。css
有不少种隐藏元素的方法。好比使用 visibility
、opacity
、transform
、position
甚至是 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
咱们如何实现这种方式的滑入滑出效果,同时还能将隐藏元素从文档流中移除呢?浏览器
继续看。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
。
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);
}
};
复制代码
使用 transitionend
事件有一个局限性,就是只能处理一个元素的过渡场景。若是要实现的是下面这种涉及到多个元素过渡效果的 交错动画 场景,就不适应了。
点击这里 查看 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 包将本例所提到到两类场景所使用的代码,都包装到了 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 上查看源代码!
(正文完)
广告时间(长期有效)
我有一位好朋友开了一间猫舍,在此帮她宣传一下。如今猫舍里养的都是布偶猫。若是你也是个爱猫人士而且有须要的话,不妨扫一扫她的【闲鱼】二维码。不买也没关系,看看也行。
(完)