# 読んでいる最中に自動更新で書き換わってしまう新着一覧

Source: https://easeweb.dev/learn/auto-updating-feeds
Topics: キーボードとフォーカス > ページは利用者が求めたときだけ変わる (WCAG 3.2.5)
The fix: 一覧は、利用者が求めたときだけ変えます。方法は2つあります。取得は続けたまま件数を一覧の前の「新着1件を表示」ボタンに書き、押されたときに新しい記事を加える方法と、一覧の前に自動更新をオフにできるチェックボックスを置き、その選択を覚えておく方法です。
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/change-on-request.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F61 https://www.w3.org/WAI/WCAG22/Techniques/general/G76 https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F41 https://amp.dev/documentation/components/amp-live-list

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

地域のニュースサイト「Harbour Herald」には新着ページがあります。その日の記事を新しい順に並べた番号付きの一覧で、記事ごとに掲載時刻が付いています。古いニュースを表示し続けないよう、スクリプトが60秒ごとに一覧を取得し直し、ページ上の一覧に上書きします。一覧全体を眺める人にとっては、新しい記事が上に現れるだけです。

スクリーンリーダーの利用者が見出しをTabキーで順にたどり、3件目の「図書館、週末の開館時間を延長」で止まって、開くかどうか考えています。ページを読み込んでから1分たつと、一覧が上書きされます。利用者がいたリンクはページから取り除かれ、同じ内容の新しいリンクに置き換わるため、フォーカスはページそのものに戻り、何が起きたのかを伝えるものもありません。リンクが毎回新しい要素になるので、新しい記事が来ていなくても、同じことが1分ごとに起こります。10時43分にフェリーの記事が入ると、図書館の記事は1つ下にずれます。画面拡大ソフトでこの記事を拡大していた人の目の前で、記事は視界から外れ、代わりに橋の記事がその場所に来ます。ページには、更新を止める手段がありません。

## 確認できる問題

達成基準3.2.5「要求による変化」（レベルAAA）は、コンテキストの変化が利用者の求めたときにだけ起こるか、利用者がそうした変化を止められることを求めています。WCAGは、ページ全体を一度に見られない人を混乱させかねない大きな変化をコンテキストの変化と呼び、フォーカスを別の場所へ移すことや、ページの内容を大きく並べ替えることもそこに含めています。一覧の上書きは、その両方を起こします。主要な内容が勝手に置き換わり、フォーカスのあるリンクも一緒に消えます。フォーカスを持つ要素が取り除かれると、ブラウザーはフォーカスをページそのものへ移し、`document.activeElement` はbodyを返します。W3Cはこれを失敗例F61として挙げています。メインのビューポートの内容が自動更新で変わり、利用者が止められないというもので、最初の例は、最新の見出しを出すために自動で更新し続けるニュースサイトです。自動更新に対する十分な達成方法G76は、自動で更新する代わりに、更新を利用者が求められるようにするものです。

ブラウザーの側に、この書き換えを止める理由はありません。タイマーは誰かが読んでいるかどうかに関係なく動き、`innerHTML` は、新しいマークアップが同じでも、置き換える要素をフォーカスのあるものも含めてすべて捨てます。

この一覧は、ページのほかの部分と並んで自動で更新されるため、レベルAの2.2.2「一時停止、停止、非表示」の対象にもなります。2.2.2は、こうした更新を一時停止、停止、非表示にするか、更新の頻度を変える手段を求めています。下の二つの修正は、この2.2.2も満たします。一部を書き換えるのではなく、タイマーでページ全体を再読み込みするページは、まず2.2.1に適合しません。こちらは[時間がたつと自動で再読み込みや転送をするページ](/ja/learn/meta-refresh)で扱います。

## 利用者への影響

この更新は、一覧全体を見て新しいものを拾う人のためのものです。一覧を1件ずつ読み進めるすべての人にとっては負担になり、ゆっくり読む人ほど何度も巻き込まれます。

## 自動チェックとAIの限界

マークアップに誤りはなく、スキャンはページを読み込んで数秒で離れるため、最初の更新よりずっと前に終わります。この診断では、利用者の操作なしに始まるタイマーを見張り、内容を取得してマークアップとしてページに書き込むものを `ew:auto-refresh-manual-check` として確認に回します。そのタイマーが主要な内容を置き換えるのか、その前に更新を止めるスイッチがあるのかは、人が見て確かめるしかないためです。ページを再読み込みするmeta refreshは、遅延が20時間未満ならaxeの `meta-refresh` が報告します。それより長いものは2.2.1を満たすため、この診断では `axe:meta-refresh-no-exceptions` として確認に回します。AIのコーディングツールに「一覧を最新に保って」と頼むと、修正前の例の `setInterval` を書くことがあります。

## 修正前：1分ごとに丸ごと上書きされる一覧

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');

setInterval(async () => {
  const response = await fetch('/latest');
  list.innerHTML = await response.text();
}, 60000);
```

- 誰かが読んでいるかどうかに関係なく、60秒ごとに一覧全体が置き換わります。
- フォーカスのある記事のリンクも一緒に取り除かれ、フォーカスはページに戻ります。
- 新しい記事が来ると、その下の記事はすべて1つ下にずれます。
- 更新を止める手段がありません。

## 修正後：新しい記事を表示するボタン

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <button type="button" id="show-new" hidden>Show 1 new story</button>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');
const showNew = document.getElementById('show-new');
let waiting = '';

setInterval(async () => {
  const newest = list.querySelector('time').dateTime;
  const response = await fetch(`/latest?after=${newest}`);
  const { count, html } = await response.json();
  if (!count) return;
  waiting = html;
  showNew.textContent = count === 1 ? 'Show 1 new story' : `Show ${count} new stories`;
  showNew.hidden = false;
}, 60000);

showNew.addEventListener('click', () => {
  list.insertAdjacentHTML('afterbegin', waiting);
  waiting = '';
  showNew.hidden = true;
  list.querySelector('a').focus();
});
```

1. 60秒ごとの取得は続けますが、ページの先頭の記事より新しいものだけを求め、一覧には書き込まずに取っておきます。
2. その件数を一覧の前のボタンに書き、ボタンを表示します。一覧は変わらないので、誰も読んでいる位置を失いません。
3. ボタンが押されたら、新しい記事を一覧の先頭に加えます。これが、利用者の求めた変化です。
4. 押したボタンはまた隠れるため、フォーカスを最初の新しい記事へ移します。利用者は、自分で表示させた記事から読み始められます。

これで修正前の例は直り、3.2.5を満たします。主要な内容は、誰かがボタンを押したときにだけ変わります。一覧が勝手に変わることはもうないので、2.2.2も満たします。

## 修正後：更新をオフにできるチェックボックス

```html
<section aria-labelledby="latest-title">
  <h2 id="latest-title">Latest</h2>
  <input type="checkbox" id="auto-update" checked>
  <label for="auto-update">Update this list automatically</label>
  <ol id="stories">
    <li>
      <time datetime="10:40">10:40</time>
      <a href="/news/storm-warning">Storm warning for the coast tonight</a>
    </li>
    …
  </ol>
</section>
```

```js
const list = document.getElementById('stories');
const autoUpdate = document.getElementById('auto-update');
autoUpdate.checked = localStorage.getItem('auto-update') !== 'off';
autoUpdate.addEventListener('change', () => {
  localStorage.setItem('auto-update', autoUpdate.checked ? 'on' : 'off');
});

setInterval(async () => {
  if (!autoUpdate.checked) return;
  const response = await fetch('/latest');
  list.innerHTML = await response.text();
}, 60000);
```

1. 「この一覧を自動で更新する」チェックボックスを一覧の前に置き、記事より先にたどり着けるようにします。
2. 更新のたびに最初にチェックボックスを確かめ、オフの間は更新を飛ばします。利用者が望む間、一覧はそのまま残ります。
3. 選択を保存し、ページの読み込み時に読み戻します。更新をオフにした人が次に訪れたときも、更新は止まったままです。

これで修正前の例は直り、変化を止める手段を認めている3.2.5と、2.2.2を満たします。ただし、誰かがオフにするまで更新は続きます。記事から読み始めたスクリーンリーダーの利用者や、リンクから一覧の中へ直接来た人は、チェックボックスを見つける前に読んでいる位置を失います。

## どの方法を選ぶか

どちらも3.2.5を満たします。おすすめはボタンです。誰かが求めるまで何も変わらないので、先に設定を探す必要がありません。チェックボックスでは、まだ見つけていない人全員に更新が届き続けるため、単独では足りません。ニュースが入りしだい読みたい人には、ボタンだと1回の操作が増えます。チェックボックスが向いているのは、試合の速報のように、読者の多くが自動で最新になってほしいと考えるページです。その場合も、チェックボックスを先に置き、選択を覚えておくことが条件です。

## 実装の判断

**フォーカスの移動先。** 押されたボタンは隠れ、隠れた要素はフォーカスを保てないため、スクリプトでフォーカスを最初の新しい記事へ移します。ブラウザー任せにして、フォーカスをページへ落とさないでください。デザイン上ボタンを表示したままにするなら、件数を「新着はありません」に変えて、フォーカスをボタンに残してもかまいません。

**新しい記事があることを伝える。** ボタンは何も読み上げずに現れますが、3.2.5にはそれで十分です。利用者は一覧の先頭に戻ったときに見つけます。スクリーンリーダーの利用者にも知らせたいなら、件数が0から1になったときに `role="status"` の領域へ短いメッセージを書きます。確認のたびに書くと、1分ごとのメッセージが次の割り込みになります。理由は[スクリーンリーダーが読み上げ続けるカウントダウンタイマー](/ja/learn/countdown-timers)で説明しています。

**画面に固定した「新着」ボタン。** このボタンを、画面の上端に固定したピル型で表示するサイトもあります。マークアップでは一覧の前に置いて、読み上げとTabの順で記事より先に来るようにし、フォーカスのある記事を決して覆わないようにします。[フォーカス中のリンクを隠す固定ヘッダーとバナー](/ja/learn/focus-not-obscured)を参照してください。

**一覧にいる間だけ止めるのは、修正になりません。** フォーカスやポインターが一覧の中にある間は更新を止めるスクリプトも、利用者が離れた瞬間に一覧を変えます。また、スクリーンリーダーが一覧を読み進めても、フォーカスが一緒に動くとは限らないため、スクリプトには誰かが読んでいることが分かりません。

**新しい記事だけを先頭に加える。** 一覧を書き換えずに `insertAdjacentHTML('afterbegin', …)` で新しい項目を加えれば、フォーカスのあるリンクはページに残り、修正前よりはよくなります。それでも、拡大して読んでいる人の前で記事がすべて下にずれるため、ボタンかチェックボックスも用意してください。

**自動で再読み込みするページ。** `<meta http-equiv="refresh" content="300">` を持つ新着ページは、経路が違うだけで同じ失敗であり、2.2.1にも適合しません。遅延の長さにかかわらずmeta refreshを消し、新しい記事を求めたときに読み込めるボタンやリンクを用意します。

**更新そのものが目的のライブページ。** ライブブログ、試合のページ、発車案内は最新の状態を保つためにありますが、3.2.5はこれらを例外にしていません。同じボタンかチェックボックスを付けてください。試合を細かく追っている人は、ボタンを押すか、更新をオンのままにします。

## 修正の確認

1. スクリプトで、`fetch` や `XMLHttpRequest` を呼び、`innerHTML`、`insertAdjacentHTML`、`replaceChildren` でマークアップを書き込む `setInterval` と `setTimeout` のコールバックを探す。どれも書き込む先がボタンだけか、最初に一覧の前のチェックボックスを確かめていることを確認する。
2. マウスを使わずに一覧の途中の記事へTabキーで移り、更新の間隔より長く待つ。フォーカスがその記事に残り、記事もその場所から動かないことを確認する。
3. 新しい記事が届いたらボタンを押す。記事が先頭に現れ、フォーカスが最初の記事へ移ることを確認する。チェックボックスの場合は、オフにしてもう一度待ち、ページを再読み込みして、オフのままで一覧も変わらないことを確認する。
4. スクリーンリーダーで一覧を1件ずつ2分間読む。読んでいる位置を失わず、ボタンやチェックボックスが何をするものか分かることを確認する。
5. 400%に拡大し、記事を1件画面に入れたまま、更新の間隔より長く待つ。新着を求めるまで記事が動かないことを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。一覧のコンポーネントや更新のスクリプトを変更した際は、再度確認します。

## 制限

これは出発点となる実装例であり、すべてのアプリケーションでの適合を保証するものではありません。このガイドが扱うのは、タイマーで丸ごと書き換わる一覧です。ページを開いたときや文字を入力したときに開くウィンドウなど、3.2.5が利用者の操作で起こすよう求めるほかの変化も、失敗の形は同じで、直し方も同じです。利用者が求めたときに起こすことです。フォーカスや値の変更でページを変える欄やセレクトは[操作するだけでページが変わるドロップダウンと入力欄](/ja/learn/context-changes)で、新しいタブで開くリンクは[新しいタブで開くことを伝えないリンク](/ja/learn/new-tab-links)で扱います。読んでいた要素が取り除かれたときの動きはスクリーンリーダーによって違うため、利用者が使っているもので確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
