作者:vivo 互聯網前端團隊- Zhang Li、Dai Wenkuan
隨着信息無障礙的建設越來越受重視,開發人員在無障礙適配中也遇到了越來越多的挑戰。本文是筆者在vivo開發H5項目做無障礙適配的實踐總結。本文主要介紹了在前端項目中常用的無障礙手勢和無障礙屬性,並且結合具體的開發案例為開發者真實展示了適配要點,提供組件適配思路。希望本文能為前端開發者帶來更多的參考和幫助。
一、背景
1.1 無障礙適配認知
無障礙(Accessibility)是指為各種能力水平的人們提供公平和平等的機會和體驗,以便他們可以更容易地訪問、使用和參與社會中的各種產品、服務和環境。這些人包括身體殘疾、聽力、視覺、認知和學習障礙等各種能力水平的人。
1.2 無障礙適配原因
常見的障礙類型包括:視覺障礙、聽覺障礙、認知障礙、行動障礙。這些“有障礙”的羣體,在使用軟件時無法像普通人一樣操作,他們或許看不清,需要更大的字體,或許看不到,需要語音播報,又或許聽不清聽不到,需要依賴視覺反饋,或許肢體操作不方便,無法自由操縱手機。
所以,國家和企業需要站在“有障礙”的羣體思考,為各種能力水平的人們提供公平和平等的機會和體驗。
- 【政策】:2011年《中國殘疾人事業“十二五”規劃綱要》指出了, 建設無障礙環境的主要任務之 一就是加強信息無障礙建設,並明確了相關的政策措施。
- 【包容性】:無障礙適配是一個重要的設計和開發目標,可以幫助您的網站或應用程序更加包容和可訪問。
- 【經濟性】:無障礙適配不僅是一種道德義務,也是一種經濟利益,因為它可以為您的網站帶來更廣泛的受眾和更好的用户體驗,帶動業務增長。
1.3 無障礙適配引導
除了系統默認支持的大字體、內容播報、語音識別、字幕、語音控制等基礎無障礙適配,更多的頁面交互還是需要前端工程師們完成適配,所以開發者們需要開發出可識別的web頁面,讓障礙羣體可以正常訪問和使用。本文主要針對視障人羣進行無障礙適配。
二、無障礙操作
在無障礙開發前,前端工程師們需要了解下無障礙的操作手勢,站在障礙人羣的角度體驗操作,這樣才能開發出更好的交互體驗。也能避免因操作錯誤導致開發錯誤和重複開發。
2.1 讀屏幕軟件
首先我們需要了解下針對視力障礙人員使用的讀屏設備或軟件,常見的讀屏軟件,如下列表:
由於我們是在vivo手機內做移動端安卓手機無障礙適配,以下的方案、案例等介紹均是基於Android-TalkBack。
2.2 常用操作手勢
藉助 TalkBack 手勢,可以在 Android 設備上進行導航和執行常用操作,以下操作為安卓9.1版及更高版本,僅在vivo手機的操作。官方鏈接:Talkback 手勢。
三、常用屬性介紹
3.1 aria屬性
ARIA全稱“Accessible Rich Internet Applications(可訪問的富互聯網應用)”,是W3C的Web無障礙推進組織在2014年3月20日發佈的可訪問富互聯網應用實現指南。是一個為殘疾人士等提供無障礙訪問動態、可交互Web內容的技術規範。ARIA 用於提高使用 HTML、CSS、JavaScript、AJAX 和相關技術開發的動態內容和高級 UI 控件的輔助功能。ARIA 現在正式成為 HTML5 規範的一部分,還可以嵌入在常用的 JavaScript 庫中。
3.2 aria狀態
3.3 role屬性
role將元素標記為不同的屬性,常用屬性“button(按鈕),region(圖形)”,根據定義的不同屬性,播報不同的內容。
3.4 tabindex 屬性
tabindex 屬性的使用可以使得原本無法取得焦點的元素獲取焦點。目的是為了使用户可以用鍵盤訪問任何可以用鼠標訪問的元素。我們知道,使用 Tab 鍵可以按文檔順序 tab 到所有可以獲取焦點的元素。tabindex 可以設置為 -1, 0 或者是任何自然數。
當用户使用 Tab 鍵瀏覽頁面時,元素獲取焦點的順序是按照 HTML 代碼裏面元素出現的順序排列的,有時跟實際看到的頁面順序並不一致。
四、無障礙適配案例
4.1 項目適配案例
1.語言設置
在全局<html>元素中的 lang 屬性設置頁面的語言:
英文:
<html lang="en">
中文:
<html lang="zh-CN"></html>
國內的中文項目一般需要設置為中文,如果設置成了英文,有些數字會播報成英文。
2.層級屬性
如果項目還沒開始適配,則開啓無障礙播報,會發現標籤div和span默認作為焦點並播報,這樣的話焦點和播報內容就非常分散。
如下圖2,就會有3個焦點,這樣過於分散,一般適配會將ui的模塊作為一個大焦點整體播報,此時在外層div添加tabindex=1聚焦。
圖1:原圖
圖2:分散焦點
圖3:一個大焦點
備註:綠色框為焦點標記
(1)聚焦播報
因為div標籤會自動播報,所以就算焦點聚焦到外層,但是內層還是會自動播報。
<div class="content" tabindex="1">
<div class="amount">
<div class="num">100</div>
<div class="unit">元</div>
</div>
<div class="desc">話費充值</div>
</div>
播報內容:100元話費充值。
(2)自定義播報
但是會存在div裏寫的內容和需要播報的內容不一致,則可在外層tabindex="1"聚焦後,通過aria-label寫上自定義內容
<div class="content" tabindex="1" aria-label="獲得100元的話費充值券">
<div class="amount">
<div class="num">100</div>
<div class="unit">元</div>
</div>
<div class="desc">話費充值</div>
</div>
播報內容:獲得100元話費充值券
3.聚焦樣式
聚焦可通過tabindex實現,但是聚焦後樣式會有黃色的邊框,可通過outline: none去除,但是頁面中的焦點太多,可通過全局去除。
在公共css文件裏設置:
*[tabindex] {
outline: none;
}
4.圖片播報
(1)圖片的alt屬性設置
圖片通過img標籤實現,如果圖片不可聚焦,則設置**alt=""**即可。如果可聚焦並需要播報內容,建議通過aria-label設置。如果使用alt,一旦圖片加載不出來,就會把alt的內容顯示出來,而且alt內容沒有樣式,在H5頁面上會顯得很突兀。
<img src="close.png" aria-label="關閉" alt=""/>
<img src="dog.png" alt=""/>
(2)去除圖片默認屬性(圖形)播報
可在img標籤裏,將role屬性改為row
<img
src="title.png"
tabindex="1"
aria-label="超級會員送您1個紅包"
role="row"
/>
5.按鈕播報
通常ui上的按鈕在選中後,需要播報按鈕內容+按鈕+引導操作(如:點按兩次即可激活)
首先需要聚焦(tabindex="1")到節點,然後需要在按鈕上的節點上寫上role="button",加上這個屬性後,後面會自動播報“按鈕,點按兩次即可激活”
案例:div中的按鈕
<div tabindex="1" role="button" class="btn">立即使用</div>
播報內容:立即使用,按鈕,點按兩次即可激活
案例:img中的按鈕
<img
class="btn"
tabindex="1"
role="button"
aria-label="開"
src="open.png"
/>
播報內容:開,按鈕,點按兩次即可激活
6.數字播報
如:手機號:181**8805中的星號不能正確播報,而是播報成乘,數字播報成整數。
錯誤播報:一百八十一乘乘乘乘八八零五
需正確播報:幺八幺星號星號星號星號八八零五
可寫一個公共方法映射數字、*號和X號播報文案,在需要的轉換方法裏調用該方法形成手機號播報文案。
公共方法如下:
function $broadcastNumber(number) {
if (number === 0) return '零'
if (!number) return ''
const numberMap = ['零', '幺', '二', '三', '四', '五', '六', '七', '八', '九']
const specialMap = { '*': '星號', 'X': '叉號', 'x': '叉號' }
return number.toString().split('').map(item => numberMap[item] || specialMap[item] || item).join('')
}
7.自定義事件播報
目前帶有點擊事件的節點,聚焦後會默認播報點按兩次即可激活(系統規範),但是這個引導不夠明確,需要就具體交互場景制定播報內容,如:點按兩次即可選中,點按兩次即可打開紅包等。
方案:在click事件節點裏層包裹div,並將焦點tabindex寫在這一層的div上,再自定義播報的內容即可。
<div class="content" tabindex="1" @click="select">
<div class="amount">
<div class="num">100</div>
<div class="unit">元</div>
</div>
<div class="desc">話費充值</div>
</div>
播報:100元話費充值,點按兩次即可激活
<div class="content" @click="select">
<div class="container" tabindex="1" aria-label="獲得100元的話費充值券,點按兩次即可選中">
<div class="amount">
<div class="num">100</div>
<div class="unit">元</div>
</div>
<div class="desc">話費充值</div>
</div>
</div>
播報:獲得100元話費充值券,點按兩次即可選中
8.額外內容播報
如:第幾項,共幾項,點按兩次即可選中
方案:可用空div加aria-label實現
<div class="content" @click="select">
<div class="container" :class="{ 'z-active' : code === item.code }" tabindex="1">
<div v-if="code === item.code" aria-label="已選中"></div>
<div class="amount">
<div class="num">{{ item.num }}</div>
<div class="unit">元</div>
</div>
<div class="desc">{{ item.desc }}</div>
<div :aria-label="`第${index+1}項,共${list.length}項,${code === item.code? '' : '點按兩次即可選中'}`"></div>
</div>
</div>
9.整體播報
通常整體播報的內容較多,且播報順序非代碼書寫順序,這個時候就需要在外層焦點裏,控制播報的內容,主要可通過兩種方法實現,aria-label拼接參數和aria-labelledby設置id。
需要播報的內容:話費充值券,50元滿減券,滿199元可使用,立即使用。
(1)aria-label拼接參數
可通過在外層節點設置tabindex=1後,再添加aria-label屬性按照需要的順序添加參數
案例:
<div class="content" tabindex="1" :aria-label="`${title}${num}元${type}${rule}${btn}`">
<div class="left">
<div class="amount">
<span class="num">{{ num }}</span>
<span class="unit">元</span>
</div>
<div class="type">{{ type }}</div>
</div>
<div class="right">
<div class="desc">
<div class="title">{{ title }}</div>
<div class="rule">{{ rule }}</div>
</div>
<div role="button" class="btn">{{ btn }}</div>
</div>
</div>
(2)aria-labelledby設置id
在外層節點設置tabindex=1後,在需要播報的內容節點裏添加id值,並將id值按照需要的順序寫在外層節點aria-labelledby屬性裏
案例:
<div class="content" tabindex="1" aria-labelledby="title amount type rule btn">
<div class="left">
<div id="amount" class="amount">
<span class="num">{{ num }}</span>
<span class="unit">元</span>
</div>
<div id="type" class="type">{{ type }}</div>
</div>
<div class="right">
<div class="desc">
<div id="title" class="title">{{ title }}</div>
<div id="rule" class="rule">{{ rule }}</div>
</div>
<div id="btn" role="button" class="btn">{{ btn }}</div>
</div>
</div>
10.局部特殊播報
如優惠券模塊,可整體選中播報全部優惠券內容,但內部立即使用按鈕又可聚焦播報跳轉。
方案:外層聚焦-設置tabindex=1,播報整塊內容設置aria-label拼接參數或aria-labelledby設置id,內層部分內容聚焦,聚焦內容設置tabindex=1,不聚焦的部分設置aria-hidden="true"(不然在選中外層焦點時候,內層非聚焦部分會重複播報)。
<div class="content" tabindex="1" aria-labelledby="title amount type rule btn">
<div class="left" aria-hidden="true">
<div id="amount" class="amount">
<span class="num">{{ num }}</span>
<span class="unit">元</span>
</div>
<div id="type" class="type">{{ type }}</div>
</div>
<div class="right">
<div class="desc" aria-hidden="true">
<div id="title" class="title">{{ title }}</div>
<div id="rule" class="rule">{{ rule }}</div>
</div>
<div id="btn" tabindex="1" role="button" class="btn">{{ btn }}</div>
</div>
</div>
11. 解決組件設置aria-labelledby="[id]”後只重複播報第一條數據的內容
因組件被Vue中的模板for循環調用,每個內容不一樣,用同一個id會導致播報同一個內容,且沒有key值的區分,需要解決設置aria-labelledby="[id]”後只重複播報第一條數據的內容
此時需要根據唯一標識區分id:可在id後拼接唯一標識號,如:"amount-${[item.id](http://item.id)}"
案例:
<div class="content" tabindex="1" :aria-labelledby="`amount-${item.id} desc-${item.id}`">
<div id="amount" class="amount">{{ item.amount }}</div>
<div id="desc" class="desc">{{ item.desc }}</div>
</div>
4.2 組件適配案例
除了具體業務的適配,還有一些共性的組件問題需要組件庫統一適配,這樣能減少各業務單獨適配的工作量。
任何一個無障礙組件的適配,都包含播報內容管理、焦點管理兩部分。對於播報內容管理,幾乎所有的組件適配都會涉及到。無障礙aria role、states一般系統都有自己默認的播報行為,儘量保持系統默認的播報。當然,也可以通過aria-label定製播報內容。焦點管理主要針對元素會發生變化的組件,如彈窗、輪播圖、各類選擇器等。
焦點管理的基本方法有3種:
- tabindex屬性;
- aria-hidden屬性;
- el.focus()方法。
tabindex="undefined"即意味着元素不可聚焦,為其他值意味着元素可聚焦。aria-hidden為true,意味着不可聚焦且不播報。對於可聚焦的元素,可以通過el.focus()方法直接聚焦在該元素上。
下面通過典型案例來説明各個組件是如何處理焦點和播報內容的。另外,由於一個html中可能多次使用同一個組件,所以下面的案例都是在不使用id屬性的基礎上完成適配的。如果一定要在某個組件使用id屬性,記得通過隨機函數對id屬性做隨機命名。
1. switch、checkbox、radio組件
這幾個組件相對簡單,使用系統默認的role即可。完成播報內容的適配即可,不需要做焦點管理。
以switch為例:首先是role=switch,然後通過aria-check播報開關狀態,最後通過aria-disabled來播報是否禁用。
<div
class="nx-switch"
tabindex="-1"
:aria-checked="!!value"
:aria-disabled="disabled"
@click="onClick"
role="switch"
>
<div :style="barStyle"></div>
<div :style="ringStyle"></div>
</div>
2.彈窗、對話框組件
彈窗組件是一個相對複雜的組件,既涉及到焦點管理,也涉及到播報內容管理。通過彈窗組件主要介紹焦點管理的小技巧。
該組件正在業務中的使用情況:
我們的彈窗組件相對比較通用,主要包括標題、內容、按鈕三個部分。其中,標題、內容既可以通過屬性傳入,也可以通過slot傳入。對於slot傳入的部分,組件不太好控制。在業務使用過程中,還普遍存在只有內容的彈窗。
適配目標:彈窗彈出時需要自動聚焦在標題上並播報。
彈窗焦點順序:彈窗彈出時需要自動聚焦在標題上並朗讀標題,然後手動移動下個焦點聚焦到説明文本,最後聚焦到操作按鈕。
(1)焦點管理
首先,需要解決彈窗彈出時的焦點聚焦問題。由於彈窗組件非常靈活,所以使用場景也非常多樣。對於屬性傳入的標題,組件內部可以獲取到該元素,並完成聚焦播報;對於slot插入的就顯得無能為力。
對於slot這種情況,組件和業務約定了一個屬性,如果業務想聚焦在這個dom元素上,就給該元素添加這個屬性。組件會通過el.querySelector查詢彈窗的後代元素,在彈窗彈出時自動添加tabindex並完成聚焦播報。
<nx-popup v-model="isPopupShow.center">
<div
nx-popup-aria-auto-focus="true"
aria-label="無障礙播報測試"
class="nx-popup-center"
>
Popup Center
</div>
</nx-popup>
另外,考慮到有業務要求彈出時不自動聚焦在彈窗上,所以還提供了屬性,用於關閉焦點管理功能。
然後,需要解決彈窗關閉時的焦點還原問題。在彈窗彈出前保存當前聚焦的元素document.activeElement,關閉彈窗以後,通過el.focus()手動聚焦在該元素上。
還剩一個焦點穿透問題。很多安卓系統aria-modal屬性不起作用,所以焦點還能夠穿透彈窗,選中頁面上的元素。
目前組件沒有特別好的辦法處理這點。開發過程中,可以對需要屏蔽的元素添加aria-hidden=true屬性
(2)播報內容管理
這部分就顯得比較簡單。對於slot的情況,播報內容可以交給業務開發控制;對於屬性傳入的情況,可以添加組件屬性,為業務提供定製化播報的能力。
3.地址選擇器組件
地址選擇器是一個滾動式交互的組件。在這個組件的開發過程中使用到了一種小技巧,即不聚焦在某元素上,也能自動播報該元素變化的內容。
該組件在業務中的使用情況:業務開發沒有對該組件定製化,所以只用考慮組件內部的適配即可。
適配目標:在彈窗彈出時自動聚焦到標題上並播報。下圖是焦點的排布。
以該圖為例,焦點聚焦在第一列的時候,播報“河北省,滑動滾輪控件,可上下滾動切換”;另外,在每一列滾動結束時,播報當前選中的地址,如“河北省,石家莊市,橋西區”
(1)焦點管理
同彈窗組件。
(2)播報內容管理
這裏的難點就在於每一列滾動結束的時候,需要播報變化後的地址,但是此時焦點還在選中的這列上。
這裏添加了一個隱藏的元素(不在頁面上顯示),並添加屬性aria-live="polite",滾動結束的時候修改ariaPickerContent的值。如果不想播報則將ariaPickerContent置為空字符串,彈窗關閉的時候記得置為空。
<div class="nx-picker-scroll-aria" aria-live="polite">
{{ ariaPickerContent }}
</div>
五、總結
本文是在vivo體系下的無障礙適配實踐,主要提煉總結了一些適配方法,對內外部的無障礙適配工作都有一定的參考和借鑑價值。下一步計劃豐富和完善組件庫的適配,沉澱一套高效的適配方案,儘量減少開發人員的適配成本,提高開發效率。
開發者應秉持着技術有温、代碼有愛的態度,堅守“勿以善小而不為”的準則,以用户為導向,讓我們的產品和服務照亮每一位用户,讓“障礙羣體”在這個有愛的互聯網時代,緊跟時代潮流。