tohokuaikiのチラシの裏

技術的ネタとか。

ReactでModalを消すときにフォーカスが消える要素に当たってますよってエラーが出たので何とかしてもらった。

こんなエラーが出た。

Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
Element with focus: button
Ancestor with aria-hidden: <div class=​"modal fade" tabindex=​"-1" style=​"display:​ block;​" aria-hidden=​"true" >​…​</div>​

Modalが消える要素なのに、フォーカスが当たっていたらダメですよと。

手っ取り早くcloseする時にフォーカスをモーダルの親コンポーネントのボタンとかにRefを当ててそこにしたら消えた。

import React, { useRef, useState } from 'react';

const MyModal = () => {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const openModal = () => setIsOpen(true);
  const closeModal = () => {
    setIsOpen(false);
    triggerRef.current?.focus(); // フォーカスを元のトリガーボタンに戻す
  };

  return (
    <div>
      <button ref={triggerRef} onClick={openModal}>
        Open Modal
      </button>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0, 0, 0, 0.5)' }}
        >
          <div style={{ background: 'white', padding: '20px' }}>
            <p>Modal Content</p>
            <button onClick={closeModal}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
};

export default MyModal;

ただ、データ一覧があって各データにボタンがあったりそのModalをトリガーしたボタンが消えてしまっていては意味がない。

そこで、こんな感じにする。ポイントは、

  • DIV要素にtabIndexを付けてフォーカス可能にする。
  • focus({ preventScroll: true })でフォーカス時にスクロールしないようにする。
import React, { useRef, useState } from 'react';

const MyModal = () => {
  const [isOpen, setIsOpen] = useState(false);
  const triggerRef = useRef<HTMLDivElement>(null); // ボタン以外の要素

  const openModal = () => setIsOpen(true);

  const closeModal = () => {
    setIsOpen(false);
    triggerRef.current?.focus({ preventScroll: true }); // tabindexが設定されていれば問題なく動作
  };

  return (
    <div>
      <div
        ref={triggerRef}
        tabIndex={0} // フォーカス可能にする
        onClick={openModal}
        style={{
          display: 'inline-block',
          padding: '10px',
          background: 'lightblue',
          cursor: 'pointer',
        }}
      >
        Open Modal (Non-button Element)
      </div>
      {isOpen && (
        <div
          role="dialog"
          aria-modal="true"
          style={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            background: 'rgba(0, 0, 0, 0.5)',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <div style={{ background: 'white', padding: '20px', borderRadius: '8px' }}>
            <p>Modal Content</p>
            <button onClick={closeModal}>Close</button>
          </div>
        </div>
      )}
    </div>
  );
};

export default MyModal;

というのをChatGPTに教えてもらった。