題幹與適用場景
實作一個可以調整任務順序的清單。滑鼠和觸控使用者可以拖曳項目,但所有排序操作也必須能透過 鍵盤,以及無需持續按住並移動指標的點按完成。螢幕閱讀器使用者需要知道哪一項發生移動及它的 新位置。設計還要保持焦點、支援取消、不遺失或重複項目,並在儲存失敗時給出明確的可見順序。
基礎題假設清單完整呈現,每項都有穩定且唯一的 ID,每次只移動一項。虛擬清單、多選移動和併發 編輯放在追問中。框架不是主要決策:React 可以負責呈現,但本題考察的是輸入建模、無障礙語意、 狀態所有權和驗證。
拖曳只是增強層,真正的業務操作是「把項目 X 移到最終位置 Y」。指標拖曳、鍵盤命令和可見移動 控制項都應呼叫同一個操作,避免三種輸入路徑產生三套細微不同的順序。
面試官考察點
第一項是能否區分身分與位置。陣列索引只是暫時位置,不能作為 React key 或持久項目身分。穩定 ID 決定移動誰,最終索引或相鄰穩定 ID 決定移到哪裡。
第二項是無障礙要求是否準確。WCAG 鍵盤操作要求與 WCAG 2.2 拖曳動作要求彼此相關,但需要 分別滿足。只有鍵盤快速鍵,並沒有給「可以點按、卻無法持續按住拖動」的使用者提供單指標替代。 可見的上移、下移或移到指定位置控制項可以同時服務這兩種輸入。
第三項是互動狀態是否嚴謹。高品質回答會區分閒置、活動排序、提交和取消,保存原始順序用於 回復,處理 pointercancel、Escape 和無效放置,並避免把每次停留位置都儲存成業務更新。
第四項是輔助技術行為。原生清單和按鈕語意、明確的拖曳把手、簡短操作說明、穩定焦點、禮貌的 狀態播報和可見插入指示,比堆疊 ARIA 屬性更重要。aria-grabbed 與 aria-dropeffect 已被棄用, 不能把它們當作解決方案。
最後是能否給出依證據的驗證:純重排測試、滑鼠與觸控、純鍵盤、螢幕閱讀器、焦點斷言、減少 動態效果和強制色彩,以及儲存失敗恢復。自動無障礙掃描無法證明語音排序流程是否容易理解。
回答前需要釐清的問題
- 是在一個清單內排序,還是跨清單移動? 單清單只需要最終位置;跨清單還需要來源和目標
所有權、允許的放置類型及原子更新。
- 順序是否需要遠端儲存? 僅本地順序可以立即提交;遠端持久化需要待儲存、成功、失敗、
重試和版本衝突行為。
- 需要涵蓋哪些輸入? 如果需要觸控、鍵盤、開關裝置、語音或螢幕閱讀器,只有滑鼠的 HTML
拖放事件不夠。
- 是否有可見的點按替代? 鍵盤快速鍵解決的是另一項需求。對於拖曳並非本質的排序清單,
應提供無需按住和移動指標的控制項。
- 移動時即時預覽,還是放下後再改變? 即時預覽的視覺回饋更強,但取消必須恢復原順序,
播報也不能隨著每個指標像素連續輸出。
- 能否多選項目? 多項移動會把模型從單一 ID 變成有序 ID 集合,還要定義可移動項與鎖定項
混合時的規則。
- 清單是否虛擬化? 畫面外位置沒有 DOM 放置目標,命中測試、播報、總數和鍵盤移動都必須
作用於資料模型。
- 其他用戶端會不會併發排序? 若會,儲存契約需要清單版本或同等前置條件,以及明確衝突策略。
30 秒回答框架
「我會把排序建模為一個純命令:將穩定項目 ID 移到最終索引。指標拖曳、鍵盤操作和可見移動按鈕 都呼叫它。介面使用原生清單與按鈕語意,保留被移動把手的焦點並播報新位置。工作階段保存原順序, Escape、pointercancel、無效放置或儲存失敗時可以恢復。最後驗證排列不變量,並涵蓋滑鼠、觸控、 僅點按、鍵盤、螢幕閱讀器、焦點和失敗流程。」
分步深入解答
先建立唯一的資料操作。輸入是穩定 ID 和從零開始的最終索引,輸出是一個新順序。操作必須保證 每個 ID 恰好出現一次,並且不修改原陣列。
interface SortableItem {
id: string
label: string
}
function moveItem(
items: ReadonlyArray<SortableItem>,
itemId: string,
targetIndex: number,
): ReadonlyArray<SortableItem> {
const fromIndex = items.findIndex((item) => item.id === itemId)
if (fromIndex === -1 || items.length < 2) return items
const finalIndex = Math.max(0, Math.min(targetIndex, items.length - 1))
if (fromIndex === finalIndex) return items
const next = [...items]
const [movedItem] = next.splice(fromIndex, 1)
if (!movedItem) return items
next.splice(finalIndex, 0, movedItem)
return next
}陣列刪除、插入和複製都可能移動元素,因此這個命令的時間複雜度為 O(n),空間複雜度為 O(n)。 對完整呈現的普通任務清單,這個成本合適。超大虛擬清單可以保存可排序秩值,或依相鄰 ID 在 伺服器端移動,但只有規模要求能證明需要時才增加這層複雜度。
React key 必須使用穩定 ID。key={item.id} 讓 React 移動既有項目節點,並避免把新索引理解為 新身分,有助於保留把手焦點和項目內部狀態。如果非同步更新仍導致焦點遺失,可以依項目 ID 保存 ref,在提交後的呈現中恢復到同一個把手。不要把焦點送回清單開頭,也不要強迫指標使用者改變焦點。
即使沒有拖曳,基礎標記也應容易理解。有序清單和清單項目可以暴露集合結構。每項使用原生按鈕, 例如命名為「調整審查順序」。提供可見的上移、下移按鈕或「移到指定位置」操作,並在每個無障礙 名稱中包含項目名。邊界上無法執行的操作應停用。這些控制項可以點按和用鍵盤啟用,既是可發現 的替代方式,也是在拖曳失敗時的簡單恢復路徑。
兩項無障礙要求的差別會改變設計。鍵盤操作要求完整排序無需指標裝置。另一項要求是,當拖曳 屬於非本質操作時,必須有無需按住並移動指標的單指標方法。方向鍵拖曳模式可以滿足鍵盤使用者, 卻不能自動滿足後一項,除非同樣的控制項還能點按。可見移動按鈕可以同時滿足二者。
可選的精簡鍵盤拖曳模式能減少重複按 Tab。焦點位於把手時,按 Enter 或 Space 開始,按 Arrow Up 和 Arrow Down 改變候選位置,再按 Enter 或 Space 提交,Escape 則取消並恢復快照。 把按鍵說明放在文字中,並由把手引用,不能要求使用者猜測。若項目還支援選取,應把選取與排序 命令分開,避免 Space 在同一焦點目標上有兩種含義。
把互動表示為短期工作階段。開始時保存 itemId、輸入類型、原始順序和目前候選索引。指標或 鍵盤移動只透過 moveItem 更新候選。提交只產生一次持久化請求和一次播報;取消恢復原順序並 播報取消。較早的伺服器回應不能只因傳回較晚,就覆蓋較新的排序工作階段。
指標輸入應使用明確把手,避免把整列設為可拖動,以免搶占連結、核取方塊、文字選取或捲動。 若用 Pointer Events 實作頁內排序,應設定小幅移動門檻、擷取指標、依項目中點計算候選位置、 顯示不只依賴色彩的插入指示,並處理 pointerup、pointercancel、擷取遺失、捲動和清單外放置。 如果需求同時包含觸控感測器、自動捲動、碰撞偵測、巢狀捲動容器和輔助技術,優先選擇經過驗證 的拖曳函式庫。
原生 HTML 拖放適合桌面或跨應用程式資料傳輸,但不會自動提供鍵盤或螢幕閱讀器排序流程。它還 要求明確宣告有效放置目標,並會在拖動期間抑制其他裝置輸入事件。選擇原生方案後,移動控制項、 焦點、播報和觸控驗證仍然不能省略。
結果要同時透過視覺與語意表達。候選插入點應使用形狀或邊框,而不只依賴色彩;保留清晰焦點 指示,並尊重減少動態效果偏好。使用持續存在的禮貌狀態區域,在有意的鍵盤移動、提交、取消和 失敗後輸出短訊息。「已將審查移到第 2 位,共 5 項」包含項目、結果、位置和總數。不要播報每次 指標停留,也不要在寫入訊息的同一時刻才建立狀態區域。
不要使用過時的拖放語意。WAI-ARIA 1.2 已把 aria-grabbed 和 aria-dropeffect 標記為棄用。 原生控制項、無障礙名稱、可讀操作說明、文字狀態、焦點和經過測試的播報更可靠。ARIA 無法補上 缺少的鍵盤行為,也無法補上不存在的點按替代。
遠端儲存時,可以傳送穩定 ID 順序,也可以傳送移動命令以及用戶端開始編輯時的版本。介面可以 樂觀預覽,在不阻斷鍵盤焦點的前提下顯示儲存狀態,並依選定產品規則決定何時確認播報。網路失敗 時,要麼保留本地待儲存順序並提供重試,要麼回復快照並播報回復。版本衝突時讀取目前順序,讓 使用者重試或執行明確定義的合併;靜默覆蓋其他用戶端順序不算恢復策略。
驗證從純命令開始。涵蓋首項、末項、相鄰位置、原位置、未知 ID 和越界索引。產生輸入時,斷言 輸出包含相同 ID、相同數量,並且只發生請求的相對移動。隨後執行互動矩陣:
- 滑鼠拖曳、觸控拖曳、僅點按控制項,以及放到清單外;
- 純鍵盤移動、提交、邊界操作和 Escape 取消;
- 每次呈現和回復後,焦點仍在被移動項目的把手;
- Safari 搭配 VoiceOver,以及 Firefox 或 Chrome 搭配 NVDA,分別只播報一次項目、說明、
新位置、取消與儲存失敗;
- 200% 縮放、強制色彩、高對比度、減少動態效果、長標籤、捲動和 RTL 版面;
- 延遲成功、請求亂序、離線失敗、重試和版本衝突。
自動無障礙工具可以發現名稱缺少、屬性無效和部分可聚焦問題,但無法判斷鍵盤模型是否易於發現、 語音順序是否連貫,以及觸控螢幕閱讀器能否完成任務。這些必須人工操作。
高品質示範回答
「我會讓項目身分獨立於陣列位置。reducer 接收項目 ID 和最終索引,傳回一個新排列,而且只有 這段程式碼能修改順序。指標拖曳、上移或下移按鈕,以及可選鍵盤拖曳模式都派發同一命令。
語意基礎是有序清單與原生按鈕。每列有明確排序把手和可見移動控制項,無障礙名稱包含項目標籤。 移動控制項很重要,因為鍵盤支援和無需拖動的點按替代是兩項不同要求。我不會依賴 aria-grabbed 或 aria-dropeffect,它們都已棄用。
排序開始時保存原順序和活動項目 ID。指標命中測試或方向鍵只更新候選位置。提交時傳送一次儲存, 並播報「已將審查移到第 2 位,共 5 項」。Escape、pointercancel、無效放置,或產品選擇的儲存 失敗規則都會恢復快照。穩定 React key 能在 DOM 順序改變後保持同一把手焦點。
指標排序使用專用把手、移動門檻、指標擷取、明確插入指示、自動捲動和取消清理。如果需求包含 觸控、巢狀捲動和跨瀏覽器碰撞處理,我會在核驗拖曳函式庫的鍵盤與螢幕閱讀器行為後再選函式庫; 只有滑鼠展示不夠。
我會先單元測試排列不變量,再用滑鼠、觸控、鍵盤、僅點按控制項、VoiceOver 和 NVDA 人工完成 任務,並涵蓋焦點、Escape、清單外放置、減少動態效果、強制色彩、延遲儲存、舊回應和版本衝突。 這樣覆蓋完整互動契約;自動掃描不能單獨構成證明。」
常見錯誤
- 用陣列索引作為 key → 排序會改變身分、焦點和項目內部狀態 → **所有 key 與命令都使用
穩定項目 ID。**
- 把拖曳設為唯一互動 → 無法持續按住並移動指標的使用者不能排序 → **提供結果等價的可見
點按控制項。**
- 加入方向鍵就聲稱全部合格 → 鍵盤等價不一定提供無需拖動的單指標方法 → **分別檢查鍵盤和
拖曳動作要求。**
- 把拖動監聽掛在整列 → 連結、選取、核取方塊和捲動會與排序衝突 → 使用明確且可聚焦的把手。
- 為每種輸入寫一套重排演算法 → 滑鼠、觸控和鍵盤擁有不同邊界錯誤 → **全部輸入呼叫同一個
穩定 ID 命令。**
- 直接修改陣列或 DOM → 呈現狀態與儲存狀態分岔 → 由狀態所有者傳回新排列。
- 把
aria-grabbed和aria-dropeffect當作無障礙方案 → 屬性已棄用,也不會增加互動 →
使用原生控制項、說明、狀態、焦點和經過測試的播報。
- 播報每次指標移動 → 狀態區域變得吵雜且延遲 → **播報有意鍵盤步驟和最終結果,不播報指標
像素。**
- 排序後把焦點移到清單開頭 → 使用者遺失目前位置 → 依穩定項目 ID 保持或恢復焦點。
- 儲存每個停留位置 → 網路競態會套用過期中間順序 → **只用版本前置條件持久化一次已提交
移動。**
- 只依賴自動掃描 → 自動工具無法驗證語音說明和完整輸入流程 → **用真實鍵盤、觸控和螢幕
閱讀器測試。**
追問及應對
追問 1:清單虛擬化後如何調整?
不能把已掛載 DOM 列當作完整集合。移動、總數和候選位置保存在資料模型中。即使目標在畫面外, 鍵盤命令也能依邏輯索引移動,再把被移動項捲入視野而不遺失其焦點目標。指標移動需要理解模型 的命中測試與自動捲動。如果虛擬化函式庫不能提供準確集合語意或穩定焦點,在實測規模允許時, 關閉虛擬化的無障礙模式可能更穩妥。
追問 2:如何一次移動多個已選項目?
保存穩定 ID 的有序集合,保持它們的相對順序,把它們作為一組移除,再插入一個最終邊界。播報 需要包含項目數量和目標位置。選取與拖曳把手必須使用不同命令,尤其是 Space 已經用於切換選取 時。若選取中包含鎖定項,應整體拒絕或明確哪些項保留;靜默只移動一部分會產生歧義。
追問 3:兩個用戶端同時排序怎麼辦?
移動請求攜帶清單版本或 ETag,伺服器只接受版本符合的移動。衝突時讀取目前順序,保留使用者想 移動的項目與目標作為脈絡,並提供重試或依既定規則合併。只有產品明確接受遺失另一個使用者的 排序決定時,才可使用最後寫入獲勝。
追問 4:原生 HTML 拖放、Pointer Events 和拖曳函式庫如何選擇?
原生拖放適合桌面和外部資料傳輸,但不會提供完整鍵盤與輔助技術流程。Pointer Events 對頁內 滑鼠和觸控排序的控制更強,但要自行實作碰撞偵測、擷取、自動捲動和清理。需求已包含這些行為時, 選擇經過測試的函式庫更合適,但必須讓它的鍵盤、觸控螢幕閱讀器、焦點和 DOM 語意通過產品驗證 矩陣。無論選哪種感測器,移動控制項與統一重排命令都保留。
追問 5:儲存失敗時使用者已經繼續操作怎麼辦?
每次已提交排序都攜帶用戶端序號和它依據的伺服器版本。較晚傳回的失敗只能回復由該請求產生的 狀態,不能用舊快照覆蓋更新的已確認順序。最簡單的安全策略是序列化儲存,只保留一個明確待儲存 順序,阻止第二次提交但不阻止焦點和閱讀。更靈活的佇列需要先實作變基與衝突測試,才能證明值得。