文章大綱與涵蓋範圍
繼上篇介紹完無障礙網站(Web Accessibility,又稱為 a11y)的目的與實踐方向,中、下篇將著重在 React Advanced Guide 裡提供的 Accessibility 指南。
在可以廣泛運用的 Accessibility 實作知識上,hightlight React 對此的支援(例如 JSX 是否也能直接應用某些 HTML 屬性?)或者調整(某些屬性在 React 裡,可能有寫法的差異)。
實作 Accessibility 的重點提示
accessible name 被寫在 HTML 元素上,用途在為輔助科技提供更多識別元素的資訊,舉個應用場境的例子:使用語音識別工具的人,可以用 accessible name 來鎖定想要選去的元素;使用螢幕閱讀工具的人,accessible name 讓他更理解該元素的意涵。
使用語音識別工具的人,可能會說 “Click Doggy Day Care link“ 來觸發 <a>
元素。
<a href="doggy.html">Doggy Day Care</a>
從上面的案例中可以看到,並非每個元素都需要特別添加更多資訊,以下舉出三項 accessible name 的實作方式 :
A. 使用屬性
圖片的 accessible name 從 alt 屬性衍生而來。
<img src="doggy.png" alt="cute doggy">
如果是 <a>
元素包著 <img>
元素,accessible name 可能會有不同的組合方式。
// accessible name 是 img 的 alt "cute doggy"
<a href="doggy.html"><img src="doggy.png" alt="cute doggy"></a>
// accessible name 是 img 的 alt 加上文字 "cute doggy 待認養"
<a href="doggy.html"><img src="doggy.png" alt="cute doggy"> 待認養</a>
B. 使用關聯元素
表單的 accessible name 則是從關聯元素衍生而來。
<input type="checkbox" id="doggy">
<label for="doggy">狗勾認養意願調查</label>
可以看見 <input>
的 id 屬性,與 <label>
的 for 屬性內容是一樣的,這會元素之間製造出關聯,讓瀏覽器把 <label>
的文字內容,當作整個 checkbox 的 accessible name。
C. 使用 ARIA 屬性
ARIA 屬性之一寫做 aria-label
ARIA 屬性是另一個替代選項,會凌駕於原生 HTML 提供的 accessible name 之上,但可能會因此造成一些問題。
假設我們有以下元素,使用語音辨識工具的使用者,會無法看見該按鈕的 accessible name(因為 accessible name 不再是 <button>
元素中的內容,而是被改成了 ARIA 屬性的內容,ARIA 會凌駕於原生設定之上)進而無法正確的使用語音指令,選取到目標元素。
// 按鈕的 accessible name 會是 "Add pizza to cart" 而非 "Add to cart"
<button aria-label="Add pizza to cart">Add to cart</button>
另一個屬性寫做 aria-labelledby
aria-labelledby 讓我們把另一個元素的 id 指定成為當前元素的標籤,在兩個元素之間創造關係,輔助科技可以利用這個屬性讓使用者在元素之間切換。當你想要當作 accessible name 的文字位於頁面的其他位置時,可以參考這種做法。
<input type="search" aria-labelledby="this">
<button id="this">Search</button>
附上:WAI-ARIA 的解決方案完整文件
https://www.w3.org/TR/wai-aria/#states_and_properties
注意
JSX 完全支援 ARIA 屬性,只是 React 中大部分的 DOM 屬性都寫成 camelCased,而 ARIA 應該要保持 hyphen-cased 的寫法。
<input
type="text"
aria-label={labelText}
aria-required="true"
onChange={onchangeHandler}
value={inputValue}
name="name"
/>
另外,上面提到的表單元素作法,在 JSX 中也支援,但是 for 屬性在 JSX 中要寫成 htmlFor。
<input id="namedInput" type="text" name="name"/>
<label htmlFor="namedInput">Name:</label>
Keyboard Accessibility 是無障礙網頁中最重要的一個面向。行動不便、無法穩定控制肌肉的使用者,會依賴鍵盤來使用網頁。以下是幾個實作重點:
Focus 的提示
鍵盤使用者使用 tab 按鍵在各個可互動的元素(例如:連結、按鈕、輸入框)之間切換,當一個元素被使用 tab 選到的時候,鍵盤會 "focus" 在這個元素上,並且可以操縱或觸發這個元素
使用者需要視覺的提示來明白現在 focus 在哪個元素上,而瀏覽器會自動提供這個提示(通常是加上邊框或者底色),而這些提示可以使用 CSS 元素隱藏。
因此,你應該要避免 outline:0 或 outline:none 的設定。此外,也可以額外添加 CSS 樣式讓 focus 的提示更加顯眼,例如加上背景顏色。
元素互動的順序
鍵盤使用者在不同元素中切換的時候,預設順序會是遵循最自然的視覺慣性(由左到右、由上到下),在大多數的網頁裡,代表著元素切換的順序會是 header、網頁主要內容,最後是 footer。
這個順序是參照頁面的 source code,所以你應該確保網頁架構不會讓順序亂掉,並且,留意使用 tabindex
的值不應大於 0,以避免去改變預設順序。
tabindex 是什麼?用來代表該元素是否可以被 focused 以及在鍵盤切換中的位置:
// 代表這個元素不能被鍵盤切換到,通常用在當你有一個螢幕之外且不希望被選擇到的內容
tabindex="-1"
// 代表這個元素是可被 focus 的,但順序會在 tableindex = 1 or 2 or 3... 之後
// 另一個用法是,當你使用了自製的元素,沒有預設順序的狀態下,可以加進此屬性來確保元素可被 focus
tabindex="0"
// 大於零的狀況,就代表元素順序 e.g. tabindex="4" 的順序在 tabindex="5" 之前並且在 tabindex="3" 之後
tabindex={positive value}
導覽列與內容結構分明
鍵盤使用者則必須按下 Tab 或者其他按鍵來切換想要選取的物件,如果你的導覽列內容太多、連結太多,會讓使用者更加吃力。針對這個情況,有三種可以調整的方向:
你可以提供 "跳到主要內容" 的連結,讓使用者可以略過長長的導覽列
使用 Semantic Structure 設計的 heading
使用 heading tag <h1>
- <h6>
,並且使用唯一的 <h1>
來描述整體網頁,並漸次使用 <h2>
- <h6>
來描述接下來的標題等內容,中間不應該跳過層級(例如 <h2>
的下一級直接跳到 <h4>
是不建議的做法),而 heading 作用以外的文字,就不要使用 heading tag 了。
使用 Semantic Structure 設計的 region
用來 <header>
<nav>
<main>
<footer>
來區分出頁面各區塊,輔助科技就可以輕易的在這些頁面區塊之間切換。幾個區塊分配要點:一個頁面有一個 <main>
;<header>
、<main>
、<footer>
元素應該要是 <body>
的直接子元素(中間沒有包其他任何東西);而 <nav>
元素應該只能被用在主要的導覽列上面。
注意
React APP 的運作方式,在 runtime 的時候會不斷地去修改 HTML DOM,有時候會造成鍵盤的 focus 功能失效,或者 focus 到一個未預期的元素上方,要修補這個漏洞,需要做一點手腳,把鍵盤的 focus 指引到正確的方向。我們需要使用 Refs 來重新設定 React App 的 focus 運作:
首先,使用 React.createRef() 創造一個 Ref,並且透過目標元素(這裡是 <input>
)上的 ref 屬性,讓 Ref 附屬在元素中。我們創造出來的 Ref 通常會在元件被創建時,指派給一個 instance property,讓這個 Ref 在這整個元件中都可以被 referenced。
我們建造出來的 Ref 會接收到底下的 DOM 元素,作為他的 current property。
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
return (
<input
type="text"
ref={this.textInput
/>
);
}
}
接下來,我們就可以在元件的某個地方,使用 focus() 來 focus 我們 reference 的那個元素。
focus() {
this.textInput.current.focus();
}
以上的案例是在同一個元件內的情況,有時候,是父元件會需要去 set focus 子元件上的元素,要達到這個目標,我們可以在子元件上設定特別的 prop,讓父元件可以接觸到子元件上的 DOM refs。
function CustomTextInput(props) {
return (
// 子元件綁定 ref 屬性
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.inputElement = React.createRef();
}
render() {
return (
// 建立出來的 Ref 被往下傳到子元件中
<CustomTextInput inputRef={this.inputElement} />
);
}
}
this.inputElement.current.focus();
如果你想更了解控制 focus 的技術,React 文件提供了一個很好的案例:
https://github.com/davidtheclark/react-aria-modal
參考資料
https://www.tpgi.com/what-is-an-accessible-name/
https://webaim.org/techniques/semanticstructure
https://reactjs.org/docs/accessibility.html