iframe で clickOutside をトリガーできません

序文

プロジェクトで発生した問題を記録します。el-dropdown の外側をクリックしても正常に閉じませんでした。他の同僚に相談したところ、彼のリマインダーで答えを見つけました。iframe に関連していることが判明しました。iframe に埋め込まれたページで、ボタンをクリックしてドロップダウンを展開し、その後 iframe をクリックすると、ドロップダウンの clickOutside イベントをトリガーできず、その結果ドロップダウンを閉じることができないことがわかります。

clickOutside をトリガーできないのはなぜですか

現在、Element、Ant Design、iView などのほとんどの UI コンポーネント ライブラリは、マウス イベントを通じて処理されます。次の段落は、iView の clickOutside コードです。iView は、クリック イベントを Document に直接バインドします。クリック イベントがトリガーされ、クリック対象がバインドされた要素に含まれているかどうかを判断し、含まれていない場合はバインドされた関数を実行します。

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 に直接バインドすると、iframe をクリックしてもイベントはトリガーされません。

問題がどこにあるのかがわかったら、次はそれを解決する方法を考えましょう。

イベントを iframe の body 要素にバインドする

いくつかの特別な方法でイベントを iframe にバインドできますが、このアプローチは洗練されておらず、問題もあります。このようなシナリオについて考えてみましょう。左側はサイドバー (ナビゲーション バー)、上部はいくつかのドロップダウンまたは選択コンポーネントを含むヘッダー、そして下部はページ領域です。

ただし、これらのページの一部は iframe に埋め込まれており、一部は現在のシステムのページです。この方法を使用すると、ルートを切り替えるときにページに iframe が含まれているかどうかを常に判断し、イベントの再バインド/アンバインドを行う必要があります。また、iframe と現在のシステムが同じドメインにない場合 (ほとんどの場合、同じドメインにありません)、このアプローチは無効です。

マスクレイヤーを追加

iframe に透明なマスク レイヤーを追加できます。ドロップダウンをクリックすると、透明なマスク レイヤーが表示され、ドロップダウンの外側の領域またはマスク レイヤーをクリックすると、clickOutside イベントが送出され、マスク レイヤーが表示されます。 clickOutside イベントをトリガーすることはできますが、問題があり、ユーザーがクリックした領域が iframe ページ内のボタンである場合、最初のクリックは有効になりません。これはインタラクションにはあまり適していません。

focusin および focusout イベントをリッスンする

実際、なぜこれを行うためにマウス イベントを使用する必要があるのか​​という考え方を変えることができます。focusin および focusout イベントは、この状況に最適です。

バインドされた要素の外側をクリックすると、 focusout イベントがトリガーされ、バインドした関数の呼び出しを遅らせるタイマーを追加できます。そして、Dropdownなどのバインディング要素をクリックするとfocusinイベントが発生しますが、その際に対象がバインディング要素に含まれるかどうかを判断し、バインディング要素に含まれる場合はタイマーをクリアします。

ただし、focusin および focusout イベントを使用するには、バインドされた要素をフォーカス可能な要素に変更するという問題を解決する必要があるため、要素をフォーカス可能な要素に変更するにはどうすればよいでしょうか。要素の tabindex 属性を -1 に設定すると、その要素はフォーカス可能な要素になります。

要素がフォーカス可能な要素になった後、その要素がフォーカスを取得すると、ブラウザによってデフォルトのハイライト スタイルが追加されることに注意してください。このスタイルが必要ない場合は、アウトライン プロパティを none に設定できます。

ただし、この方法は優れていますが、ブラウザの互換性など、まだいくつかの問題があります。以下は MDN が提供するブラウザの互換性状況です。図から、Firefox の下位バージョンではこのイベントがサポートされていないことがわかります。プロジェクトが Firefox ブラウザの下位バージョンをサポートしているかどうかを検討する必要があります。
ここに画像の説明を挿入

フォーカス外部ライブラリを使用する

focus-outside は、上記の問題を解決するために作成されたウェアハウスで、コードは 200 行未満です。また、使い方も非常に便利で、バインドとアンバインドの 2 つのメソッドしかなく、他のサードパーティ ライブラリに依存せず、複数の要素に対する同じ関数のバインドをサポートしています。

同じ関数を複数の要素にバインドする理由は、要素と Ant デザインとの互換性のためです。要素と Ant デザインは Dropdown を body 要素に挿入し、ボタンをクリックして Dropdown を表示するときに、そのボタンとコンテナーが分離されるためです。ドロップダウン領域をクリックすると、ボタンはフォーカスを失い、focusout イベントをトリガーします。この時点では実際にはドロップダウンを閉じたくないので、これらを同じバインディング ソースとして扱います。

ここでは、Element と Ant Design が body 要素にポップアップ レイヤーを配置する理由について説明します。ドロップダウンが親要素の下に直接マウントされている場合、親要素のスタイルの影響を受けるためです。たとえば、親要素に overflow: hidden がある場合、ドロップダウンが非表示になることがあります。

使いやすい

// import { bind, unbidn } from 'focus-outside'
// 建议使用下面这种别名,防止和你的函数命名冲突了。
import {
    
     bind: focusBind, unbind: focusUnbind } from 'focus-outside'

// 如果你是使用 CDN 引入的,应该这样使用
// <script src="https://unpkg.com/[email protected]/lib/index.js"></script>
// const { bind: focusBind, unbind: focusUnbind } = FocusOutside

const elm = document.querySelector('#dorpdown-button')
// 绑定函数
focusBind(elm, callback)

function callback () {
    
    
  console.log('您点击了 dropdown 按钮外面的区域')
  // 清除绑定
  focusUnbind(elm, callback)
}

知らせ

前述したように、要素がフォーカス可能な要素になった後、ブラウザーはフォーカスを取得したときにその要素にハイライト スタイルを追加します。このスタイルを表示したくない場合は、この要素の CSS プロパティのアウトラインを次のように設定する必要があります。なし。className パラメータは focsout-outside 0.5.0 バージョンで追加され、デフォルトのクラス名 focus-outside が各バインド要素に追加されます。className パラメータを渡すことでクラス名をカスタマイズできます。unbind 関数が実行されると、要素を削除するとクラス名が変更されます。

<div id="focus-ele"></div>

// js
const elm = document.querySelector('#focus-ele')
// 默认类名是 focus-outside
focusBind(elm, callback, 'my-focus-name')

// css
// 如果你需要覆盖所有的默认样式,可以在这段代码放在全局 CSS 中。
.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
<template>
  <div v-outside="handleOutside"></div>
</template>

<script>
import outside from './outside.js'

export default {
    
    
  directives: {
    
     outside },

  methods: {
    
    
    handleOutside () {
    
    
      // 做点什么...
    }
  }

要素での使用

<tempalte>
  <el-dropdown
    ref="dropdown"
    trigger="click">
    <span class="el-dropdown-link">
      下拉菜单<i class="el-icon-arrow-down el-icon--right"></i>
    </span>
    <el-dropdown-menu
      ref="dropdownContent"
      slot="dropdown">
      <el-dropdown-item>黄金糕</el-dropdown-item>
      <el-dropdown-item>狮子头</el-dropdown-item>
      <el-dropdown-item>螺蛳粉</el-dropdown-item>
      <el-dropdown-item>双皮奶</el-dropdown-item>
      <el-dropdown-item>蚵仔煎</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
import {
    
     bind: focusBind, unbind: focusUnbind } from 'focus-outside'

export default {
    
    
  mounted () {
    
    
    focusBind(this.$refs.dropdown.$el, this.$refs.dropdown.hide)
    focusBind(this.$refs.dropdownContent.$el, this.$refs.dropdown.hide)
  },

  destoryed () {
    
    
    focusUnbind(this.$refs.dropdown.$el, this.$refs.dropdown.hide)
    focusUnbind(this.$refs.dropdownContent.$el, this.$refs.dropdown.hide)
  }
}
</script>

Ant デザインで使用

import {
    
     Menu, Dropdown, Icon, Button } from 'antd'
import {
    
     bind: focusBind, unbind: focusUnbind } from 'focus-outside'

function getItems () {
    
    
  return [1,2,3,4].map(item => {
    
    
    return <Menu.Item key={
    
    item}>{
    
    item} st menu item </Menu.Item>
  })
}

class MyMenu extends React.Component {
    
    
  constructor (props) {
    
    
    super(props)
    this.menuElm = null
  }

  render () {
    
    
    return (<Menu ref="menu" onClick={
    
    this.props.onClick}>{
    
    getItems()}</Menu>)
  }

  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 = (<MyMenu outside={
    
     this.handleOutside } onClick={
    
     this.handleClick } />)
    return (
      <Dropdown
        ref="divRef"
        visible={
    
    this.state.visible}
        trigger={
    
    ['click']}
        overlay={
    
     menu }>
        <Button style={
    
    {
    
     marginLeft: 8 }} onClick={
    
     this.handleClick }>
          Button <Icon type="down" />
        </Button>
      </Dropdown>
    )
  }

  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(
  <MyDropdown/>,
  document.getElementById('container')
)

要約する

iframe 要素はマウス イベントをトリガーできません。iframe が埋め込まれたシステムで clickOutside がトリガーされる場合、より良い方法は、focusin および focusout イベントを使用し、HTML 属性 tabindex を -1 に設定して、要素をフォーカス可能な要素に変えることです。ブラウザは、フォーカス可能な要素にデフォルトのハイライト スタイルを追加します。このスタイルが必要ない場合は、CSS プロパティのアウトラインを none に設定できます。

関連リンク

MDN focusin
MDN focusout
focus-outside
tabindex
HTML の tabindex 属性と Web ページのキーボード アクセシビリティについて話す

Guess you like

Origin blog.csdn.net/qq_44376306/article/details/127387380