iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Modern Web

前端不寫 Python,照樣 ship 一把網頁無障礙 CLI系列 第 19

Day 19:我的表單焦點跳對了,畫面上卻沒有一個字說明原因

  • 分享至 

  • xImage
  •  

Day 19 · W3 · 無障礙線 · 難度 ★★★☆☆

本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01

空表單按下送出,焦點跳到「姓名」那一格。

跳得很準。而我一行都沒寫。

昨天量頁籤的焦點順序,今天換表單。我原本以為「送出之後焦點要跳到第一個沒填的欄位」會是最容易漏掉的那件事,實測下來它是自己會動的那件。

真正沒人做的是另一半。

一句話主軸:required 換來的焦點跳轉是免費的,但它只買到一半。

全站只有一個表單,而它是搜尋框

先講一件我自己都沒預期的事。六個頁面掃過去找 <form>

/           1 個   頁首搜尋框
/about-us   1 個   頁首搜尋框
/pricing    1 個   頁首搜尋框
/contact    1 個   頁首搜尋框
/works      1 個   頁首搜尋框
/blog       1 個   頁首搜尋框

聯絡表單不在裡面。它在一個對話框裡,要按下「預約諮詢」才會被掛進畫面。

這件事等一下會變成重點,先記著。

送出之後,焦點跳了,畫面卻沒說話

把對話框打開,五個欄位全部留空,按送出:

焦點落點              <input type="text"> 姓名   ← 跳了
aria-live 容器        0 個
aria-invalid="true"   0 個
DOM 裡的錯誤文字      0 筆

焦點跳得完全正確:停在第一個沒填的必填欄位上。

而畫面上(更精確地說,DOM 裡)沒有任何一個字說明它為什麼停在那裡。

原因在這一行沒有出現的屬性:這個表單沒有 novalidate,欄位帶著原生 required。所以整套驗證是瀏覽器接管的:它擋下送出、聚焦第一個無效欄位、彈出一個小泡泡。

那個泡泡從來不進 DOM。抓不到、改不了樣式、aria-live 播不了、下一個互動就消失。

瀏覽器在這裡像一個只會擋人的門衛:它確實把你攔下來了,也確實把你帶到出問題的那一格,但它不說為什麼。

碼表要的是兩件事,不是一件

對應的檢測碼是 GN2330300E,WCAG 3.3.3,AA 級。條文原文:

提供文字描述以指明未完成的必填欄位,並建立可以讓使用者跳到出錯之處的機制

讀慢一點:「文字描述」和「跳轉機制」,中間那個字是「並」。

我做到了第二件,第一件沒有。而做到的那件還是瀏覽器代勞的。

這條規則我四月就寫進工具裡了,但當時的實作只檢查焦點跳轉那一半。一個焦點默默移動、畫面什麼都沒說的表單,在那個版本裡是通過的。

八月底重新把每條規則的實作跟碼表原文對一遍,才發現這條檢查得比條文要求的少。補上第二個義務之後,同一個表單的判定就從通過變成不通過 —— 表單一個字都沒改,變的是規則終於問完了整句話。

五種寫法,只有一種兩件都成立

我做了一個測試檔,同一組欄位寫五遍,每遍的驗證方式不同:

必填錯誤提示五種寫法的對照表:狀態一用瀏覽器原生驗證,沒有文字描述、焦點跳到第一個錯欄、空表單不會真的送出、規則判定 fail;狀態二自己接手驗證只印訊息,有文字描述、焦點留在送出鍵、不會真送出、判定 caveat;狀態三自己接手兩件都做,有文字描述、焦點跳到第一個錯欄、不會真送出、判定 caveat;狀態四自己接手但什麼都沒做,沒有文字描述、整頁重載、空表單真的送出、判定 caveat;狀態五留著原生驗證再用 invalid 事件補上文字描述,有文字描述、焦點跳到第一個錯欄、不會真送出、規則不報等於通過

五種寫法裡,只有最後一種同時滿足碼表的兩個義務。

狀態二跟狀態三共用同一段驗證程式,差別只有最後那一行 focus()。少那一行,焦點就留在送出鍵上,使用者被告知「有欄位沒填」,卻得自己回頭找是哪一格。

狀態四最兇:加了 novalidate 卻忘了補驗證。實測按下送出,網址直接變成:

?name=&email=

空表單真的送出去了。

留著原生驗證,把講清楚那半接回來

最直覺的補法是加 novalidate,自己接管全部驗證。但那條路有代價,等一下說。

有一個更省的做法:required 照留,只把「講清楚」那半接回來。 瀏覽器發現欄位無效時會先派發一個 invalid 事件,攔住它就好:

form.addEventListener('invalid', (e) => {
  e.preventDefault();                    // 壓掉原生泡泡,改用自己的文字
  document.getElementById(e.target.id + '-err').hidden = false;
  e.target.setAttribute('aria-invalid', 'true');
  if (!first) first = e.target;          // invalid 依文件順序派發
  if (queued) return;
  queued = true;
  setTimeout(() => {
    summary.hidden = false;
    summary.textContent = '有 ' + n + ' 個必填欄位尚未填寫';
    first.focus();
    first = null; queued = false;
  }, 0);
}, true);

瀏覽器照樣擋下送出,所以空表單不會產生真資料;錯誤文字進了 DOM,role="alert" 會被報讀出來;焦點自己送到第一個錯欄。兩個義務都成立。

兩個坑是寫這段時實際踩到的:

  • invalid 事件不會冒泡,必須用捕獲階段監聽,也就是第三個參數要給 true
  • 聚焦要排到 setTimeout 的下一輪。第一版我用 queueMicrotask,焦點被瀏覽器自己的聚焦蓋過去,跑到第二個欄位上,量了才發現

為什麼預設掃不到它

回到開頭那件事:表單在對話框裡,初始畫面上不存在。

同一頁掃兩次,差別只在有沒有讓工具去點那顆按鈕:

不點        9 條,表單相關 0 條
點了       11 條
             GN2330300E   焦點有跳,但沒有文字描述
             GN3330602E   對話框內表單 5 個欄位,請人工走一次流程

點按鈕這件事預設是關的。 理由不是做不到,是在別人的正式網站上亂點按鈕,可能觸發預約、扣款、寄信、送出一筆分析事件。這跟 Day 11 講的偽陽性是同一種取捨:一個檢查工具造成的副作用,比它漏抓一條還難交代。

同一個理由也決定了工具敢不敢按送出鍵。看回上面那張表:狀態二、三、四全是 caveat,因為它們都帶 novalidate ——探針拒絕對這種表單空送,那一按在正式站上就是狀態四那個結果,一筆真的空記錄。

所以這條規則能自動判定的,只有還在用瀏覽器原生驗證的那一種。一旦你自己接手驗證,也就是真的有可能寫錯的時候,機器只能說「我不敢按,請人工走一次」。

工具能判與不能判的分界圖:左半邊是使用瀏覽器原生驗證的表單,探針可以安全地按下送出鍵,因此規則能給出通過或不通過的判定,但這種寫法的錯法只有一種;右半邊是自行接管驗證並加上 novalidate 的表單,探針拒絕按送出鍵以免產生真資料,因此規則只能給出 caveat 請人工確認,而這種寫法正是可能寫錯的地方

判得動的那一種,錯法只有一種;可能寫錯的那一種,判不動。

這是能力邊界,不是漏掉。工具在這裡說「我沒檢查」,比說「沒問題」誠實。

修的時候又挖出兩個洞

為了把上面那些數字量準,我發現規則跟探針各有一個 bug,兩個都是把「我沒檢查」變成了沉默。

第一個:那條規則報完第一個有問題的表單之後就整條停掉了。判斷「探針不敢按」的分支寫的是繼續看下一個,判斷「這個表單有問題」的分支寫的卻是結束。一頁上有搜尋框、聯絡表單、訂閱表單很正常,第一個出事,後面的就靜音了。

消失的不是失敗,是那些 caveat。報告從「這三個表單我沒檢查」變成什麼都不說。

第二個更隱蔽。探針點完按鈕會確認「這一下有沒有把我導到別頁」,導走了就退回來。它比對的是頁面載入時記下的網址 —— 但在整站掃描的共用瀏覽器路徑上,這個頁面已經被前面幾個探針動過,其中一個會按下「跳至主要內容」,網址早就多了個 #main-content

於是第一次點擊還沒發生,「已經被導走」就已經成立。三個候選按鈕全部被退回去,對話框裡的表單一個都探不到。

任何有跳至主要內容連結的頁面都中。網站做得越無障礙,這條壞得越穩定。

兩個都修掉了,並補了測試:先把 bug 放回去確認新測試會紅,再拿掉確認轉綠。修完對自家六頁重跑,只有那一頁多了一則失敗跟一則 caveat,其餘五頁一個字都沒動。

今天的重點

  • required 免費送你焦點跳轉,但碼表要的是兩件事。 中間那個字是「並」。
  • 瀏覽器的原生提示不進 DOM。 抓不到、改不了、播不了,你的網頁控制不了它。
  • invalid 事件是最省的補法:原生驗證照留,只把文字描述接回來。記得用捕獲階段、記得把聚焦排到下一輪。
  • 判得動的那一種,錯法只有一種。 自己接手驗證之後機器只能給 caveat,那不是漏掉,是邊界。
  • 會靜默的東西要有機器盯著。 兩個 bug 消失的都是 caveat,不是失敗。

明天 Day 20:檢查工具講完了,換那份出貨給 AI 的說明書。CLI 能跑,不等於 AI 會用對。 那份說明書真正在做事的不是「怎麼用」那幾節,是那些「不准」。


上一篇
Day 18:我把三顆按鈕從 Tab 順序裡拿掉,鍵盤才算走得對
下一篇
Day 20:CLI 能跑,不等於 AI 會用對
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言