前言
在公司的一次小組分享會上,組長給我們分享了一個他在項目中遇到的一個問題。愛掏網 - it200.com在一個嵌入 iframe 的系統中,當我們點擊按鈕展開 Dropdown 展開后,再去點擊 iframe 發現無法觸發 Dropdown 的 clickOutside 事件,導致 Dropdown 無法關閉。愛掏網 - it200.com
查看在線示例
為什么無法觸發 clickOutside
目前大多數的 UI 組件庫,例如 Element、Ant Design、iView 等都是通過鼠標事件來處理, 下面這段是 iView 中的 clickOutside 代碼,iView 直接給 Document 綁定了 click 事件,當 click 事件觸發時候,判斷點擊目標是否包含在綁定元素中,如果不是就執行綁定的函數。愛掏網 - it200.com
bind (el, binding, vnode) {
function documentHandler (e) {
if (el.contains(e.target)) {
return false;
}
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = documentHandler;
document.addEventListener('click', documentHandler);
}
復制代碼
但 iframe 中加載的是一個相對獨立的 Document,如果直接在父頁面中給 Document 綁定 click 事件,點擊 iframe 并不會觸發該事件。愛掏網 - it200.com
知道問題出現在哪里,接下來我們來思考怎么解決?
給 iframe 的 body 元素綁定事件
我們可以通過一些特殊的方式給 iframe 綁定上事件,但這種做法不優雅,而且也是存在問題的。愛掏網 - it200.com我們來想想一下這樣一個場景,左邊是一個側邊欄(導航欄),上面是一個 Header 里面有一些 Dropdown 或是 Select 組件,下面是一個頁面區域。愛掏網 - it200.com
但這些頁面有的是嵌入 iframe,有些是當前系統的頁面。愛掏網 - it200.com如果使用這種方法,我們在切換路由的時候就要不斷的去判斷這個頁面是否包含 iframe,然后重新綁定/解綁事件。愛掏網 - it200.com而且如果 iframe 和當前系統不是同域(大多數情況都不是同域的),那么這種做法是無效的。愛掏網 - it200.com
添加遮罩層
我們可以通過給 iframe 添加一個透明遮罩層,點擊 Dropdown 的時候顯示透明遮罩層,點擊 Dropdown 之外的區域或遮罩層,就派發 clickOutside 事件并關閉遮罩層,這樣雖然可以觸發 clickOutside 事件,但存在一個問題,如果用戶點擊的區域正好是 iframe 頁面中的某個按鈕,那么第一次點擊是不會生效的,這種做法對于交互不是很友好。愛掏網 - it200.com
監聽 focusin 與 focusout 事件
其實我們可以換一種思路,為什么一定要用鼠標事件來做這件事呢?focusin 與 focusout 事件就很適合處理當前這種情況。愛掏網 - it200.com
當我們點擊綁定的元素之外時就觸發 focusout 事件,這時我們可以添加一個定時器,延時調用我們綁定的函數。愛掏網 - it200.com而當我們點擊綁定元素例如 Dropdown 會觸發 focusin 事件,這時候我們判斷目標是否包含在綁定元素中,如果包含在綁定元素中就清除定時器。愛掏網 - it200.com
不過使用 focusin 與 focusout 事件需要解決一個問題,那就是要將綁定的元素變成 focusable 元素,那么怎么將元素變成 focusable 元素呢?我們通過將元素的 tabindex 屬性置為 -1 , 該元素就變成 focusable 的元素。愛掏網 - it200.com
需要注意的是,元素變成 focusable 元素之后,當它獲取焦點的時候,瀏覽器會給它加上默認的高亮樣式,如果你不需要這種樣式可以將 outline 屬性設置為 none。愛掏網 - it200.com
不過這種方法雖然很棒,但是也會存在一些問題,瀏覽器兼容性,下面是 MDN 給出的瀏覽器兼容情況,從圖中可以看出 Firefox 低版本不支持這個事件,所以你需要去權衡你的項目是否支持低版本的 Firefox 瀏覽器。愛掏網 - it200.com
使用 focus-outside 庫
focus-outside?正是為了解決上述問題所創建的倉庫,代碼不到 200 行。愛掏網 - it200.com使用起來也非常方便,它只有兩個方法,bind 與 unbind,不依賴其他第三方庫,并且支持為多個元素綁定同一個函數。愛掏網 - it200.com
為什么要給多個元素綁定同一個函數,這么做是為了兼容 Element 與 Ant Design,因為 Element 與 Ant Design 會將 Dropdown 插入 body 元素中,它的按鈕和容器是分離的,當我們點擊按鈕顯示 Dropdown,當我們點擊 Dropdown 區域,這時候按鈕會失去焦點觸發 focusout 事件。愛掏網 - it200.com事實上我們并不希望這時關閉 Dropdown,所以我將它們視為同一個綁定源。愛掏網 - it200.com
這里說明下 Element 與 Ant Design 為什么要將彈出層放在 body 元素中,因為如果直接將 Dropdown 掛載在父元素下,會受到父元素樣式的影響。愛掏網 - it200.com比如當父元素有 overflow: hidden,Dropdown 就有可能被隱藏掉。愛掏網 - it200.com
簡單使用
// import { bind, unbidn } from 'focus-outside'
// 建議使用下面這種別名,防止和你的函數命名沖突了。愛掏網 - it200.com
import { bind: focusBind, unbind: focusUnbind } from 'focus-outside'
// 如果你是使用 CDN 引入的,應該這樣使用
//
// const { bind: focusBind, unbind: focusUnbind } = FocusOutside
const elm = document.querySelector('#dorpdown-button')
// 綁定函數
focusBind(elm, callback)
function callback () {
console.log('您點擊了 dropdown 按鈕外面的區域')
// 清除綁定
focusUnbind(elm, callback)
}
復制代碼
查看在線示例
注意
前面說到過元素變成 focusable 元素后,當它獲取焦點瀏覽器會給它加上高亮樣式,如果你不希望看到和這個樣式,你需要將這個元素的 CSS 屬性 outline 設置為 none。愛掏網 - it200.comfocsout-outside 0.5.0 版本中新增 className 參數,為每個綁定的元素添加 focus-outside 默認類名,你要可以通過傳遞 className 參數自定義類名,當執行 unbind 函數時候會將類名從元素上刪除 。愛掏網 - it200.com
// js
const elm = document.querySelector('#focus-ele')
// 默認類名是 focus-outside
focusBind(elm, callback, 'my-focus-name')
// css
// 如果你需要覆蓋所有的默認樣式,可以在這段代碼放在全局 CSS 中。愛掏網 - it200.com
.my-focus-name {
outline: none;
}
復制代碼
在 Vue 中使用
// outside.js
export default {
bind (el, binding) {
focusBind(el, binding.value)
},
unbind (el, binding) {
focusUnbind(el, binding.value)
}
}
// xx.vue
復制代碼
查看在線示例
在 Element 中使用
下拉菜單
黃金糕 獅子頭 螺螄粉 雙皮奶 蚵仔煎 復制代碼
查看在線示例
在 Ant Design 中使用
import { Menu, Dropdown, Icon, Button } = antd
import { bind: focusBind, unbind: focusUnbind } = 'focus-outside'
function getItems () {
return [1,2,3,4].map(item => {
return {item} st menu item
})
}
class MyMenu extends React.Component {
constructor (props) {
super(props)
this.menuElm = null
}
render () {
return ()
}
componentDidMount () {
this.menuElm = ReactDOM.findDOMNode(this.refs.menu)
if (this.menuElm && this.props.outside) focusBind(this.menuElm, this.props.outside)
}
componentWillUnmount () {
if (this.menuElm && this.props.outside) focusUnbind(this.menuElm, this.props.outside)
}
}
class MyDropdown extends React.Component {
constructor (props) {
super(props)
this.dropdownElm = null
}
state = {
visible: false
}
render () {
const menu = ( )
return (
)
}
componentDidMount () {
this.dropdownElm = ReactDOM.findDOMNode(this.refs.divRef)
if (this.dropdownElm) focusBind(this.dropdownElm, this.handleOutside)
}
componentWillUnmount () {
if (this.dropdownElm) focusUnbind(this.dropdownElm, this.handleOutside)
}
handleOutside = () => {
this.setState({ visible: false })
}
handleClick = () => {
this.setState({ visible: !this.state.visible })
}
}
ReactDOM.render(
,
document.getElementById('container')
)
復制代碼
查看在線示例
總結
iframe 元素無法觸發鼠標事件,如果在嵌入 iframe 的系統中觸發 clickOutside, 更好的做法是使用 focusin 與 focusout 事件,將 HTML 屬性 tabindex 設置為 -1 可以將元素變成 focusable 元素。愛掏網 - it200.com瀏覽器會給 focusable 元素加上默認的高亮樣式,如果你不需要這種樣式,可以將 CSS 屬性 outline 設置為 none。愛掏網 - it200.com
相關鏈接
- MDN focusin
- MDN focusout
- focus-outside
- 說說 tabindex 的那些事兒
- HTML tabindex 屬性與 web 網頁鍵盤無障礙訪問
原文發布時間為:2024年07月02日
作者:掘金
本文來源:掘金?如需轉載請聯系原作者