<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Keyboard on Tarragon</title><link>https://tarrragon.github.io/blog/tags/keyboard/</link><description>Recent content in Keyboard on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/keyboard/index.xml" rel="self" type="application/rss+xml"/><item><title>輸入機制決策表</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/</guid><description>&lt;p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。&lt;/p>
&lt;h2 id="四個決策維度">四個決策維度&lt;/h2>
&lt;h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤&lt;/h3>
&lt;p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。&lt;/p>
&lt;p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 &lt;code>@&lt;/code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 &lt;code>visiblePassword&lt;/code> 型別（避免自動校正和建議）。&lt;/p>
&lt;p>app_tunnel 的 terminal 輸入框用 &lt;code>TextInputType.visiblePassword&lt;/code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 &lt;code>ls -la&lt;/code> 或 &lt;code>/usr/bin/&lt;/code> 成其他東西（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>）。&lt;/p>
&lt;h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入&lt;/h3>
&lt;p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。&lt;/p>
&lt;p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（&lt;code>ls -la\n&lt;/code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（&lt;code>l&lt;/code>、&lt;code>s&lt;/code>、&lt;code> &lt;/code>、&lt;code>-&lt;/code>、&lt;code>l&lt;/code>、&lt;code>a&lt;/code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。&lt;/p>
&lt;p>app_tunnel 選擇整行送出（&lt;code>onSubmitted&lt;/code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。&lt;/p>
&lt;h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制&lt;/h3>
&lt;p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。&lt;/p>
&lt;p>三個控制項各自有獨立的影響：&lt;/p>
&lt;ul>
&lt;li>&lt;code>autocorrect&lt;/code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。&lt;/li>
&lt;li>&lt;code>enableSuggestions&lt;/code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。&lt;/li>
&lt;li>&lt;code>enableIMEPersonalizedLearning&lt;/code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist&lt;/a>。&lt;/li>
&lt;/ul>
&lt;h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理&lt;/h3>
&lt;p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。&lt;/p>
&lt;p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。&lt;/p>
&lt;h2 id="決策表作為設計產物">決策表作為設計產物&lt;/h2>
&lt;p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>選項&lt;/th>
 &lt;th>理由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Keyboard&lt;/td>
 &lt;td>visiblePassword&lt;/td>
 &lt;td>CLI 指令不適用自動校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Submit&lt;/td>
 &lt;td>整行送出&lt;/td>
 &lt;td>protocol 簡單，犧牲 Tab 補全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>IME&lt;/td>
 &lt;td>全關&lt;/td>
 &lt;td>安全考量 + 非自然語言輸入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Special keys&lt;/td>
 &lt;td>底部工具列&lt;/td>
 &lt;td>手機無實體 Esc/Tab/Ctrl&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 &lt;code>enableIMEPersonalizedLearning: false&lt;/code> 就是安全漏洞，漏掉 &lt;code>autocorrect: false&lt;/code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。&lt;/p>
&lt;p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計&lt;/a>，安全敏感欄位的 IME 控制逐項列在 &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist&lt;/a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>中從 test 的角度分析。&lt;/p></description><content:encoded><![CDATA[<p>輸入機制是設計產物，在功能規格階段決定，和 API schema、畫面狀態矩陣同級。手機鍵盤的行為由多個參數控制，每個參數都是一個設計決策，影響使用者體驗、UI layout 和通訊協議。</p>
<h2 id="四個決策維度">四個決策維度</h2>
<h3 id="keyboard-type顯示哪種鍵盤">Keyboard type：顯示哪種鍵盤</h3>
<p>Keyboard type 決定使用者按下輸入框時出現什麼鍵盤。數字鍵盤、email 鍵盤、URL 鍵盤、一般文字鍵盤 — 每種鍵盤的按鍵配置和自動行為不同。</p>
<p>選擇判斷依據是「使用者要輸入什麼內容」。email 地址用 email 鍵盤（有 <code>@</code> 鍵），電話號碼用數字鍵盤，密碼或 CLI 指令用 <code>visiblePassword</code> 型別（避免自動校正和建議）。</p>
<p>app_tunnel 的 terminal 輸入框用 <code>TextInputType.visiblePassword</code> — 因為 CLI 指令包含路徑分隔符、flag 縮寫等非自然語言內容，一般文字鍵盤會嘗試自動校正 <code>ls -la</code> 或 <code>/usr/bin/</code> 成其他東西（<a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a>）。</p>
<h3 id="submit-model怎麼送出輸入">Submit model：怎麼送出輸入</h3>
<p>Submit model 決定使用者輸入的內容何時傳送給系統。兩個基本選項：整行送出（使用者按 Enter/Send 後一次傳送整行）和逐字元送出（每個按鍵即時傳送）。</p>
<p>這個決策直接影響通訊協議設計（本章合成，UF-8 Derive）。整行送出代表每次傳送一個完整指令字串（<code>ls -la\n</code>），server 端按行處理。逐字元送出代表每個按鍵產生一個 WebSocket frame（<code>l</code>、<code>s</code>、<code> </code>、<code>-</code>、<code>l</code>、<code>a</code>），server 端需要處理單字元輸入，包括 Tab 補全和 Ctrl+C 這類立即回應的操作。</p>
<p>app_tunnel 選擇整行送出（<code>onSubmitted</code>），代表 Tab 補全在 client 端無法觸發（因為 Tab 不會單獨送出），但實作成本較低且協議設計較簡單。逐字元送出支援 Tab 補全和命令編輯，但 protocol 複雜度顯著提高。</p>
<h3 id="ime-policy輸入法的行為控制">IME policy：輸入法的行為控制</h3>
<p>IME（Input Method Editor）policy 控制手機輸入法的自動行為：自動校正、建議列、個人化學習。每個行為在某些輸入場景是幫助，在另一些場景是干擾或安全風險。</p>
<p>三個控制項各自有獨立的影響：</p>
<ul>
<li><code>autocorrect</code>：自動校正把輸入替換成字典中的詞。CLI 指令和路徑不是自然語言，自動校正會破壞輸入內容。</li>
<li><code>enableSuggestions</code>：建議列在鍵盤上方顯示候選詞。在 terminal 場景中建議列遮擋畫面底部的終端機輸出。</li>
<li><code>enableIMEPersonalizedLearning</code>：IME 從使用者輸入中學習新詞，跨 app 適用。CLI 輸入可能包含密碼和路徑 — 這是安全問題，見 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">安全敏感輸入框的 IME 控制 checklist</a>。</li>
</ul>
<h3 id="special-keys特殊按鍵的處理">Special keys：特殊按鍵的處理</h3>
<p>手機鍵盤沒有桌面鍵盤的 Esc、Tab、Ctrl、方向鍵。如果應用需要這些按鍵，必須自建 UI 元件提供。</p>
<p>app_tunnel 用底部工具列提供 Esc/Tab/Ctrl/方向鍵。這個工具列的設計（按鈕大小、排列、長按行為）是 UX 決策，不是實作細節。</p>
<h2 id="決策表作為設計產物">決策表作為設計產物</h2>
<p>四個維度的決策應該在功能規格中以表格形式記錄，讓 code review 時可以逐項對照實作是否符合規格。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>選項</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Keyboard</td>
          <td>visiblePassword</td>
          <td>CLI 指令不適用自動校正</td>
      </tr>
      <tr>
          <td>Submit</td>
          <td>整行送出</td>
          <td>protocol 簡單，犧牲 Tab 補全</td>
      </tr>
      <tr>
          <td>IME</td>
          <td>全關</td>
          <td>安全考量 + 非自然語言輸入</td>
      </tr>
      <tr>
          <td>Special keys</td>
          <td>底部工具列</td>
          <td>手機無實體 Esc/Tab/Ctrl</td>
      </tr>
  </tbody>
</table>
<p>app_tunnel 的六個 TextField 參數全是 W2 hotfix 事後補上的，沒有一個是事前規劃。每個參數都有 gotcha — 漏掉 <code>enableIMEPersonalizedLearning: false</code> 就是安全漏洞，漏掉 <code>autocorrect: false</code> 就是 UX 問題。事先決策並記錄在規格中，code review 時逐項勾選，比事後逐一發現問題的成本低。</p>
<p>四個維度在不同場景下的具體決策各有不同。CLI 場景的特殊需求見 <a href="/blog/ux-design/03-input-mechanism/terminal-input-design/" data-link-title="Terminal app 輸入設計" data-link-desc="CLI 場景在手機上的特殊需求 — 非自然語言輸入、特殊按鍵需求、整行 vs 逐字元送出對 protocol 的影響">Terminal app 輸入設計</a>，安全敏感欄位的 IME 控制逐項列在 <a href="/blog/ux-design/03-input-mechanism/ime-security-checklist/" data-link-title="安全敏感輸入框的 IME 控制 checklist" data-link-desc="處理密碼、API key、伺服器路徑等 secret 的輸入框需要關閉 IME 的個人化學習和自動校正 — 安全要求而非 UX 偏好">IME 安全 checklist</a>。Submit model 的選擇（整行 vs 逐字元）直接影響通訊協議的設計 — 這個交叉影響在 <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>中從 test 的角度分析。</p>
]]></content:encoded></item><item><title>U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField</title><link>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/</guid><description>&lt;p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。&lt;/p>
&lt;p>W2-001 修復時加入的 &lt;code>TextField&lt;/code> 及其參數：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">TextField&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nl">keyboardType:&lt;/span> &lt;span class="n">TextInputType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">visiblePassword&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 避免自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableSuggestions:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉建議列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">autocorrect:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉自動校正
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">enableIMEPersonalizedLearning:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 關閉 IME 個人化學習
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">onSubmitted:&lt;/span> &lt;span class="n">_submitInput&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// Enter 送出整行
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nl">textInputAction:&lt;/span> &lt;span class="n">TextInputAction&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">send&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 鍵盤顯示「傳送」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計決策&lt;/th>
 &lt;th>事前規劃&lt;/th>
 &lt;th>事後 hotfix 的風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>visiblePassword&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用預設 &lt;code>text&lt;/code>，iOS 會自動校正 &lt;code>ls -la&lt;/code> 成其他東西&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableSuggestions: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>建議列遮擋終端機畫面下方&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>autocorrect: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>路徑 &lt;code>/usr/bin/&lt;/code> 可能被校正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>enableIMEPersonalizedLearning: false&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>CLI 輸入含密碼和路徑，IME 學習是安全風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>onSubmitted&lt;/code>（整行送出）&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>TextInputAction.send&lt;/code>&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>如果用 &lt;code>newline&lt;/code>，使用者按 Enter 會換行不送出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>輸入設計影響 UI layout 和 protocol&lt;/strong>。&lt;code>onSubmitted&lt;/code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 &lt;code>command\n&lt;/code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>IME 控制有安全意涵&lt;/strong>。&lt;code>enableIMEPersonalizedLearning: false&lt;/code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>事後 hotfix 的六個參數每個都有 gotcha&lt;/strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 &lt;code>autocorrect: false&lt;/code> 但忘了 &lt;code>enableIMEPersonalizedLearning: false&lt;/code>），漏掉的那個就成為安全漏洞。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>功能規格新增「輸入機制決策表」&lt;/strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。&lt;/li>
&lt;li>&lt;strong>輸入機制跟 protocol 一起設計&lt;/strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。&lt;/li>
&lt;li>&lt;strong>安全敏感參數強制列入 review checklist&lt;/strong>：&lt;code>enableIMEPersonalizedLearning&lt;/code>、&lt;code>autocorrect&lt;/code> 在處理 secret 的輸入框中是安全要求，不是可選項。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計 mobile 輸入機制 → &lt;a href="https://tarrragon.github.io/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表&lt;/a>&lt;/li>
&lt;li>想看 protocol 跟輸入的關聯 → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type&lt;/a>（sendData 的型別決策）&lt;/li>
&lt;li>想做安全審查 → 待補：CLI 輸入安全 checklist&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明輸入機制是設計產物（在企劃階段決定），不是實作細節（在寫 code 時順便加）。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 Terminal 畫面在 W2 修復前沒有任何文字輸入元件。使用者只能透過底部工具列的特殊鍵（Esc/Tab/Ctrl/方向鍵）操作終端機，無法打字。</p>
<p>W2-001 修復時加入的 <code>TextField</code> 及其參數：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">TextField</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">keyboardType:</span> <span class="n">TextInputType</span><span class="p">.</span><span class="n">visiblePassword</span><span class="p">,</span>   <span class="c1">// 避免自動校正
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nl">enableSuggestions:</span> <span class="kc">false</span><span class="p">,</span>                       <span class="c1">// 關閉建議列
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nl">autocorrect:</span> <span class="kc">false</span><span class="p">,</span>                             <span class="c1">// 關閉自動校正
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nl">enableIMEPersonalizedLearning:</span> <span class="kc">false</span><span class="p">,</span>           <span class="c1">// 關閉 IME 個人化學習
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nl">onSubmitted:</span> <span class="n">_submitInput</span><span class="p">,</span>                      <span class="c1">// Enter 送出整行
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nl">textInputAction:</span> <span class="n">TextInputAction</span><span class="p">.</span><span class="n">send</span><span class="p">,</span>          <span class="c1">// 鍵盤顯示「傳送」
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="p">)</span></span></span></code></pre></div><p>每個參數都是一個設計決策，但沒有一個是事前規劃的 — 全部是寫 code 時臨時判斷。</p>
<table>
  <thead>
      <tr>
          <th>設計決策</th>
          <th>事前規劃</th>
          <th>事後 hotfix 的風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>visiblePassword</code></td>
          <td>沒有</td>
          <td>如果用預設 <code>text</code>，iOS 會自動校正 <code>ls -la</code> 成其他東西</td>
      </tr>
      <tr>
          <td><code>enableSuggestions: false</code></td>
          <td>沒有</td>
          <td>建議列遮擋終端機畫面下方</td>
      </tr>
      <tr>
          <td><code>autocorrect: false</code></td>
          <td>沒有</td>
          <td>路徑 <code>/usr/bin/</code> 可能被校正</td>
      </tr>
      <tr>
          <td><code>enableIMEPersonalizedLearning: false</code></td>
          <td>沒有</td>
          <td>CLI 輸入含密碼和路徑，IME 學習是安全風險</td>
      </tr>
      <tr>
          <td><code>onSubmitted</code>（整行送出）</td>
          <td>沒有</td>
          <td>如果逐字元送出，Tab 補全和命令編輯需要完全不同的 protocol 設計</td>
      </tr>
      <tr>
          <td><code>TextInputAction.send</code></td>
          <td>沒有</td>
          <td>如果用 <code>newline</code>，使用者按 Enter 會換行不送出</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>輸入設計影響 UI layout 和 protocol</strong>。<code>onSubmitted</code>（整行送出）vs 逐字元即時送出不只是 UI 問題 — 整行送出代表 protocol 層送的是 <code>command\n</code>，逐字元送出代表每個按鍵都是一個 WS frame。這個決策應該在 protocol spec 階段就做，因為它影響 server 端的行為預期。</p>
</li>
<li>
<p><strong>IME 控制有安全意涵</strong>。<code>enableIMEPersonalizedLearning: false</code> 不只是 UX 偏好 — CLI 輸入可能包含資料庫密碼、API key、伺服器路徑。IME 學習這些內容等於把 secret 存到了 IME 的詞庫裡，跨 app 可用。這是安全問題，不是 UX 問題。</p>
</li>
<li>
<p><strong>事後 hotfix 的六個參數每個都有 gotcha</strong>。如果這些決策在企劃階段做，可以寫成決策表並在 code review 時對照。事後 hotfix 時開發者可能漏掉其中一兩個（例如只加 <code>autocorrect: false</code> 但忘了 <code>enableIMEPersonalizedLearning: false</code>），漏掉的那個就成為安全漏洞。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>功能規格新增「輸入機制決策表」</strong>：keyboard type / submit model / IME policy / special keys 四個維度，每個列出選項和取捨理由。</li>
<li><strong>輸入機制跟 protocol 一起設計</strong>：「整行送出」還是「逐字元」決定了 WS 訊框的設計，必須在 protocol spec 階段決定。</li>
<li><strong>安全敏感參數強制列入 review checklist</strong>：<code>enableIMEPersonalizedLearning</code>、<code>autocorrect</code> 在處理 secret 的輸入框中是安全要求，不是可選項。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 mobile 輸入機制 → <a href="/blog/ux-design/03-input-mechanism/four-dimension-decision/" data-link-title="輸入機制決策表" data-link-desc="Keyboard type / submit model / IME policy / special keys 四個維度的決策框架 — 每個維度都是設計決策，影響 UI layout 和 protocol">輸入機制決策表</a></li>
<li>想看 protocol 跟輸入的關聯 → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type</a>（sendData 的型別決策）</li>
<li>想做安全審查 → 待補：CLI 輸入安全 checklist</li>
</ul>
]]></content:encoded></item><item><title>模組三：輸入機制設計</title><link>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/03-input-mechanism/</guid><description>&lt;p>回答「使用者怎麼輸入資料」。手機鍵盤和桌面鍵盤的差異比想像的大。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>UF-6&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>6 個 TextField 參數全是事後 hotfix — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UF-7&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>enableIMEPersonalizedLearning 有安全意涵（secret → IME 詞庫）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UF-8&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &amp;#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3&lt;/a>&lt;/td>
 &lt;td>整行送出 vs 逐字元影響 protocol 設計 — &lt;strong>SSoT 主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 輸入機制四維度決策表（keyboard type / submit model / IME policy / special keys）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Terminal app 輸入設計（CLI 特殊需求）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 表單 UX 模式（validate / auto-fill / error feedback）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 搜尋 UX 模式（debounce / instant / suggestion）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 安全敏感輸入框的 IME 控制 checklist&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試&lt;/a>：整行 vs 逐字元影響 protocol test 斷言&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>：IME 個人化學習 = secret 洩漏風險&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「使用者怎麼輸入資料」。手機鍵盤和桌面鍵盤的差異比想像的大。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>UF-6</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>6 個 TextField 參數全是事後 hotfix — <strong>本模組主寫</strong></td>
      </tr>
      <tr>
          <td>UF-7</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>enableIMEPersonalizedLearning 有安全意涵（secret → IME 詞庫）</td>
      </tr>
      <tr>
          <td>UF-8</td>
          <td><a href="/blog/ux-design/cases/terminal-input-mechanism-absent/" data-link-title="U.C3 終端機文字輸入機制未設計、事後 hotfix 補 TextField" data-link-desc="Flutter 終端機 app 的鍵盤輸入完全未設計 — 沒有 TextField、沒有 keyboard type 選擇、沒有 IME 控制。W2 修復時才補上 TextField &#43; 6 個參數（enableSuggestions/autocorrect/enableIMEPersonalizedLearning/keyboardType/textInputAction/onSubmitted），全是散落 hotfix">U.C3</a></td>
          <td>整行送出 vs 逐字元影響 protocol 設計 — <strong>SSoT 主寫</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 輸入機制四維度決策表（keyboard type / submit model / IME policy / special keys）</li>
<li><input checked="" disabled="" type="checkbox"> Terminal app 輸入設計（CLI 特殊需求）</li>
<li><input checked="" disabled="" type="checkbox"> 表單 UX 模式（validate / auto-fill / error feedback）</li>
<li><input checked="" disabled="" type="checkbox"> 搜尋 UX 模式（debounce / instant / suggestion）</li>
<li><input checked="" disabled="" type="checkbox"> 安全敏感輸入框的 IME 控制 checklist</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">testing 模組三 協議整合測試</a>：整行 vs 逐字元影響 protocol test 斷言</li>
<li>→ <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>：IME 個人化學習 = secret 洩漏風險</li>
</ul>
]]></content:encoded></item><item><title>鍵盤可達性：focus indicator、tab 順序、escape 路徑</title><link>https://tarrragon.github.io/blog/report/keyboard-accessibility/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/keyboard-accessibility/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>鍵盤使用者導航三要素：focus 可見、tab 順序合理、有 escape 路徑。&lt;/strong> 三者任一缺失、鍵盤使用者就卡住。視覺使用者看不到 focus 也能用滑鼠繼續、鍵盤使用者沒有 fallback。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>鍵盤可達性&lt;/strong>。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>視覺呈現面的 a11y&lt;/strong>（對比 / 放大）由 &lt;a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>行動 / motor 使用者的 a11y&lt;/strong>（hit target）由 &lt;a href="../motor-accessibility-hit-target/">#53 Motor 可達性&lt;/a> 處理&lt;/li>
&lt;li>&lt;strong>DOM 移動時的 focus 處理&lt;/strong>由 &lt;a href="../focus-management-on-dom-move/">#37 focus management on DOM move&lt;/a> 處理（本篇處理「靜態 focus 設計」、#37 處理「動態 focus 移動」）&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼鍵盤可達性需要獨立盤點">為什麼鍵盤可達性需要獨立盤點&lt;/h2>
&lt;h3 id="使用者類型">使用者類型&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>使用者&lt;/th>
 &lt;th>為什麼用鍵盤&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>全盲（screen reader 使用者）&lt;/td>
 &lt;td>完全靠鍵盤、滑鼠看不到游標位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低視力&lt;/td>
 &lt;td>鍵盤比滑鼠精準（不需要瞄準）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Motor 障礙&lt;/td>
 &lt;td>鍵盤比滑鼠手部負擔小&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Power user&lt;/td>
 &lt;td>鍵盤比滑鼠快&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最後一類占人口比例不小 — 鍵盤可達性對全體使用者都有價值、不只 a11y 使用者。&lt;/p>
&lt;h3 id="三要素的失敗模式">三要素的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>要素&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;th>後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Focus 可見&lt;/td>
 &lt;td>&lt;code>outline: 0&lt;/code> 移除預設 focus 但沒補替代&lt;/td>
 &lt;td>鍵盤使用者不知道 focus 在哪、迷失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab 順序&lt;/td>
 &lt;td>順序跟視覺布局不一致&lt;/td>
 &lt;td>跳來跳去、迷失&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Escape 路徑&lt;/td>
 &lt;td>Modal 沒有 ESC 關閉&lt;/td>
 &lt;td>卡在 modal 出不來&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三者都是「視覺使用者通常不會碰到、鍵盤使用者必碰」— 開發者用滑鼠測 100% OK、鍵盤使用者一進去就壞。&lt;/p>
&lt;hr>
&lt;h2 id="風險點-1focus-indicator-的可見度">風險點 1：Focus indicator 的可見度&lt;/h2>
&lt;p>&lt;strong>位置&lt;/strong>：tab focus 到 search input、scope radio、filter checkbox 等元素。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>瀏覽器預設 focus outline（藍色 2px）&lt;/li>
&lt;li>某些 theme 用 &lt;code>outline: 0&lt;/code> 移除 — 鍵盤使用者迷失&lt;/li>
&lt;li>自訂 outline 要對比足夠（WCAG 2.4.7、AA 3:1 對比 + 至少 2px 寬）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：鍵盤使用者 tab 過去看不到 focus 在哪、不知道下一個 enter 會激活誰。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：用 keyboard tab 過所有互動元素、確認每個都有可見 focus。&lt;/p>
&lt;p>&lt;strong>修正方向&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-css" data-lang="css">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c">/* 預設 — 信任瀏覽器 outline */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c">/* 不寫 outline: 0 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c">/* 客製 — 用 :focus-visible（只在鍵盤觸發時顯示、滑鼠點擊不顯示） */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">:&lt;/span>&lt;span class="nd">focus-visible&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">outline&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="kc">solid&lt;/span> &lt;span class="kc">currentColor&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">outline-offset&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="kt">px&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c">/* 移除 outline 必須補 box-shadow / border 等替代 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="nt">button&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nd">focus&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">outline&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">box-shadow&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="kt">px&lt;/span> &lt;span class="nf">var&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">--&lt;/span>&lt;span class="n">focus&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="kc">color&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>:focus-visible&lt;/code> 是現代做法 — 滑鼠使用者不看到 outline（不會覺得「煩」）、鍵盤使用者看到 outline（必要的回饋）。&lt;/p>
&lt;h3 id="focus-indicator-的對比度">Focus indicator 的對比度&lt;/h3>
&lt;p>WCAG 2.4.11 要求 focus indicator 跟相鄰背景對比 ≥ 3:1：&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>鍵盤使用者導航三要素：focus 可見、tab 順序合理、有 escape 路徑。</strong> 三者任一缺失、鍵盤使用者就卡住。視覺使用者看不到 focus 也能用滑鼠繼續、鍵盤使用者沒有 fallback。</p>
<blockquote>
<p>本篇焦點：<strong>鍵盤可達性</strong>。</p>
<ul>
<li><strong>視覺呈現面的 a11y</strong>（對比 / 放大）由 <a href="../visual-aids-contrast-zoom-responsive/">#40 視覺輔助</a> 處理</li>
<li><strong>行動 / motor 使用者的 a11y</strong>（hit target）由 <a href="../motor-accessibility-hit-target/">#53 Motor 可達性</a> 處理</li>
<li><strong>DOM 移動時的 focus 處理</strong>由 <a href="../focus-management-on-dom-move/">#37 focus management on DOM move</a> 處理（本篇處理「靜態 focus 設計」、#37 處理「動態 focus 移動」）</li>
</ul></blockquote>
<hr>
<h2 id="為什麼鍵盤可達性需要獨立盤點">為什麼鍵盤可達性需要獨立盤點</h2>
<h3 id="使用者類型">使用者類型</h3>
<table>
  <thead>
      <tr>
          <th>使用者</th>
          <th>為什麼用鍵盤</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全盲（screen reader 使用者）</td>
          <td>完全靠鍵盤、滑鼠看不到游標位置</td>
      </tr>
      <tr>
          <td>低視力</td>
          <td>鍵盤比滑鼠精準（不需要瞄準）</td>
      </tr>
      <tr>
          <td>Motor 障礙</td>
          <td>鍵盤比滑鼠手部負擔小</td>
      </tr>
      <tr>
          <td>Power user</td>
          <td>鍵盤比滑鼠快</td>
      </tr>
  </tbody>
</table>
<p>最後一類占人口比例不小 — 鍵盤可達性對全體使用者都有價值、不只 a11y 使用者。</p>
<h3 id="三要素的失敗模式">三要素的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>要素</th>
          <th>失敗模式</th>
          <th>後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus 可見</td>
          <td><code>outline: 0</code> 移除預設 focus 但沒補替代</td>
          <td>鍵盤使用者不知道 focus 在哪、迷失</td>
      </tr>
      <tr>
          <td>Tab 順序</td>
          <td>順序跟視覺布局不一致</td>
          <td>跳來跳去、迷失</td>
      </tr>
      <tr>
          <td>Escape 路徑</td>
          <td>Modal 沒有 ESC 關閉</td>
          <td>卡在 modal 出不來</td>
      </tr>
  </tbody>
</table>
<p>三者都是「視覺使用者通常不會碰到、鍵盤使用者必碰」— 開發者用滑鼠測 100% OK、鍵盤使用者一進去就壞。</p>
<hr>
<h2 id="風險點-1focus-indicator-的可見度">風險點 1：Focus indicator 的可見度</h2>
<p><strong>位置</strong>：tab focus 到 search input、scope radio、filter checkbox 等元素。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li>瀏覽器預設 focus outline（藍色 2px）</li>
<li>某些 theme 用 <code>outline: 0</code> 移除 — 鍵盤使用者迷失</li>
<li>自訂 outline 要對比足夠（WCAG 2.4.7、AA 3:1 對比 + 至少 2px 寬）</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者 tab 過去看不到 focus 在哪、不知道下一個 enter 會激活誰。</p>
<p><strong>第一個該查的</strong>：用 keyboard tab 過所有互動元素、確認每個都有可見 focus。</p>
<p><strong>修正方向</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c">/* 預設 — 信任瀏覽器 outline */</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c">/* 不寫 outline: 0 */</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c">/* 客製 — 用 :focus-visible（只在鍵盤觸發時顯示、滑鼠點擊不顯示） */</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="kc">currentColor</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">outline-offset</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c">/* 移除 outline 必須補 box-shadow / border 等替代 */</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nt">button</span><span class="p">:</span><span class="nd">focus</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span> <span class="k">box-shadow</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">0</span> <span class="mi">0</span> <span class="mi">3</span><span class="kt">px</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">focus</span><span class="o">-</span><span class="kc">color</span><span class="p">);</span> <span class="p">}</span></span></span></code></pre></div><p><code>:focus-visible</code> 是現代做法 — 滑鼠使用者不看到 outline（不會覺得「煩」）、鍵盤使用者看到 outline（必要的回饋）。</p>
<h3 id="focus-indicator-的對比度">Focus indicator 的對比度</h3>
<p>WCAG 2.4.11 要求 focus indicator 跟相鄰背景對比 ≥ 3:1：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* 較差 — 灰底 + 灰 outline、對比不足 */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">.</span><span class="nc">button</span> <span class="p">{</span> <span class="k">background</span><span class="p">:</span> <span class="mh">#f0f0f0</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">.</span><span class="nc">button</span><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#cccccc</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c">/* 好 — 跟背景對比足夠 */</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">button</span><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#0066cc</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><hr>
<h2 id="風險點-2tab-順序與視覺布局的對齊">風險點 2：Tab 順序與視覺布局的對齊</h2>
<p><strong>位置</strong>：搜尋頁元素：H1 → search input → scope radio → results → filter sidebar。</p>
<p><strong>判讀</strong>：</p>
<p>預設 tab 順序 = DOM 順序。如果視覺布局跟 DOM 順序不一致（例如 sidebar 在右、但 DOM 在前）、鍵盤使用者體驗：</p>
<ul>
<li>Tab 1：H1（OK）</li>
<li>Tab 2：跑到 sidebar（視覺在右下、鍵盤跳過去）</li>
<li>Tab 3：search input（視覺在左上、鍵盤跳回來）</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者 tab 順序看似隨機、失去空間感。</p>
<p><strong>第一個該查的</strong>：用 keyboard tab 過所有互動元素、看 focus 移動順序是否符合視覺閱讀順序（左到右、上到下）。</p>
<p><strong>修正方向</strong>：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM 順序對齊視覺順序</td>
          <td>改 HTML 結構讓 DOM 順序就是 tab 順序</td>
      </tr>
      <tr>
          <td>用 <code>tabindex</code> 調整順序</td>
          <td>顯式控制 tab 順序（風險：違反 DOM 順序、對 screen reader 仍依 DOM）</td>
      </tr>
      <tr>
          <td>Skip link 跳過長 navigation</td>
          <td>讓鍵盤使用者快速跳到主內容</td>
      </tr>
  </tbody>
</table>
<p>預設選「DOM 順序對齊視覺順序」 — 不需要 <code>tabindex</code>、對所有 a11y 工具都正確。</p>
<h3 id="skip-link-設計">Skip link 設計</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#main&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;skip-link&#34;</span><span class="p">&gt;</span>跳到主內容<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">nav</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">nav</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">&lt;</span><span class="nt">main</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;main&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">main</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">.</span><span class="nc">skip-link</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">absolute</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">-40</span><span class="kt">px</span><span class="p">;</span>       <span class="c">/* 預設藏起來 */</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">bg</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">padding</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">.</span><span class="nc">skip-link</span><span class="p">:</span><span class="nd">focus</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>            <span class="c">/* tab 到時顯示 */</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>第一個 tab 焦點 = skip link、鍵盤使用者可以選擇跳過 nav 直達主內容。</p>
<hr>
<h2 id="風險點-3modal--overlay-的-escape-路徑">風險點 3：Modal / overlay 的 escape 路徑</h2>
<p><strong>位置</strong>：Pagefind drawer 在 mobile 模式展開、filter sidebar 在某些 layout 是 modal-like。</p>
<p><strong>判讀</strong>：</p>
<p>鍵盤使用者進入 modal 後需要：</p>
<ol>
<li>按 ESC 可以關閉</li>
<li>Tab 順序限制在 modal 內（focus trap、不會 tab 到背景元素）</li>
<li>關閉 modal 後 focus 回到觸發元素</li>
</ol>
<p>任一缺失 = 卡住。</p>
<p><strong>症狀</strong>：鍵盤使用者打開 filter drawer 後 tab 跑到背景元素、不知道怎麼關 drawer。</p>
<p><strong>第一個該查的</strong>：開啟 modal / drawer / overlay、按 ESC 看會不會關、tab 看會不會跑到背景。</p>
<p><strong>修正方向</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">openModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">showModal</span><span class="o">?</span><span class="p">.()</span> <span class="o">||</span> <span class="p">(</span><span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;block&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="c1">// ESC 關閉
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="nx">modal</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">===</span> <span class="s1">&#39;Escape&#39;</span><span class="p">)</span> <span class="nx">closeModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// Focus trap（簡化版）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">focusables</span> <span class="o">=</span> <span class="nx">modal</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;button, input, select, [tabindex]&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">focusables</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">?</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;keydown&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">key</span> <span class="o">!==</span> <span class="s1">&#39;Tab&#39;</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="kd">var</span> <span class="nx">first</span> <span class="o">=</span> <span class="nx">focusables</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="kd">var</span> <span class="nx">last</span> <span class="o">=</span> <span class="nx">focusables</span><span class="p">[</span><span class="nx">focusables</span><span class="p">.</span><span class="nx">length</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span> <span class="o">===</span> <span class="nx">first</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">      <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> <span class="nx">last</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">e</span><span class="p">.</span><span class="nx">shiftKey</span> <span class="o">&amp;&amp;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span> <span class="o">===</span> <span class="nx">last</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span> <span class="nx">first</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="kd">function</span> <span class="nx">closeModal</span><span class="p">(</span><span class="nx">modal</span><span class="p">,</span> <span class="nx">trigger</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="nx">modal</span><span class="p">.</span><span class="nx">close</span><span class="o">?</span><span class="p">.()</span> <span class="o">||</span> <span class="p">(</span><span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;none&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">  <span class="nx">trigger</span><span class="o">?</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>  <span class="c1">// 焦點回觸發元素
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p><strong>用 <code>&lt;dialog&gt;</code> 元素自動 trap</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">dialog</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;filter-modal&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">dialog</span><span class="p">&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">modal</span><span class="p">.</span><span class="nx">showModal</span><span class="p">();</span>  <span class="c1">// 自動 focus trap + ESC 處理
</span></span></span></code></pre></div><p><code>&lt;dialog&gt;</code> 是現代做法 — 鍵盤行為由瀏覽器處理、不需要手寫 trap 邏輯。</p>
<hr>
<h2 id="設計取捨focus-處理策略">設計取捨：focus 處理策略</h2>
<p>當需要客製 focus 視覺時、四種做法：</p>
<h3 id="a信任瀏覽器預設-outline這個專案的預設">A：信任瀏覽器預設 outline（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：完全不寫 <code>outline</code> 規則、瀏覽器藍色 outline 自動套用</li>
<li><strong>選 A 的理由</strong>：成本最低、跨瀏覽器一致、不會意外破壞</li>
<li><strong>適合</strong>：對 focus 視覺沒有強烈品牌需求</li>
<li><strong>代價</strong>：focus 看起來「不夠精緻」（瀏覽器預設不一定符合品牌風格）</li>
</ul>
<h3 id="b用-focus-visible-客製-outline">B：用 <code>:focus-visible</code> 客製 outline</h3>
<ul>
<li><strong>機制</strong>：<code>:focus-visible { outline: 2px solid var(--brand); }</code>、滑鼠點擊不顯示</li>
<li><strong>跟 A 的取捨</strong>：B 達到品牌一致性、滑鼠使用者不被「煩」；A 簡單但視覺一般</li>
<li><strong>B 比 A 好的情境</strong>：品牌設計嚴格要求 focus 視覺</li>
</ul>
<h3 id="c用-box-shadow-取代-outline">C：用 <code>box-shadow</code> 取代 outline</h3>
<ul>
<li><strong>機制</strong>：<code>:focus-visible { box-shadow: 0 0 0 3px var(--focus); outline: 0; }</code></li>
<li><strong>跟 B 的取捨</strong>：C 跟 outline 視覺差異是「跟著元素圓角」、適合圓角 UI；outline 永遠是矩形</li>
<li><strong>C 比 B 好的情境</strong>：圓角元素需要 focus 跟隨圓角</li>
</ul>
<h3 id="d完全移除-focus-indicator">D：完全移除 focus indicator</h3>
<ul>
<li><strong>機制</strong>：<code>*:focus { outline: 0; }</code>、不補替代</li>
<li><strong>成本特別高的原因</strong>：違反 WCAG 2.4.7、鍵盤使用者完全無法導航</li>
<li><strong>D 是反模式</strong>：違反 WCAG 2.4.7（合規層） — 即使品牌追求極簡、也該保留 focus indicator</li>
</ul>
<p>「邏輯 tab 順序」要素的詳細展開（DOM vs tabindex 的取捨、跟 mental model 對齊）見 <a href="../tab-order-mental-model-alignment/">#71 Tab Order = DOM Order = Mental Model 三者對齊</a>。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>篇</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../focus-management-on-dom-move/">#37 Focus management on DOM move</a></td>
          <td>互補 — 本篇處理「靜態 focus 設計」、#37 處理「DOM 移動時 focus 該怎麼跟」</td>
      </tr>
      <tr>
          <td><a href="../native-html-over-aria-role/">#39 Native HTML 優先於 ARIA role</a></td>
          <td>用 <code>&lt;button&gt;</code> / <code>&lt;dialog&gt;</code> / <code>&lt;input&gt;</code> 等 native element、自動獲得正確 keyboard 行為</td>
      </tr>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>客製 focus 樣式時、注意不要打破 framework 內部的 focus 邏輯</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="開發階段檢查清單">開發階段檢查清單</h2>
<table>
  <thead>
      <tr>
          <th>檢查</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Focus 可見</td>
          <td>拔掉滑鼠、只用鍵盤、tab 過所有互動元素、確認每個都有可見 focus</td>
      </tr>
      <tr>
          <td>Focus 對比</td>
          <td>DevTools Contrast Ratio 量 focus indicator 跟背景對比 ≥ 3:1</td>
      </tr>
      <tr>
          <td>Tab 順序</td>
          <td>tab 過去確認順序符合視覺閱讀順序</td>
      </tr>
      <tr>
          <td>ESC 關閉</td>
          <td>開啟 modal / drawer、按 ESC 看會不會關</td>
      </tr>
      <tr>
          <td>Focus trap</td>
          <td>開啟 modal、tab 看是否限制在 modal 內</td>
      </tr>
      <tr>
          <td>Focus return</td>
          <td>關閉 modal、看 focus 是否回觸發元素</td>
      </tr>
  </tbody>
</table>
<p>每個 ~30 秒、開發完成前跑一輪。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鍵盤使用者反映「不知道 focus 在哪」</td>
          <td>確認沒有 <code>outline: 0</code> 沒補替代、用 <code>:focus-visible</code></td>
      </tr>
      <tr>
          <td>Tab 順序看起來隨機</td>
          <td>DOM 順序對齊視覺順序、必要時用 skip link</td>
      </tr>
      <tr>
          <td>Modal 開啟後鍵盤使用者卡住</td>
          <td>加 ESC 關閉 + focus trap、或改用 <code>&lt;dialog&gt;</code></td>
      </tr>
      <tr>
          <td>Modal 關閉後 focus 跑到頁面開頭</td>
          <td>關閉時手動 <code>trigger.focus()</code></td>
      </tr>
      <tr>
          <td>Focus 在 dark mode 看不清</td>
          <td>加對比度檢查（≥ 3:1）</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：鍵盤可達性的三要素都是「視覺使用者通常不會碰、鍵盤使用者必碰」 — 開發階段必須拔滑鼠測一輪、不能依賴使用者通報。</p>
]]></content:encoded></item></channel></rss>