class ModalPortal extends PureComponent { constructor(props) { ... this.node.addEventListener('click', this.handleClick); } componentWillUnmount() { this.node.removeEventListener('click', this.handleClick); } handleClick = e => { const { closeModal } = this.props; const target = e.path[0]; if (target === this.node) { onClose(); } }; ... }
按下 ESC 关闭
上面我们实现了点击遮罩层关闭模态框,然后我们应该实现按下 ESC 关闭这个功能。通点击事件一样,我们只需要监听 keydown 事件就可以了,这一次不用考虑到底是哪里触发的问题了,只要 overlay 监听到 keydown 就关闭模态框。但是这里也有一个小问题,就是 overlay 是 div,默认是监听不到 keydown 事件的,对于这个问题,我们可以给 div 添加一个 tabIndex: 0 的属性,通过指定 tabIndex,将 div 赋予 focusable 的能力,当模态框打开后,我们手动调用 focus 将焦点放到 overlay 上,这样就能监听到键盘事件。
const ESC_KEY = 27; class ModalPortal extends PureComponent { constructor(props) { ... this.node = createElement('div', { class: `modal-${random()} ${props.className}`, tabIndex: 0, }); this.node.addEventListener('keydown', this.handleKeyDown); } componentWillUnmount() { ... this.node.removeEventListener('keydown', this.handleKeyDown); } checkIfVisible = () => { const { visible } = this.props; if (visible) { ... this.node.focus(); } else { ... } }; handleKeyDown = e => { const { closeModal } = this.props; if (e.keyCode === ESC_KEY) { closeModal(); } }; ... }
消除滚动条导致的页面抖动
在上面的防止遮罩层后面背景滚动是通过在 body 上设置 overflow: hidden 来防止滚动,但是如果 body 已经有了滚动条,那么 overflow 属性会造成滚动条消失。滚动条在 chrome 上为 15px,打开和关闭模态框会使页面不停地对这 15px 做处理,导则页面抖动。为了防止抖动,我们可以在滚动条消失后给 body 添加 15px 的右边距,滚动条出现后在删除右边距,通过这样的方法,页面就不会发生抖动了。
因为各个浏览器的标准不一致,所以我们应该想办法计算出滚动条的宽度。为了计算出滚动条的宽度,我们可以使用 innerWidth 和 offsetWidth 这两个属性。offsetWidth 是包含边框的长度,理所当然的包含了滚动条的宽度,只需要使用 offsetWidth 减去 innerWidth,得到的差值就是滚动条的宽度了。我们可以手动创建一个隐藏的有宽度的且有滚动条的元素,然后通过这个元素来获取滚动条的宽度。
const calcScrollBarWidth = function() { const testNode = createElement('div', { style: 'visibility: hidden; position: absolute; width: 100px; height: 100px; z-index: -999; overflow: scroll;' }); document.body.appendChild(testNode); const scrollBarWidth = testNode.offsetWidth - testNode.clientWidth; document.body.removeChild(testNode); return scrollBarWidth; }; const preventJitter = function() { const scrollBarWidth = calcScrollBarWidth(); if (parseInt(document.documentElement.style.marginRight) === scrollBarWidth) { document.documentElement.style.marginRight = 0; } else { document.documentElement.style.marginRight = scrollBarWidth + 'px'; } };
结语
我们上面讨论了做好一个模态框所需要考虑的技术,但是肯定还有不完善和错误的地方,所以,如果错误的地方请给我提 issue 我会尽快修正。代码