天天看點

為知乎增加快捷鍵

作者:閃念基因
為知乎增加快捷鍵

最近,知乎在桌面端網頁上陸續增加了一些快捷鍵:

  • 在首頁、個人頁和搜尋結果頁等頁面浏覽時[1]:按 J、K 來選中清單中的條目,按 O 來展開或收起全文,按 V、D 來贊同與反對等。另外,按 Esc 也可以退出大部分彈窗與彈出式菜單。
  • 在播放視訊時:按 F 讓播放器全屏,按 M 讓視訊靜音,按數字 0-9 可以跳轉到對應的視訊進度(0%-90%)等。
  • 在回答與文章的編輯器中也有一些快捷鍵:比如按 ⌘+K 可以插傳入連結接等。把光标移動到工具欄中的按鈕上可以檢視介紹。
為知乎增加快捷鍵

在非輸入狀态下按 ?(Shift+/)就可以檢視快捷鍵清單

在開發快捷鍵的過程中,有一些值得分享的經驗。

開發快捷鍵

快捷鍵的作用域

實作快捷鍵不困難,因為監聽鍵盤事件很簡單。大部分時候,我們隻需要在希望響應事件的最外層元素上注冊事件就行,而響應事件的回調(比如關閉模态框)也都可以在這個最外層元件内實作。

為知乎增加快捷鍵

知乎首頁的元件結構

但是這次的情況要更加複雜。以在首頁按 V 贊同答案為例:清單内的時間線條目(FeedItem)包括了回答(AnswerItem)在内各種類型的内容,而在回答裡的贊同按鈕(VoteButton)才是适合響應快捷鍵事件的地方。在最外側的 FeedItem 上容易注冊事件卻無法響應事件,在最内側的 VoteButton 中可以響應事件卻不能簡單地注冊事件。

如何在 React 的元件關系中優雅地實作子元件(贊同、收藏按鈕等)注冊快捷鍵和回調函數、同時直到父元件(整個回答甚至 Feed 條目)的範圍内都可以響應快捷鍵是比較有趣的。

一種簡單的想法是:放棄在子元件中注冊快捷鍵,全部在父元件中注冊快捷鍵,在回調的時候調用子元件的執行個體方法。這會造成以下這些問題:

  • 幾乎放棄了元件的層級抽象:從外向裡調用子元件執行個體方法比較 anti-pattern、難以管理(更别提在 React 中,越過接入 connect 等函數的 HOC 來擷取真正的元件執行個體了)。
  • 快捷鍵會被重複定義和實作:比如在想法、文章和答案等所有引用贊同按鈕的父元件中,都注冊和實作同樣的按 V 贊同的快捷鍵。
  • 在知乎的元件結構中,這種實作最多隻能在位于層級中部的 AnswerItem 範圍内進行,因為 FeedItem 的實作不關心子元件内部的業務。

現在知乎使用的方案是:基于 React context 來為所有子元件共享一個「快捷鍵執行個體」,在這個「作用域」下的所有子元件聲明快捷鍵時,都會注冊到這個執行個體上面。

參考下方的僞代碼:

function FeedItem() {
  const feedItemElement = useRef(null)
  return (
    <ShortcutContext value={feedItemElement}>
      <div className="FeedItem" ref={feedItemElement}>
        <AnswerItem />
      </div>
    </ShortcutContext>
  )
}           

在 FeedItem 中,以自身的 HTML element 來初始化一個新的快捷鍵執行個體并設定在 context 中,實際的 keydown 等鍵盤事件是注冊在這個 element 上。是以子元件都可以通過 context 在這個執行個體上注冊新的快捷鍵。

function VoteButton() {
  const handleVote = () => console.log('voting')
  // 在 useShortcut 中查找目前 context 中存在的快捷鍵執行個體,并注冊在執行個體上
  useShortcut('v', handleVote)
  return <button onClick={handleVote}>贊同</button>
}           

在 VoteButton 中,就可以像響應點選事件一樣注冊并響應快捷鍵事件,看起來和寫起來都非常清爽。

清單導航的快捷鍵

使用 J、K 進行 Vim 風格的清單導航是許多網站常見的快捷鍵設計[2]。下面是一些如果從零開始實作時,可能會遇到的細節問題:

  • 直接使用 element.focus() 方法會由浏覽器決定一個合适的滾動位置,這通常是不符合預期的(對于知乎來說,是因為頂部有一個固定的導航欄,原生滾動經常會被這個導航欄擋住)。我們使用 preventScroll: true 參數禁用了這個原生的滾動,并使用自己計算的結果來 window.scrollTo() 到指定位置。
  • 不是所有元素都是可以被 focus 的。如果隻是普通的 <div> 元素的話,可以設定 tabIndex 為 0 或 -1 使其可被 focus。如果設定為 -1 的話則隻可以被 focus,但是不能被 Tab 鍵選中[3]。是否希望被 Tab 選中,主要在于該元素内是否有可以被閱讀的元素,或者該元素本身有可互動的行為。
  • 如果在 focus 到某個清單元素後,又用滾輪或者觸摸闆移動了網頁到很遠的距離,再按 J、K 進行導航時就需要放棄已經 focus 的元素的,盡可能地 focus 在視圖區域内可見的元素上[4],或者采用别的政策[5]。
  • 和其他類似産品不同,知乎大部分清單元素不是整個區域都可以點選并進入詳情頁,而是鼓勵在目前頁面展開并消費内容的(否則可以直接使用一個 <a> 标簽包裹。這次特意實作了 focus 在清單項時,按 Enter 鍵可以進入詳情頁的快捷鍵。
  • 如果因為點選而 focus 在某個清單項時(比如第 2 項),按 J、K 導航時最好隻是先在該元素上顯示 focus outline,而不是直接進行快捷鍵導航(到第 1 項或者第 3 項)。因為此時使用者不一定知道自己已經 focus 在這個清單項上。這和知乎不為點選行為導緻的 focus 添加 outline 有關。

Focus Outline 該在什麼時候展現

當使用快捷鍵進行浏覽時,「知道光标在哪裡」很重要。螢幕閱讀器可以閱讀目前 focus 元素的内容。如果不通過聲音的話、就隻能通過視覺樣式來知道目前正在 focus 的元素是什麼。

為知乎增加快捷鍵

Chrome 為 Input 标簽預設添加的 focus outline

浏覽器預設會為可 focus 的元素通過 :focus 僞類增加一個 outline 樣式,因為這個樣式不是很好看,再加上可 focus 的元素往往也會單獨設計一些響應點選的樣式,是以一般産品或設計會要求工程師取消掉該樣式。但是如果簡單取消所有 focus 樣式後,用滑鼠當然知道「我在點哪裡」,但在使用鍵盤通路的場合就根本不知道「我在哪裡」了。

最優雅的方案是使用 CSSWG 的 :focus-visible 僞類來添加 focus 樣式(同時禁用原先的 :focus 樣式),在 WICG 對該樣式的 polyfill 中有詳細的介紹。簡單來說,就是「隻有用鍵盤觸發的 focus 才應該添加 focus 樣式」。

為知乎增加快捷鍵

知乎按鈕的 focus 樣式

知乎通過和該 polyfill 類似的思想實作了這一設計:在使用鍵盤操作後,會為 <html> 元素添加 data-focus-visible 屬性。隻有在包含該屬性的情況下,各個元素才會添加 focus 樣式。而且知乎還修改了預設的樣式、更為美觀。你可以使用 Tab 鍵 tab 到贊同按鈕上檢視這個效果。

在開發這一部分功能的時候,還有一個特殊的設計:使用 ⌘+C 等快捷鍵進行複制粘貼操作(準确地說,是按鍵中包括 Control 或 Shift 等 Modifier Key)時,不認為是一般的鍵盤操作,也不展現 focus outline,否則 outline 會過于頻繁地時有時無。有意思的是,Twitter 其實也已經做了類似的處理,讓人感到大洋兩岸的工程師都在為使用者體驗而努力…

快捷鍵和可通路性的關系

很多關于快捷鍵的讨論都會有視障使用者的參與,因為使用螢幕閱讀器浏覽網頁、必須使用包括 Tab 鍵在内的各種鍵盤快捷鍵進行光标定位與操作,他們是「最會用鍵盤刷知乎」的人。但是,對視障使用者的支援遠不止添加快捷鍵這麼簡單。

@devil纏

在一個答案的評論區[6]提到:

如果隻是單個按鍵,(快捷鍵)基本上沒有任何用處。以 V 為例:當用 Tab 到達「贊同」按鈕時,直接(按)空格就可以點贊同。(另外)如果在檢視内容區點選 V,焦點不會跑到「贊同」按鈕上。

合理的快捷鍵有可能用處不大,而不合理的快捷鍵不但不能幫助視障使用者,還會幫倒忙。

@殷曉波

@devil纏

都提到「使用 V 贊同之後,希望可以 focus 到贊同按鈕」,如果不真正使用螢幕閱讀器浏覽網頁,是無法想象這句話的原因的:

為知乎增加快捷鍵

快捷鍵贊同後,需要轉移焦點

簡單來說,螢幕閱讀器隻有在焦點改變時才會閱讀焦點内的文字,它監控不到「贊同按鈕變深藍」這樣普通人可以輕松了解的設計回報。如果使用快捷鍵贊同後不改變焦點,連按下鍵盤發生了什麼都不知道,也就不會知道「已經贊同了答案」。此時 tab 到贊同按鈕上閱讀到「已贊同」的文字才知道發生了什麼就很奇怪。

浏覽器的點選行為會自動 focus 在可互動的元素上(例如 <button> 或 <a>),而此時按 Enter 或 Space 等快捷鍵可以「模拟一次點選」,這套現成的體系很容易被忽略。在實際體驗中,tab 到「閱讀全文」按鈕再按 Space 來展開全文并不比用快捷鍵 O 來展開全文麻煩很多。

除此之外,很多讀屏軟體或者視障使用者也會定義、開發個性化的快捷鍵[6]。這麼來看,使用 <button> 等語義化标簽使元素可互動元素也可以被 focus、盡可能使用 <a> 而不是在監聽 onClick 事件時使用 location.href 進行頁面跳轉、配置好 aria 屬性等…對 Accessibility 更有意義。

總的來說,實作快捷鍵和實作其他功能一樣也要注意視障使用者的使用、互動體驗,比如:

  • 任何快捷鍵操作後都要像點選一樣轉移焦點。
  • 轉移焦點後,還需要配置 aria-label 等屬性來閱讀出有意義的提示文字,比如贊同還是已贊同、反對還是已反對。

快捷鍵隻是 Accessibility 的一部分,而可通路性又是一個更加系統和複雜的工程。知乎做了一些努力[7],但還遠遠不夠。也歡迎對這個領域有更多了解的朋友提出建議。

Q & A

可以關閉快捷鍵嗎?

為知乎增加快捷鍵

是的。如果你使用 Vimperator 或者 Vimium 等浏覽器擴充定制了快捷鍵而不想和知乎的沖突,可以在桌面端網頁的個人偏好設定(https://www.zhihu.com/settings/preference)中關閉快捷鍵[8]。

這篇文章的注釋與參考是如何做到的?

這是編輯器的新功能,會在開放後再行介紹。

參考

^這些快捷鍵在遷移到新版 Web 頁面時沒有同步遷移,在很長一段時間内都沒有實作。

^包括 Twitter、Facebook、Gmail 與新浪微網誌等,知乎從這些網站的實作細節中受益良多。

^一個叫 tabbable 的庫中有關于這兩者差別的介紹,這個庫在實作 focus trap 等效果時很有用。 https://github.com/davidtheclark/tabbable

^如何高效地查找離視窗最近、滾動距離最小的元素,這個算法比較有意思,這裡不贅述了。

^知乎和 Facebook 與新浪微網誌一樣,會選中視野内可見的新元素,而 Twitter 會放棄滾動。

^ab根據 @devil纏 在這個答案評論區中的說法,他使用的讀屏環境還會定義包括 K 下跳 10 個連結、Shift+K 上跳 10 個連結等快捷鍵 https://www.zhihu.com/question/19842222/answer/17152043

^@長天之雲 的答案介紹了一些知乎對 a11y 的支援 https://www.zhihu.com/question/20487917/answer/15265930

^隻在目前使用的浏覽器中生效。

作者:孫北吉

出處:https://zhuanlan.zhihu.com/p/59928288