# 長押しや時間内の2回押しが必要なボタン

Source: https://easeweb.dev/learn/timed-key-presses
Topics: キーボードとフォーカス > キーを長押ししたり、決まった間隔で押したりしなくても操作できる (WCAG 2.1.1)
The fix: 普通に1回押せば済むようにします。押したら、答えるまで待つ確認を開くか、すぐに削除して、ページを開いている間はリストのあった場所に「元に戻す」を残します。長押しや素早い2回押しは追加の方法として残してもかまいませんが、唯一の方法にはしません。
Test with: keyboard, speech input, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Techniques/general/G202 https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

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

## 場面

ショップのアカウントページに、顧客が保存したリストが並んでいます。たとえば14アイテムの「キッチンのアイデア」で、それぞれに削除する方法があります。デザイナーは、ダイアログを挟まずに、削除の前にひと呼吸置いてほしいと考えました。そこでボタンには「長押しで削除」と書かれています。マウス、指、Enter、Spaceのどれかで押し続けると、ボタンの中をゲージが進み、1.5秒たつ前に離すとゲージが戻ります。同じ考えのもう一つの形は、普通の「リストを削除」ボタンです。1回押すと「もう一度押して削除」に変わり、3秒たつと元に戻ります。

どちらもキーボードで動きます。ただし、手とキーボードがタイミングを合わせられる場合だけです。スイッチの押下、「Enterを押す」という音声のコマンド、スクリーンキーボードのタップは、どれも押してすぐ離す1回の押下として届きます。ゲージは進んだ瞬間に戻り、リストは残ります。時間内にもう一度押すには、3秒以内にボタンへもう一度届く必要がありますが、スイッチのスキャンや音声のコマンドでは、間に合わないことがあります。

## 確認できる問題

達成基準2.1.1は、すべての機能を「個々のキーストロークに特定のタイミングを要することなく」キーボードで操作できることを求めています。解説書は、このタイミングの例を2つ挙げています。短い時間のうちにキーを繰り返し押したり、続けて押したりする必要があるもの、そしてキーストロークとして受け付けられる前に、キーを長く押し続ける必要があるものです。長押しのボタンは2つ目に、「もう一度押す」ボタンは1つ目に当たります。どちらもキーボードの操作を受け付けていますが、それでも基準を満たしません。削除する方法が、時間内に押すことしかないためです。

時間を緩めても結果は変わりません。短い長押しも、長い待ち時間も、間に合わない人がいるタイミングであることに変わりはありません。基準を満たすには、キーストロークから時間の条件をなくします。

## 利用者への影響

この問題はテストではほとんど表に出ません。ページを作ってテストする人は、タイマーが想定するとおりにキーを押すためです。表に出るのは、「何も削除できない」という顧客と、消したはずのリストが並んだアカウントです。

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

ボタンは名前が見えるHTML標準のボタンなので、自動チェックでは問題が見つかりません。Enterを押すスクリプトにとっては、何も起きないボタンであり、別の何かを待っているボタンとの区別がつきません。そもそも、本番のサイトで削除のボタンを押すテストは、診断では実行できません。そこでEaseWebの診断は、ページの言葉を読みます。操作要素の近くにある「Hold to」「press and hold」「press again to」や「長押し」は、確認の対象として報告します。言葉のない長押しや、1回目を押した後にだけ現れる「もう一度押して」は、どのチェックも見つけられません。AIのコーディングツールは、長押しで確定するボタンを頼まれれば作り、キーボードで動くようにkeydownとkeyupのハンドラーも加えますが、そこで終わることがあります。2.1.1のタイミングの部分は、見落としやすいほうです。

## 修正前：押し続けている間だけ削除できるボタン

```html
<h2>保存したリスト</h2>
<ul>
  <li>
    <h3>キッチンのアイデア</h3>
    <p>14アイテム</p>
    <button type="button" id="delete-list" class="hold">
      <span class="hold-fill" aria-hidden="true"></span>
      長押しで削除
    </button>
  </li>
  …
</ul>
```

```js
const button = document.getElementById('delete-list');
let timer;
function startHold(event) {
  if (event.type === 'keydown' && (event.repeat || (event.key !== 'Enter' && event.key !== ' '))) return;
  event.preventDefault();
  button.classList.add('holding'); // 1.5秒でゲージが端まで進みます
  timer = setTimeout(deleteList, 1500);
}
function stopHold() {
  clearTimeout(timer);
  button.classList.remove('holding');
}
button.addEventListener('pointerdown', startHold);
button.addEventListener('keydown', startHold);
button.addEventListener('pointerup', stopHold);
button.addEventListener('pointerleave', stopHold);
button.addEventListener('keyup', stopHold);
```

削除するには、キーかポインターを1.5秒押し続けるしかありません。1回すばやく押すと、タイマーが始まってすぐに止まり、ブラウズモードのスクリーンリーダーが送るクリックも、同じようにすぐ終わるか、タイマーを始めもしません。

## 修正前：3秒以内にもう一度押す必要があるボタン

```html
<h2>保存したリスト</h2>
<ul>
  <li>
    <h3>キッチンのアイデア</h3>
    <p>14アイテム</p>
    <button type="button" id="delete-list">リストを削除</button>
  </li>
  …
</ul>
```

```js
const button = document.getElementById('delete-list');
let armed = false;
let timer;
button.addEventListener('click', () => {
  if (armed) {
    clearTimeout(timer);
    deleteList();
    return;
  }
  armed = true;
  button.textContent = 'もう一度押して削除';
  timer = setTimeout(() => {
    armed = false;
    button.textContent = 'リストを削除';
  }, 3000);
});
```

どちらの押下も普通の1回ですが、2回目は1回目から3秒以内でなければならず、過ぎるとボタンは1回目を忘れます。この待ち時間が、2.1.1が認めていないタイミングです。

## 修正後：1回押すと、待ってくれる確認が開く

```html
<h2 id="lists-heading" tabindex="-1">保存したリスト</h2>
<ul>
  <li id="list-kitchen">
    <h3>キッチンのアイデア</h3>
    <p>14アイテム</p>
    <button type="button" id="delete-list">リストを削除</button>
  </li>
  …
</ul>

<dialog id="confirm-delete" role="alertdialog"
  aria-labelledby="confirm-title" aria-describedby="confirm-message">
  <form method="dialog">
    <h2 id="confirm-title">「キッチンのアイデア」を削除しますか？</h2>
    <p id="confirm-message">リストと、保存した14アイテムが削除されます。</p>
    <button value="keep" autofocus>リストを残す</button>
    <button value="delete">リストを削除</button>
  </form>
</dialog>
```

```js
const dialog = document.getElementById('confirm-delete');
document.getElementById('delete-list').addEventListener('click', () => {
  dialog.returnValue = '';
  dialog.showModal();
});
dialog.addEventListener('close', async () => {
  // 「リストを残す」とEscape：ブラウザーがフォーカスを「リストを削除」に戻します。
  if (dialog.returnValue !== 'delete') return;
  await deleteList();
  document.getElementById('list-kitchen').remove();
  document.getElementById('lists-heading').focus();
});
```

長押しで作ろうとしたひと呼吸が、質問になり、その人に必要なだけ待ちます。クリック、タップ、Enter、Spaceのどれか1回で開き、もう1回押せば答えられます。フォーカスは「リストを残す」から始まるため、繰り返された押下ではリストが残ります。リストを削除した後は、フォーカスがページのbodyに落ちず、リストの上の見出しに移ります。質問そのものを読み上げさせる方法は、[質問が読み上げられない確認ダイアログ](/ja/learn/confirmation-dialogs)で扱っています。

## 修正後：すぐに削除し、残り続ける「元に戻す」を置く

```html
<h2>保存したリスト</h2>
<ul>
  <li id="list-kitchen">
    <div class="list-body">
      <h3>キッチンのアイデア</h3>
      <p>14アイテム</p>
      <button type="button" class="delete">リストを削除</button>
    </div>
    <p class="deleted" tabindex="-1" hidden>
      「キッチンのアイデア」を削除しました。
      <button type="button" class="undo">元に戻す</button>
    </p>
  </li>
  …
</ul>
```

```js
const card = document.getElementById('list-kitchen');
const body = card.querySelector('.list-body');
const deleted = card.querySelector('.deleted');
card.querySelector('.delete').addEventListener('click', async () => {
  await deleteList(); // サーバーはリストをごみ箱に残すため、元に戻せます
  body.hidden = true;
  deleted.hidden = false;
  deleted.focus();
});
card.querySelector('.undo').addEventListener('click', async () => {
  await restoreList();
  deleted.hidden = true;
  body.hidden = false;
  card.querySelector('.delete').focus();
});
```

1回押せば削除され、長押しで作ろうとした安全策は、押す前ではなく押した後に来ます。メッセージがリストのあった場所に現れてフォーカスを受けるため、スクリーンリーダーは「「キッチンのアイデア」を削除しました。元に戻す」と読み上げ、次のTabで「元に戻す」に移ります。メッセージは、ページを離れるまで残ります。消えていくトーストに「元に戻す」を置くと、時間の制限が戻ってしまいます。詳しくは[読む前や元に戻す前に消えるトースト](/ja/learn/toast-timing)を参照してください。この方法では、`restoreList()` が戻せるように、サーバーが削除したリストをごみ箱や論理削除でしばらく残しておく必要があります。

## どの方法を選ぶか

どちらも2.1.1を満たし、どちらも誰にもタイミングを合わせることを求めません。確認は、削除のたびに1回多く押す必要がありますが、サーバー側の変更は要らず、間違いを起きる前に止めます。削除したものを元に戻せない場合は、こちらを選んでください。「元に戻す」は削除を1回の押下のまま保つため、よく整理する人や、1回押すだけでも負担になる人に向いていますが、サーバーがリストを戻せる場合にしか使えません。削除が多く、元に戻せる場合に選んでください。どちらの場合も、マウスや指での長押しは、好む人のための近道として残せます。ただし、クリック、タップ、Enter、Spaceだけで、操作全体が済むことが条件です。その長押しは、指やボタンを離したときに完了し、外へずらすと取り消せるようにします。詳しくは[押した瞬間に実行され、キャンセルできないボタン](/ja/learn/pointer-cancellation)を参照してください。

## 実装の判断

操作要素にはHTML標準の `button` を使い、自分でキーのハンドラーを書かなくてもEnterとSpaceで動くようにします。また、ボタン自体のフォーカスリングを付けてください。長押しのボタンの中を進むゲージは、フォーカスの表示ではありません。`div` で作った操作要素は、別の形で2.1.1を満たしません。詳しくは[Tabキーで到達できないdivのボタン](/ja/learn/native-buttons)を、リングがない場合は[フォーカス表示が見えない独自デザインのボタン](/ja/learn/visible-focus)を参照してください。

修正前のコードを、数字を変えて直そうとしないでください。0.5秒の長押しも押し続ける必要があり、10秒の待ち時間もいずれ終わります。keydown、pointerdown、1回目のクリックで始まるタイマーをすべて数え、どれも人と操作の間に立っていないことを確かめます。

同じ問題は、削除のボタン以外の形でも現れます。どれにも、1回押すだけの方法があります。

- 長押しで音声メッセージを録音する：1回押すと録音が始まり、もう1回押すと止まるようにし、録音した時間を示す「停止」ボタンを置きます。
- 通話中にSpaceを押している間だけ話せる：押すたびに切り替わる「ミュート」ボタンを置き、Spaceの長押しは好む人のための追加の方法として残します。フォーカスのある操作部品だけでなく、ページのどこでも反応するSpaceは、[オフにできない1文字のショートカット](/ja/learn/character-key-shortcuts)と同じように音声入力や文字入力の邪魔になるため、オフにもできるようにします。
- 目のアイコンやShiftを押している間だけパスワードを表示する：もう一度押すまで表示が続く「パスワードを表示」ボタンにし、`aria-pressed` でどちらの状態かを伝えます。
- Escapeを2回押すとエディターが閉じる：1回のEscapeで閉じるようにし、保存していない内容が失われる場合は、先に待ってくれる確認を出します。

キーの組み合わせは、タイミングを求める押下ではありません。Ctrl + Kは1つの組み合わせとして押し、固定キー機能を使えば1つずつ順に押せます。決まった時間押し続けるものはありません。セッションの時間切れなど、作業全体にかかる時間の制限は、この基準ではなく、2.2.1「タイミング調整可能」の対象です。

## 修正の確認

1. マウスを使わずにTabで削除のボタンへ移動し、Enterを1回すばやく押す。Spaceでも同じように試す。確認が開くか、リストが削除されてメッセージにフォーカスが移ることを確認する。
2. 確認を開いたまま、または削除のメッセージを表示したまま30秒以上待つ。勝手に閉じたり、リセットされたり、元に戻ったりせず、「リストを残す」「リストを削除」「元に戻す」がすべて使えることを確認する。
3. スクリーンリーダーのブラウズモードで、削除のボタンでEnterを押す。開いたときに確認の質問とメッセージが読み上げられるか、押した後に「「キッチンのアイデア」を削除しました」が読み上げられることを確認する。
4. スイッチ、スクリーンキーボード、音声入力（「Enterを押す」「リストを削除をクリック」など）で、その方法のペースのまま、リストを削除し、元に戻すか残すことを確認する。
5. 削除、残す、元に戻すの後に、フォーカスの位置を確認する。「リストを削除」、削除のメッセージ、リストの見出しのどれかにあり、ページのbodyにはないこと。

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

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。サーバーとのやりとりと、複数のリストの中の元の位置にリストを戻す処理は省略しています。マウスや指での長押しと、長押しで開くメニューについては、ポインター側から[右クリックや長押し、スワイプでしか出ない操作メニュー](/ja/learn/context-menus)で扱っています。キーボード、スイッチ、音声入力、実際に使用する支援技術で、操作全体を確認してください。

---

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