iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 15
1
Modern Web

實踐無障礙網頁設計(Web Accessibility)系列 第 15

實作無障礙網頁功能:真・button —— 按鈕的世界比你想像複雜,別再用 div 刻假按鈕了!

這系列無障礙的鐵人賽文章,實踐的內容主要是根據 W3C:WAI-ARIA 的實踐,從設計模式及組件(Design Patterns and Widgets)裡面挑選最想嘗試的,如果有朋友想瞭解全部 Widget 該怎麼實作及其規範,歡迎自行爬規範內容,也許我們可以討論一下;若以下文章內容理解有任何錯誤,請多指教~

(圖片來源:unDraw

按鈕 Button(打開文件

3.5 Button
A button is a widget that enables users to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation. A common convention for informing users that a button launches a dialog is to append "…" (ellipsis) to the button label, e.g., "Save as…"

按鈕,在一個網站當中,可以說無所不在。它讓使用者可以藉由點擊送出表單、打開對話視窗、取消行為與刪除某操作。最常用的做法是點擊按鈕之後,開啟一個視窗後去做一些事,比如說:存檔、列印...等。

使用 <div><button> 製作按鈕的差異

外觀透過 CSS 都能做到一樣的樣式設定,原生的按鈕元素已具備許多方便鍵盤及螢幕閱讀器操作的特性,以下來介紹有哪些重要觀念:

focusable:鍵盤使用者可使用 tab 選到?

  • <div> :需要加上 tablindex="0" ,告訴鍵盤它是可以被操作的。
  • <button>:原生的按鈕元素,在使用者用鍵盤底選 tab 時就能聚焦。

語義不同,role 也不同

div 即使加上 tabindex,變成可聚焦之元素,div 在語義上只是代表一個群組的集合,而 button 卻是代表一個可點按的按鍵,若我們同樣製作一個「註冊」按鈕,使用 VoiceOver 讀出:

  • <div> :念出「註冊 group」,只表達群組,無法推敲更深層的意義。
  • <button>:念出「註冊 button」,不會造成混淆。

使用鍵盤無法觸發

假設我們使用 JS 在兩個元素加上監聽,當點擊事件發生時,就 alert 一個視窗,告訴你他被點擊了。

anyButton.addEventListener('click', function () {
	// doSomething(); => 原生按鈕可以被鍵盤的 Enter 正常觸發
})

anyDiv.addEventListener('click', function () {
	// doSomething(); => 假裝的按鈕需要監聽 Keycode
})
  • <div> :使用滑鼠點擊,正常觸發。透過鍵盤 tab 選擇,再按下 enter,無法反應,需要額外新增的監聽事件,去聽 keycode 是否按下 enter 鍵、空白鍵。
  • <button>:使用滑鼠點擊,正常觸發。透過鍵盤 tab 選擇,再按下 enter,正常觸發!

Disabled 的差異

同樣為 div 與 button 加上 disabled 屬性,以及樣式:

<div class="fancy" disabled></div>
<button class="fancy" disabled></button>

.fancy[disabled] {
	background-color: gray;
}

那麼是否可被關注的狀態就該因為 disabled 而被取消,換句話說,這時我們使用 tab 去選網頁上的內容,disabled 屬性的元素是不該被選取到的,會從 focus order 移除,否則將造成使用者非常混亂。

  • <div> :依然可以 focus。
  • <button>:無法選取。

來介紹一下,如果我們要做按鈕組件的一些應用,在 WAI-ARIA 有哪些注意事項。

WAI-ARIA

除了傳統的按鈕組件,WAI-ARIA 還支援另外兩種按鈕類型:

  • Toggle Button
    • 它有兩種狀態:關閉(不按)、開啟(按)。
    • 若要通知 AT 輔助技術,按鈕已經切換狀態,使用 aria-pressed 屬性並指定 true/ false
    • 例如,在音樂播放器中標記為靜音的按鈕被按下的狀態時,設置 aria-pressed="true"來表示「靜音」。
      • 重要提示:
        關鍵是切換狀態時,按鈕上的文字或是標籤不會因此改變(註:標籤指的是當按鈕以圖示表示,會加上 aria-label 代表其標題意義)。在上例中,當按下的狀態為 true 時,標籤保持「靜音」 ,這樣螢幕閱讀器就會朗讀「靜音 toggle button pressed」。換句話說,如果設計要求按鈕標籤在按下後,從「靜音」更改為「未靜音」這種文字內容更新,則不再需要使用 aria-pressed 屬性,念出來會混淆視聽。
  • Menu Button
    • 這其實另外一個設計模式了。此時, Menu Button 會和另一個設計模式 Menu / Menu Bar 一起應用,與 aria-haspopup 屬性通力合作。

重要註記

「按鈕」的行為表現和「超連結」不一樣,它們的樣式與角色模型也許都能符合功能要的需求。但是有很多作法是讓有像超連結樣式的元素,行為表現得像按鈕一樣,這時在元素上加上 role="button" 可以幫助輔助科技瞭解組件是有什麼功能性。


鍵盤的可訪問性

  • Enter 或 Space
    • 觸發按鈕
  • 按鈕被觸發後的 Focus 焦點狀態,會因為不同情境而有不同行為表現:
    • 若觸發按鈕後,會打開對話視窗,焦點移動到「對話視窗」。
    • 若觸發按鈕後,會關閉對話視窗,焦點回到「打開視窗的按鈕」;不過當對話視窗的行為是要刪除原本按鈕的環境,焦點必須移動到新的環境。
      • 比如說:是一個確認視窗,「確認刪除此列?」。
    • 若觸發按鈕後,畫面內容沒有改變,焦點應「保持在自己身上」。
      • 比如說:有個按鈕叫做「授權」,主管點了之後開啟系統中某下屬的權限,系統畫面不會有任何改變。
    • 若觸發按鈕後,畫面內容改變,焦點移動到「下一步動作的起始點」。
      • 比如說:有個按鈕叫做「下一步」或是啟用「搜尋」功能,那麼焦點接下來就要移動到下一個步驟的起始點,或移動到搜尋框。
    • 若按鈕是被快捷鍵觸發的,焦點始終保存在快捷鍵觸發時的畫面中。
      • 如果當時畫面是在一個 list 中,本來焦點在某一列上,Alt + U 當作觸發按鈕的快捷鍵,可以移動到上一列的話,焦點就移動到上一列。
      • 如果焦點是在 list 本身,Alt + U 快速鍵,焦點無法超出清單的範圍,一樣保持在 list 上。

Roles、States、Properties

  • 按鈕的角色是 button,如果不是原生 <button>,應該要加上 role="button"
  • 為按鈕提供標籤(名稱),預設是以按鈕元素的文字為標籤,如果按鈕元素內容是圖片,可以使用 aria-label="名稱" 或是 aria-labelledby="某元素id"
  • 如果按鈕在頁面中的某元素有提供額外的功能敘述,也可以在按鈕上設 aria-describedby="某元素id"
  • 按鈕狀態
    • 目前是禁用的狀態, aria-disabled="true",提醒一下,只有寫 aria-disabled 也是 true 唷。
    • 可切換狀態的按鈕,需要設定 aria-pressed 屬性,值是 truefalse

實踐開始囉!

今天想引用 WAI-ARIA Practices 1.1 示範的例子來說明最簡單的按鈕概念,之後銜接 Menu Button 來做一些 RWD 常用的漢堡按鈕的話,也會比較清楚、輕鬆。

以下範例是使用兩個非原生具有 button 語義的標籤來實作按鈕,分別是<div><a>,要做的事情是「列印」與「靜音」,因為原範例 svg 路徑的關係,微調一下 JS 寫法。

希望能做到:

  • [x] 支援螢幕閱讀器
  • [x] 可使用鍵盤操作

HTML [ 打開 codepen 操作看看 ]

<section>
    <p>這是一個 div 按鈕,執行列印功能</p>
    <div tabindex="0"
         role="button"
         id="action">
      Print Page
    </div>
</section>
<section>
    <p>這是一個 a 按鈕,切換靜音與聲音功能</p>
    <a tabindex="0"
       role="button"
       id="toggle"
       aria-pressed="false">
      Mute
        <i class="fas fa-volume-up"></i>
    </a>
</section>
  • 因為元素都不是 <button>
    • 需要補充語義 role="button"
    • 需要加上可被鍵盤 tab 鍵 focus 的設定, tabindex="0"
  • 第二個範例使用 <a> ,功能需要切換兩種不同的狀態,所以加上 aria-pressed 屬性,值是 false 代表按下之前是有聲音的,按下之後需要關閉聲音,除了樣式要區隔以外,將 aria-pressed 設為 true
[role="button"] {
  display: inline-block;
  position: relative;
  padding: 0.4em 0.7em;
  border: 1px solid hsl(213, 71%, 49%);
  border-radius: 5px;
  box-shadow: 0 1px 2px hsl(216, 27%, 55%);
  color: #fff;
  text-shadow: 0 -1px 1px hsl(216, 27%, 25%);
  background-color: hsl(216, 82%, 51%);
  background-image: linear-gradient(to bottom, hsl(216, 82%, 53%), hsl(216, 82%, 47%));
}

[role="button"]:hover {
  border-color: hsl(213, 71%, 29%);
  background-color: hsl(216, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(216, 82%, 33%), hsl(216, 82%, 27%));
  cursor: default;
}

[role="button"]:focus {
  outline: none;
}

[role="button"]:focus::before {
  position: absolute;
  z-index: -1;

  /* button border width - outline width - offset */
  top: calc(-1px - 3px - 3px);
  right: calc(-1px - 3px - 3px);
  bottom: calc(-1px - 3px - 3px);
  left: calc(-1px - 3px - 3px);
  border: 3px solid hsl(213, 71%, 49%);

  /* button border radius + outline width + offset */
  border-radius: calc(5px + 3px + 3px);
  content: '';
}

[role="button"]:active {
  border-color: hsl(213, 71%, 49%);
  background-color: hsl(216, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(216, 82%, 53%), hsl(216, 82%, 47%));
  box-shadow: inset 0 3px 5px 1px hsl(216, 82%, 30%);
}

[role="button"][aria-pressed] {
    /* 這裡只是幫有需要切換狀態的範例加上新的顏色(紫色)做為區別 */
  border-color: hsl(261, 71%, 49%);
  box-shadow: 0 1px 2px hsl(261, 27%, 55%);
  text-shadow: 0 -1px 1px hsl(261, 27%, 25%);
  background-color: hsl(261, 82%, 51%);
  background-image: linear-gradient(to bottom, hsl(261, 82%, 53%), hsl(261, 82%, 47%));
}

[role="button"][aria-pressed]:hover {
    /* 這裡只是幫有需要切換狀態的範例加上新的顏色(紫色)做為區別 */
  border-color: hsl(261, 71%, 29%);
  background-color: hsl(261, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(261, 82%, 33%), hsl(261, 82%, 27%));
}

[role="button"][aria-pressed]:focus::before {
    /* 這裡只是幫有需要切換狀態的範例加上新的顏色(紫色)做為區別 */
  border-color: hsl(261, 71%, 49%);
}

/* 開始為狀態加上樣式 */
[role="button"][aria-pressed="true"] {
  padding-top: 0.5em;
  padding-bottom: 0.3em;
  border-color: hsl(261, 71%, 49%);
  background-color: hsl(261, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(261, 82%, 63%), hsl(261, 82%, 57%));
  box-shadow: inset 0 3px 5px 1px hsl(261, 82%, 30%);
}

[role="button"][aria-pressed="true"]:hover {
  border-color: hsl(261, 71%, 49%);
  background-color: hsl(261, 82%, 31%);
  background-image: linear-gradient(to bottom, hsl(261, 82%, 43%), hsl(261, 82%, 37%));
  box-shadow: inset 0 3px 5px 1px hsl(261, 82%, 20%);
}    

SCSS

  • 主要都是透過最淺白的設定讓大家讀懂,不同的狀態,需要各種相應的樣式來配合。
    • 尚未點擊的:預設樣式
    • 滑鼠摸到的: :hover
    • 滑鼠點擊中(mousedown): :active
    • 成為焦點的: :focus
    • 最後再將狀態是「按壓的按鈕」加上樣式: [role="button"][aria-pressed="true"]

最後,codepen 在這裏,快來使用鍵盤操作看看吧!結論是,除了 <button> 外,還是可以使用 <div> 或是 <a> 來製作一個按鈕,不過需要幫忙補充語義及 JS 要做的事情都比較多(監聽各種 keycode,注意事項請參考上文),大家就自行斟酌囉。


Reference


上一篇
實作無障礙網頁功能:麵包屑 Breadcrumb
下一篇
實作無障礙網頁功能:真・button —— RWD 漢堡選單應用
系列文
實踐無障礙網頁設計(Web Accessibility)30

尚未有邦友留言

立即登入留言