# 確保した座席が時間切れで解放されてしまう購入手続きのタイマー

Source: https://easeweb.dev/learn/checkout-timers
Topics: 制限時間と動き > 操作に制限時間がまったくない (WCAG 2.2.3)
The fix: 購入手続きからタイマーをなくします。座席は支払いを受けるのと同じリクエストで確保し、その時点で空いているかを確かめます。なくなっていたら、支払いは受けず、入力した内容を残して、近くの空いている座席を提案します。延長できるタイマーでも、時間内に終えることを求める点は変わらないため、2.2.3は満たしません。
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/no-timing.html https://www.w3.org/WAI/WCAG22/Techniques/general/G5 https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html https://www.w3.org/WAI/WCAG22/Understanding/conformance https://www.eventbrite.com/help/en-us/articles/435853/how-to-increase-or-decrease-the-amount-of-time-to-complete-an-order/

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

## 場面

小さな劇場が、チケットをオンラインで販売しています。座席表で2席を選ぶと、支払いが済むまでほかの人が買えないように座席が確保されます。購入手続きの画面の上部には「座席を確保しています　残り10:00」というバーが出て、残り時間が1秒ずつ減っていきます。その下で、利用者は氏名、メールアドレス、配慮のご希望、カード情報を入力します。バーがゼロになると、ページには「時間切れになりました。座席の確保を解除しました。」と表示されます。座席は再び販売に戻り、もう一度選び直すと、フォームは空になっています。

大手のサービスも、既定ではこう動きます。Eventbriteでは、主催者が変えない限り、参加者が注文を終えるまでの時間は20分で、その間、選ばれたチケットはほかの人には買えません。Ticketmasterのヘルプでは、購入手続きのすべてのページの上にタイマーがあり、時間が切れるとチケットはほかの人が買えるようになると説明しています。ネットショップ向けのプラグインにも、普通のカートに同じカウントダウンを付けて商品は確保済みだと伝え、ゼロになるとカートを空にできるものがあります。

## 確認できる問題

レベルAAAの達成基準2.2.3「タイミング非依存」は、イベントや活動にとってタイミングが本質的な部分でないことを求めています。例外は、操作を伴わない同期したメディアと、リアルタイムのイベントだけです。レベルAの2.2.1「タイミング調整可能」と違い、欠かせない制限、20時間より長い制限という例外はなく、延長できるようにすることで満たす道もありません。W3Cが挙げる十分な達成方法はG5の一つだけで、時間制限なしに活動を終えられるようにすることです。

購入手続きはメディアではなく活動です。混み合う販売が、WCAGの言うリアルタイムのイベント（見ているのと同時に起こり、すべてがコンテンツで作られているわけではないもの）に当たるかどうかは、W3Cも書いていません。2.2.1がリアルタイムのイベントの例に挙げているのはオークションで、W3C自身のチケットの確保の例は、リアルタイムのイベントではなく、欠かせない制限という例外のほうに置かれています。そのため、このガイドでも購入手続きをリアルタイムのイベントとしては扱いません。座席表より後の手順は、すべて確保のタイマーの10分以内に終えなければならず、購入手続きは時間に左右されます。W3C自身の2.2.1の例は、すぐに売り切れるチケットの2分間の確保を欠かせない制限として扱っていて、レベルAではそれが認められます。2.2.3では認められません。

タイマーが生まれる原因は、座席を確保するタイミングにあります。座席を選んだ瞬間に確保するなら、その確保はいずれ自動で切れるようにするしかありません。そうしないと、途中でやめた購入手続きのたびに、座席が販売から外れたままになってしまいます。支払いの時点で確保すれば、時間を計るものはなくなります。

## 利用者への影響

10分あれば、入力が速く、カードを手元に用意している人なら間に合います。入力や読み取り、判断に時間がかかる人は、この競争に負けます。しかも負けるのは、フォームのほとんどを入力し終えた最後の段階です。座席を選び直すことになり、そのときには希望の座席が売れているかもしれません。スマートフォンでは、入力中はスクリーンキーボードが画面の半分を覆い、カウントダウンは画面の外へスクロールしてしまいます。

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

座席の解放は、スクリプトのタイマーか、サーバーにある予約の有効期限で起こり、どちらもスキャンが読むページには現れません。EaseWebの診断は、timerロールや、「確保」「残り時間」のような言葉を添えたカウントダウンがあるページを、2.2.1に照らして人が確認する対象に挙げます。しかし2.2.3を調べるチェックはありません。延長できるタイマーは2.2.1を満たすため、どこかの手順がまだ時間に左右されていないかは、購入手続きのコードを読まないと分かりません。AIアシスタントに、同じ座席を2人が買わないようにする方法を尋ねれば、チケットのサービスが説明しているのと同じ期限付きの確保を提案することがあります。そのタイマーで誰が取り残されるかまでは、質問からは見えてきません。自分のペースで購入手続きを入力してみるか、予約のコードを読んで、初めて分かります。

## 修正前：時間付きで確保する座席

```html
<p id="hold" data-expires="2026-11-14T18:40:00Z">座席を確保しています　残り<span id="hold-time">10:00</span></p>
<form method="post" action="/orders">
  <input type="hidden" name="hold" value="h_81f2">
  <label for="name">氏名</label>
  <input id="name" name="name" autocomplete="name">
  <label for="email">メールアドレス</label>
  <input id="email" name="email" type="email" autocomplete="email">
  <label for="access">配慮のご希望（任意）</label>
  <textarea id="access" name="access"></textarea>
  …
  <button>$48.00を支払う</button>
</form>
```

```js server.js
// 座席表で選んだ座席を10分間確保する
app.post('/holds', async (req, res) => {
  const hold = await seats.hold(req.body.seats, { minutes: 10 });
  res.json({ id: hold.id, expires: hold.expires });
});

app.post('/orders', async (req, res) => {
  const hold = await seats.findHold(req.body.hold);
  if (!hold || hold.expires < Date.now()) return res.redirect('/seats?expired=1');
  const order = await seats.confirm(hold, (total) => payments.charge(req.body.card, total));
  res.redirect(`/orders/${order.id}`);
});
```

```js
// 1秒ごとに減らし、ゼロになったら座席表へ戻す
const clock = document.getElementById('hold-time');
const end = Date.parse(document.getElementById('hold').dataset.expires);
const timer = setInterval(() => {
  const left = Math.max(0, end - Date.now());
  clock.textContent = `${Math.floor(left / 60000)}:${String(Math.floor(left / 1000) % 60).padStart(2, '0')}`;
  if (left === 0) {
    clearInterval(timer);
    location.href = '/seats?expired=1';
  }
}, 1000);
```

- 座席は、フォームの入力にどれだけかかっても、選んだ時点から10分間しか確保されません。
- ゼロになると、ページは座席表へ戻り、座席は再び販売されます。
- フォームに入力した内容は、ページと一緒に失われます。

## 修正後：支払いの時点で確保する座席

```html
<p>座席は、お支払いの時点で確定します。</p>
<form method="post" action="/orders">
  <input type="hidden" name="seats" value="F7,F8">
  <label for="name">氏名</label>
  <input id="name" name="name" autocomplete="name">
  <label for="email">メールアドレス</label>
  <input id="email" name="email" type="email" autocomplete="email">
  <label for="access">配慮のご希望（任意）</label>
  <textarea id="access" name="access"></textarea>
  …
  <p id="seats-gone" tabindex="-1" hidden></p>
  <button>$48.00を支払う</button>
</form>
```

```js server.js
// フォームの入力中は、何も確保しない
app.post('/orders', async (req, res) => {
  // 空きの確認と確保は、支払いと同じこの時点で初めて行う
  const order = await seats.bookIfFree(req.body.seats.split(','), (total) => payments.charge(req.body.card, total));
  if (!order) {
    // 入力中に売れていた。支払いは受けず、近くの空いている座席を提案する
    return res.status(409).json({ nearest: await seats.nearestFree(req.body.seats.split(',')) });
  }
  res.json({ receipt: `/orders/${order.id}` });
});
```

```js
// ページを離れずにフォームを送り、入力した内容が失われないようにする
const form = document.querySelector('form');
const gone = document.getElementById('seats-gone');
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const response = await fetch(form.action, { method: 'POST', body: new URLSearchParams(new FormData(form)) });
  const result = await response.json();
  if (response.ok) {
    location.href = result.receipt;
    return;
  }
  // 入力の間に売れていたら、近くの空席を提案する。空席がなければ、そう伝える
  if (result.nearest) {
    form.elements.seats.value = result.nearest.seats;
    gone.textContent = `入力している間に、選んだ座席がほかの方に購入されました。${result.nearest.label}が空いています。このまま「支払う」を押すと、こちらを予約します。ほかの入力内容は変わっていません。`;
  } else {
    gone.textContent = '入力している間に、選んだ座席がほかの方に購入され、空いている座席はなくなりました。お支払いは発生していません。';
  }
  gone.hidden = false;
  gone.focus();
});
```

1. 確保の処理とその有効期限、カウントダウンを削除します。座席を選んでもフォームの `seats` 欄が埋まるだけになり、ページにもサーバーにも、時間切れになるものはなくなります。
2. 空きの確認と確保を、支払いを受けるのと同じリクエストで行います。意味を持つのは、「支払う」を押した瞬間だけになります。
3. フォームは `fetch()` で送り、ページにとどまります。座席がなくなっていても、支払いは受けず、どの欄にも入力した内容が残ります。
4. 起きたことと、近くの空いている座席を「支払う」ボタンのそばに書き、フォーカスをそこへ移します。メッセージは読み上げられて目にも入り、利用者はもう一度「支払う」を押すだけで済みます。

これで修正前の例は直り、2.2.3を満たします。購入手続きのどの手順も、時間に左右されません。入力している間に座席がほかの人に売れることはありますが、そのときはメッセージで知らせるだけで、入力した内容は失われません。

## 実装の判断

**数分で売り切れる販売。** 確保がないと、入力している間に座席が売れることがあり、入力がいちばん遅い人ほど、なくなっている可能性が高くなります。2.2.3が取り除くのは時間制限であって、競争そのものではありません。それほど混み合う販売について、W3C自身の2.2.1の例は短い確保を残し、欠かせない制限として扱っています。これはレベルAは満たしますが、2.2.3は満たしません。AAAを満たしたい販売では、何日かかけて申し込みを受け付け、後から座席を割り当てる抽選にすれば、誰の席も入力の速さでは決まりません。W3Cの達成方法G5も、オークションについて同じ答えを出しています。時間を競って入札させるのではなく、一人一回の入札にする方法です。なお、W3Cは、サイト全体にAAAを一律に求めることを勧めていません。コンテンツによっては、すべてのAAAの達成基準を満たせないからです。チケットのサイトがAAAを目指さないと決めるとしたら、この部分です。

**ネットショップのカート。** 普通のネットショップのカートに付くカウントダウンも、実際に在庫を確保しているかどうかにかかわらず、時間制限です。それを加えるプラグインは、急がせてカートの放棄を減らす方法として紹介しています。これは削除し、修正が座席について行うように、在庫は支払いのリクエストで確かめます。

**カード番号の欄。** 決済サービスの枠の中に入力したカード番号は、ページを開いている限りその枠に残るため、`fetch()` でフォームを送れば、カード番号も残ります。送信して新しいページを表示するフォームでは、氏名や配慮のご希望は戻せますが、カード番号は戻せません。カード番号は、サイトがページに書き戻してはいけないものだからです。

**メッセージ。** 何がなくなったか、何を提案するか、何をすればよいかを短い文で書き、利用者が押したボタンのそばに出します。空いている座席がなければそう伝え、その場合も支払いは受けません。フォーカスを移すと読み上げられ、画面拡大ソフトの視野にも入ります。フォームの結果の伝え方全般は、[読み上げられないフォームの送信結果](/ja/learn/form-results)を参照してください。

**修正にならないもの。** 確保を延長する「時間を延長する」ボタンは2.2.1を満たし、こちらにはガイドがあります。[警告も延長もないセッションタイムアウト](/ja/learn/time-limits)を参照してください。しかし、利用者が制限の中で終える必要は残るため、2.2.3は満たしません。W3Cの2.2.1の例のように、氏名やカードの入力を座席表より前に移しても同じです。時間を計る部分は短くなりますが、座席の選択と「支払う」ボタンはその中に残ります。10分を30分にするなど、タイマーを長くしても、タイマーであることに変わりはありません。

## 修正の確認

1. 購入手続きのスクリプトと、サーバーの予約のコードを読む。タイマーや定期実行の処理、有効期限によって、座席が解放されたり、フォームが消えたり、ページが移動したりすることがなく、支払いのリクエストより前に座席を確保する処理もない。
2. 座席を選んでフォームを半分まで入力し、1時間そのままにしておく。何も変わっておらず、カウントダウンや残り時間も表示されていない。
3. フォームを入力し終えて支払う。座席はこの手順で予約され、領収書にその座席が書かれている。
4. テスト用のビルドで、別のブラウザーから同じ座席を予約してから、最初のブラウザーで「支払う」を押す。支払いは発生せず、どの欄にも入力した内容が残り、代わりの座席を伝えるメッセージにフォーカスがある。
5. スクリーンリーダーで購入手続きをゆっくり入力し、手順4を繰り返す。入力中に遮られることがなく、メッセージは表示されたときに読み上げられる。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。予約のコードや購入手続きを変更した際は、再度確認します。

## 制限

ここで扱うのは、座席や在庫をタイマー付きで確保する購入手続きで、ネットショップや予約サイトでよく見る2.2.3の失敗です。もう一つの形は、制限時間のあるクイズやテストです。W3Cはその場合について、制限時間をなくし、時間を計らずに解いたときの成績をもとに採点の基準を決める方法を示しています。利用者をログアウトさせるセッションと、それを延長できる警告は2.2.1の範囲です。[警告も延長もないセッションタイムアウト](/ja/learn/time-limits)を参照してください。参加者が同時に入札するオークションのような生のイベントと、再生するだけの映像や音声は、2.2.3の対象外です。チケットを確保なしで販売できるかどうかは、どれだけ早く売れるかを知る運営側が判断することです。

---

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