🍎

iOS / SafariでonBlurが発火しない問題を解決したい (React)

2024/05/22に公開

対象

Safariブラウザのみで発生する 「onBlurが発火しない問題」 の解決方法についてお話しします。
(Reactを対象にお話ししますが、問題の原因と解消方法自体は他のフレームワークでも参考に頂けるかと。)

問題

下記の様にonBlurで何か処理をしている場合に、Safariブラウザでのみ上手く動かない。。。という問題に直面しました。

const SomethingComponent: React.FC = () => {
  const [isActive, setIsActive] = useState(false)
  return (
    <button
      onClick={() => {
        console.log('set active')
        setIsActive(true)
      }}
      onBlur={() => {
        console.log('set inactive')
        setIsActive(false)
      }}
    >
     {isActive ? 'アクティブ' : '非アクティブ'}
    </button>
  )
}

結果

🙆‍♂️Chrome 🙅Safari
Chrome test

原因

Safariでは、Focusが発火しない模様。
https://stackoverflow.com/questions/61245883/why-blur-and-focus-doesnt-work-on-safari

ワークアラウンド

下記の様にコンポーネントのクリックハンドラーに要素を強制的にフォーカスさせる処理を追加することで、
Safariブラウザでもblurイベントを検知できるようになります。
(ちょっとまだ挙動は異なりますが、色々調整すれば要件に沿った挙動は満たせるかと)

const SomethingComponent: React.FC = () => {
  const ref = useRef<HTMLButtonElement>(null)
  const [isActive, setIsActive] = useState(false)
  return (
    <button
      ref={ref}
      onClick={() => {
        console.log('set active')
        ref.current?.focus()
        setIsActive(true)
      }}
      onBlur={() => {
        console.log('set inactive')
        setIsActive(false)
      }}
    >
     {isActive ? 'アクティブ' : '非アクティブ'}
    </button>
  )
}

補足

下記の様に、クリックイベント発火時に、強制的にフォーカスを当てる処理を追加したラッパーを作成し、
ボタンコンポーネントの代わりに使うことで、影響範囲少なく使えるかと思います。

import { ButtonHTMLAttributes, useRef } from 'react'

const BlurButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
  const ref = useRef<HTMLButtonElement>(null)
  return (
    <button
      ref={ref}
      {...props}
      onClick={(e) => {
        ref.current?.focus() // focusイベントを強制発火
        props?.onClick && props.onClick(e)
      }}
    >
      {props.children}
    </button>
  )
}

export default BlurButton

実際の動いているアプリの挙動は下記の集合駅検索サービスKokone上で確認できます!
https://kokone-app.com/

Discussion