問題與使用情境
完整頁面導覽會替換文件及其標題。SPA 軟導覽繼續使用同一份文件,焦點可能留在一個周圍內容已消失的連結上。螢幕閱讀器使用者可能不知道新的商品頁、結帳步驟或錯誤頁已經就緒。
策略需要區分五類變化:首次載入、使用者發起的新路由、目前任務內的篩選或排序、被替換或取消的導覽,以及瀏覽器上一頁/下一頁。它還要與對話框等元件自己的焦點協議共存。目標是提供可預測的上下文,而非每次 URL 變化都移動焦點。
面試官考察什麼
面試官首先看候選人能否建立語意決策模型。「每次路由變化都聚焦標題」看似無障礙,卻會破壞篩選、錨點變化、首次 hydration 和歷史還原。強回答先判斷使用者的任務是否改變。
其次是瀏覽器基礎:tabIndex={-1} 可讓標題接收程式化焦點,又不會把它加入依序 Tab 路徑;focus() 預設可能捲動元素;preventScroll 能拆分焦點還原與捲動還原;正數 tabindex 會製造脆弱的焦點順序。
最後是並行處理。如果路由 B 比 C 更晚完成,B 不能改回標題或搶走焦點。自動化能驗證作用中元素、標題和順序,卻不能證明每一種瀏覽器與輔助技術組合會如何播報。
回答前的釐清問題
- 哪些變化會開始新任務? 從商品頁進入結帳是新任務,調整排序通常不是。這決定焦點移動或留在觸發控制項。
- 何時算路由提交完成? 只在勝出的路由已渲染最終主標題後聚焦,不在點擊、URL 變化或骨架畫面出現時執行。
- 浮層由誰管理? 對話框負責自己的初始焦點、焦點限制和關閉後還原;路由策略不能與它競爭。
- 歷史記錄要還原什麼? 明確每個歷史項目還原焦點、捲動位置或兩者。兩者必須協調,避免連續跳動。
- 支援哪些瀏覽器和輔助技術? 播報行為存在差異,發布測試矩陣必須明確。
- 每個路由能否提供描述性標題和唯一主標題? 把它們設為路由契約;
main地標只作受控降級。
30 秒回答框架
「我先對導覽分類,再決定是否移動焦點。首次載入不強制聚焦。使用者發起的新任務路由只有在最終提交後才更新文件標題,並聚焦設定了 tabIndex=-1 的描述性 H1。同一任務內的篩選保留觸發控制項,透過持久的禮貌狀態區播報完成結果。每次導覽都有權杖,只有最新且已提交的路由能修改標題和焦點。上一頁/下一頁優先還原該歷史項目保存的有效語意目標,否則退回 H1,並與捲動還原協調。測試涵蓋首次載入、快速連續導覽、錯誤、篩選、歷史、鍵盤順序、可見焦點及真實螢幕閱讀器。」
分步深入分析
先建立轉換決策表:
| 轉換 | 焦點動作 | 播報方式 |
|---|---|---|
| 首次載入或 hydration | 不強制移動 | 使用原生文件與標題行為 |
| PUSH 到新任務 | 聚焦已提交路由的 H1 | 更新標題並聚焦標題 |
| 同任務篩選、排序、分頁 | 保留觸發控制項 | 必要時禮貌播報結果或狀態 |
| 上一頁/下一頁 POP | 還原有效目標,否則 H1 | 還原上下文或聚焦標題 |
| 重新導向或錯誤路由 | 聚焦該路由最終 H1 | 使用最終標題和主標題 |
| 對話框開啟或關閉 | 交給對話框協議 | 對話框名稱與返回目標 |
路由契約提供最終文件標題、描述性 H1 引用和語意路由鍵。H1 使用 tabIndex={-1},可被程式聚焦但不增加一個 Tab 停靠點,並保留清晰的焦點樣式。跳過導覽連結仍應是第一個可聚焦控制項並指向 main;它解決重複導覽略過問題,與路由焦點並不衝突。
焦點動作屬於提交邊界。為每次導覽嘗試分配遞增權杖。資料和介面完成後,僅當權杖仍為目前值且 H1 已掛載時才執行副作用。不要依賴固定延遲,網路和渲染速度會讓它產生競態。
beginNavigation(kind):
token = nextToken()
rememberCurrentFocus(historyEntryKey)
commitRoute(token, kind, title, heading):
if token != currentToken or heading is not connected:
return
document.title = title
if kind is PUSH or semantic-task REPLACE:
heading.focus()
else if kind is POP:
focus(validSavedTarget(historyEntryKey) or heading)保存穩定的路由內焦點 ID,不保存自動產生的 CSS 路徑。POP 時只還原仍存在、可見、可用且在還原狀態中有意義的元素,否則聚焦 H1。若路由器獨立還原捲動位置,可使用 focus({ preventScroll: true }) 後只還原一次捲動;一般 PUSH 通常讓聚焦自然顯示 H1 更清楚。
避免重複播報。先更新標題再聚焦新 H1,通常已提供足夠上下文,但具體語音取決於輔助技術。預設不要再用即時區域重複同一標題。篩選操作保留焦點時,可用持久的 role="status" 或 aria-live="polite" 播報最終結果數。assertive 可能中斷目前語音,只用於真正緊急的資訊。
測試判定應直接來自策略:首次 hydration 不能搶焦點;PUSH 最終提交後,document.activeElement 是新 H1、標題已更新,下一次 Tab 到達第一個合乎邏輯的互動控制項;篩選後觸發控制項仍有焦點,狀態只更新一次;A→B→C 競態中即使 B 最後完成,C 仍擁有標題和焦點;錯誤與重新導向聚焦各自標題;POP 還原有效目標或確定性降級。
自動化涵蓋上述 DOM 斷言、目標卸載、縮放及減少動態效果版面。隨後用鍵盤和支援矩陣中的真實組合手動測試,例如 VoiceOver/Safari,以及 NVDA 或 JAWS 與支援的瀏覽器。核對焦點可見、順序合理、捲動不突兀、播報可理解,並記錄版本,因為語音結果是整合行為,不是單靠 DOM 就能保證的結果。
高品質範例回答
「我會把焦點行為納入路由的語意契約。每個路由提供最終文件標題和描述性 H1 引用。首次 hydration 不移動焦點;使用者發起且改變任務的 PUSH 在最終提交後聚焦 H1;篩選和排序保留觸發控制項;POP 優先還原穩定的歷史目標,失敗時退回 H1。
H1 使用 tabIndex=-1,因此能被程式化聚焦而不會增加 Tab 停靠點。我保留可見焦點樣式和指向 main 的跳過導覽連結。標題和焦點只在勝出的路由提交後一起更新。每次導覽有一個權杖,過期非同步結果會被忽略,取消的路由就無法搶焦點。
歷史項目保存路由內語意焦點 ID,只還原已連接、可見且可用的目標。若捲動單獨還原,則用 preventScroll 聚焦後只處理一次捲動。同任務非同步更新使用持久的禮貌狀態區;不會在標題和即時區域重複播報路由名稱。
自動化驗證作用中元素、標題、Tab 順序、篩選保留、A→B→C 快速導覽、重新導向、錯誤和 POP 降級。最後用支援的鍵盤與螢幕閱讀器矩陣驗證真實語音、焦點樣式和捲動。這樣能區分確定的 DOM 行為與必須實際觀察的輔助技術行為。」
常見錯誤
- 每次 URL 變化都聚焦 → 篩選和錨點變化打斷目前任務 → 先按語意分類。
- 點擊連結時立即聚焦 → 目標可能尚不存在或被取消 → 只處理勝出的已提交路由。
- 使用固定延遲 → 不同渲染速度產生競態 → 使用路由生命週期和導覽權杖。
- 聚焦
body或使用正數tabindex→ 上下文和順序不明確 → 聚焦設定tabIndex=-1的描述性 H1。 - POP 總是跳到 H1 → 上一頁會遺失使用者位置 → 還原有效歷史目標並提供確定性降級。
- 重複播報標題 → 使用者聽到冗餘語音 → 路由優先用聚焦標題,同頁更新才用狀態區。
- 把自動掃描當成驗收 → 掃描無法驗證真實語音 → 加入鍵盤和輔助技術手動測試。
追問與回答
追問 1:為什麼不總是聚焦 main 地標?
H1 通常能更具體地命名新任務。路由無法提供標題時,main 可作為降級,但要求每個路由提供主標題能同時改善可見結構、文件層級和可測試性。
追問 2:滑鼠點擊導覽後也要移動焦點嗎?
使用者操作替換主要任務時,一致的上下文對不同輸入方式都有價值,包括會點擊的螢幕閱讀器使用者。應根據語意導覽決策,不要猜測使用者是否只用鍵盤。背景重新整理不能移動焦點。
追問 3:快速導覽時 B 比 C 更晚完成怎麼辦?
B 的權杖已不是目前值,其提交副作用直接返回,不能修改標題、焦點或狀態。中止 B 的請求可以節省資源,但取消可能太晚或不受支援,因此仍需權杖檢查。
追問 4:焦點還原和捲動還原如何配合?
一般 focus() 可能把目標捲動到視窗。POP 若由路由器還原保存的捲動位置,就用 preventScroll 驗證並聚焦目標,再執行一次捲動還原。只能有一個明確的捲動所有者。
追問 5:什麼時候使用 assertive 即時區域?
僅在延遲聽到會造成嚴重問題時使用,例如緊急工作階段或安全訊息。結果數量和完成狀態使用 polite。路由上下文通常由最終標題和聚焦 H1 提供,避免第二次播報。
追問 6:做到這些就符合 WCAG 嗎?
不能單憑這一項下結論。該策略有助於動態應用程式維持可理解的焦點順序和上下文,完整符合性還取決於語意、名稱、鍵盤操作、對比度、錯誤處理等要求,應按產品目標檢查完整使用者旅程。