10日目: react-js/react-modal を探訪

React でモーダルダイアログを表示したいが試したことがないので、react-js/react-modal をさくっと試してみる。

npm create vite@latest でアプリケーションを作成して、react-modal をインストール。

$ npm install react-modal
$ npm install -D @types/react-modal

src/App.tsx を以下のように編集。

diff --git a/src/App.tsx b/src/App.tsx
index afe48ac..c6d4011 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,9 +2,23 @@ import { useState } from 'react'
 import reactLogo from './assets/react.svg'
 import viteLogo from '/vite.svg'
 import './App.css'
+import Modal from 'react-modal'
+
+const customStyles = {
+  content: {
+    top: '50%',
+    left: '50%',
+    right: 'auto',
+    bottom: 'auto',
+    marginRight: '-50%',
+    transform: 'translate(-50%, -50%)',
+    color: '#000',
+  },
+};

 function App() {
   const [count, setCount] = useState(0)
+  const [modalIsOpen, setIsOpen] = useState(false)

   return (
     <>
@@ -25,6 +39,24 @@ function App() {
           Edit <code>src/App.tsx</code> and save to test HMR
         </p>
       </div>
+      <button onClick={() => setIsOpen(true)}>Open Modal</button>
+      <Modal
+        isOpen={modalIsOpen}
+        onRequestClose={() => setIsOpen(false)}
+        style={customStyles}
+        contentLabel="Example Modal"
+      >
+        <h2>Hello, react-modal</h2>
+        <button onClick={() => setIsOpen(false)}>close</button>
+        <div>I am a modal</div>
+        <form>
+          <input />
+          <button>tab navigation</button>
+          <button>stays</button>
+          <button>inside</button>
+          <button>the modal</button>
+        </form>
+      </Modal>
       <p className="read-the-docs">
         Click on the Vite and React logos to learn more
       </p>

動いた。かんたん。せっかくなのでテストも書いてみる。

$ npm i -D vitest jsdom @testing-library/react @testing-library/jest-dom

vitest.config.ts を追加。

/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
  plugins: [react()],
  test: {
    // ... Specify options here.
    environment: 'jsdom',
  },
})

package.jsonscriptstest: "vitest" を追加。

src/App.test.tsx にテストを書く。

import { expect, test } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';

test('モーダルが正しく動作すること', () => {
  render(<App />);

  // モーダルが表示されていないことを確認
  expect(screen.queryByText('Hello, react-modal')).toBeNull();

  // ボタンをクリック
  fireEvent.click(screen.getByText('Open Modal'));

  // モーダルが表示されていることを確認
  expect(screen.queryByText('Hello, react-modal')).toBeInTheDocument();
});

テストを実行。

$ npm run test run

> hello-react-modal@0.0.0 test
> vitest run


 RUN  v0.34.6 /path/tohello-react-modal

stderr | src/App.test.tsx > モーダルが正しく動作すること
Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting `ariaHideApp={false}`.

 ✓ src/App.test.tsx (1)
   ✓ モーダルが正しく動作すること

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  12:52:14
   Duration  2.15s (transform 198ms, setup 0ms, collect 540ms, tests 122ms, environment 642ms, prepare 209ms)

パスしたのだが、警告が表示される。

Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting ariaHideApp={false}.

警告: 反応モーダル: アプリ要素が定義されていません。 Modal.setAppElement(el) を使用するか、appElement={el} を設定してください。これは、モーダルを開いたときにスクリーン リーダーにメイン コンテンツが表示されないようにするために必要です。推奨されませんが、「ariaHideApp={false}」を設定することでオプトアウトできます。

警告を抑止するだけならば、以下でイケる。

diff --git a/src/App.tsx b/src/App.tsx
index c6d4011..77dc637 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -44,6 +44,7 @@ function App() {
         isOpen={modalIsOpen}
         onRequestClose={() => setIsOpen(false)}
         style={customStyles}
+        ariaHideApp={false}
         contentLabel="Example Modal"
       >
         <h2>Hello, react-modal</h2>

しかしながらこれだとアクセシビリティが損なわれるので、Modal.setAppElement(el) を使うべきらしい。 臭いものにフタはよくない。わかる。

setAppElement に関してはドキュメントにも記載があった。

reactcommunity.org

解決方法としてはドキュメントルートを示して、モーダル以外の要素が隠れていることを明示すればよいとのこと。

diff --git a/src/App.tsx b/src/App.tsx
index c6d4011..dbec4be 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -16,6 +16,8 @@ const customStyles = {
   },
 };

+Modal.setAppElement('#root')
+
 function App() {
   const [count, setCount] = useState(0)
   const [modalIsOpen, setIsOpen] = useState(false)

ただし、これだとテストでエラーになるので以下の議論が起きていた。

github.com

解決策は多数あるが目的は達成できているので、今日の探訪はこれで終わり。