在許多狀況下,[可用的HTML表單組件]()是不夠的。若你想在諸如<select>
元素的組件上[應用高級樣式]()、或者想定製組件的行爲,你就只能選擇建立本身的表單組件。java
咱們將經過本文學習如何構建一個表單組件。爲達到目的,咱們選擇重構<select>
元素做爲例子。jquery
注意:咱們會專一於構建組件,但不會關注如何保證代碼的通用和可重用。構建組件時會涉及到一些特殊的JavaScript代碼和未知上下文中的DOM操做,而這些內容已經超出了本文的討論範圍。git
在構建一個定製組件前,應先從明確你想要達到的效果開始,這會節省你寶貴的時間。具體來說,清晰地定義組件的全部狀態是很重要的。要作到這點,最好從一個已經存在的、狀態和行爲已經爲人所熟知的組件開始,這樣你就只需儘量地模仿該組件便可。github
在咱們的例子中,咱們會重構<select>
元素。下面是咱們指望達到的結果:web
上面的截屏展現了咱們組件的三個主要狀態:普通狀態(左)、激活狀態(中)和打開狀態(右)。chrome
至於組件的行爲,咱們但願能夠像其餘原生組件同樣,經過鼠標和鍵盤來操控它。先從定義組件如何到達各個狀態開始:segmentfault
組件變爲普通狀態:數組
頁面加載
組件激活且用戶點擊了組件外任意地方
組件激活且用戶用鍵盤把焦點移動到別的組件
注意:在頁面上移動焦點一般是經過敲tab鍵來實現的,但不是全部地方都遵循這個慣例。好比Safari上默認是用Option+Tab組合鍵來實如今頁面上移動焦點。
組件變爲激活狀態:
用戶點擊了組件
用戶按tab鍵且組件得到了焦點
組件處於打開狀態且用戶點擊了組件
組件變爲打開狀態:
組件處於其餘非打開狀態且用戶點擊了它
在知道如何改變狀態後,定義組件的值如何被改變也是很重要的:
組件的值改變:
在組件處於打開狀態時,用戶點擊了一個選項
在組件處於激活狀態時,用戶按了上下方向鍵
最後咱們來定義下組件選項的行爲:
當組件處於打開狀態時,被選中的選項會高亮
當鼠標移到一個選項上,該選項會高亮且原先高亮狀態的選項會恢復到普通狀態
考慮例子的演示目的,咱們的分析就到此爲止;然而若是你認真讀過上文,會發現咱們漏了一些效果。好比,當組件處於打開狀態時,若是用戶按了tab鍵會發生什麼呢?答案是--什麼都不會發生。正確的效果雖然顯而易見(譯註:參考select原生組件,也是什麼都不會發生),但事實是咱們沒有在上述說明中定義它,這個效果很容易就會被忽視。在團隊協做中,若是設計組件的人和實現它的人不一樣,這是特別容易出現的問題。
另外一個有趣的問題是:組件處於打開狀態時,用戶按上下方向鍵會發生什麼?要回答它,須要一點技巧。若考慮激活狀態和打開狀態是徹底不相干的,那答案就仍是「什麼都不會發生」,由於咱們並未給打開狀態定義任何鍵盤交互。另外一方面,若是考慮激活狀態和打開狀態有部分重疊,那答案就是:值可能會改變但選項也所以不會被高亮(譯註:大概由於組件已經處於激活狀態了吧),這也是由於當組件處於打開狀態時,咱們並未給選項未定義任何鍵盤交互(只是定義了組件打開時應該發生什麼,卻沒定義打開後要幹嗎)。
在咱們的例子中,缺失的特性仍是比較明顯的,因此咱們還能處理得了它;但當面對來自外部的新組件時,因爲沒人知道正確的行爲是什麼,這時就會形成真正的麻煩。所以,花些時間在設計階段是頗有必要的,若是你此時定義了一個不佳的交互,或忘記了去定義,後續在用戶使用了該交互時再去重定義是很困難的。若(處理交互時)你有疑問,應積極尋求他人的幫助;而若你心中有數,則應絕不猶豫地進行用戶測試。上面討論的過程,可稱之爲UX(譯註:用戶體驗)設計。若是你想了解更多這方面的內容,能夠參考下面這些資源:
注意:在多數系統中,還有有一種方法能夠打開
<select>
元素以查看全部可用的選項(這和用鼠標點擊<select>
元素是同樣的)。這個方法在Windows下是用Alt+下方向鍵來實現的,咱們的例子中並未實現它--但要這樣作也很簡單,由於整個操做的機制已經被用於實現click事件了。
上面咱們肯定了組建的基本功能,如今能夠來構建咱們的組件了。第一步咱們要定義其HMLT結構,併爲其添加基本的語義。下面是咱們重構<select>
元素所需的代碼:
<!-- 這是組件的主要容器. tabindex 特性用於讓用戶能聚焦到該組件。 用JavaScript來設置它是一個更好的辦法 --> <div class="select" tabindex="0"> <!-- 這個容器用於展現組件的當前值 --> <span class="value">Cherry</span> <!-- 這個容器會包含組件裏的全部可用選項,由於選項是一個列表,全部採用ul元素更加合適 --> <ul class="optList"> <!-- 每一個選項只會包含要展現的內容,稍後咱們會了解如何處理其真實值,用來和表單數據一塊兒發出去 --> <li class="option">Cherry</li> <li class="option">Lemon</li> <li class="option">Banana</li> <li class="option">Strawberry</li> <li class="option">Apple</li> </ul> </div>
要注意此處class名的使用;這些class標記了每一個相關的元素,而不須要依賴其實際使用的HTML元素。這麼作能確保咱們不會把CSS和JavaScript與HTML結構做強關聯,從而作到改變後續的組件代碼實現時,不破壞使用該組件的代碼。好比你想實現一個一樣的<optgroup>
元素時,可用直接用相同的代碼來調用。
如今咱們已經有了組件的結構了,接下來要來設計組件了。建立這個自定義組件的目的,是爲了用咱們想要的形式來給該組件添加樣式。要作到這點,咱們要把CSS的編碼工做拆爲兩部分:第一部分是讓咱們組件和<select>
元素看起來一致的必要CSS規則,第二部分是用來讓組件變成咱們想要的樣子的樣式。
必要的樣式是用來處理咱們組件的三個狀態的。
.select { /* 給選項列表建立一個定位上下文 */ position: relative; /* 讓咱們的組件成爲文本流的一部分,並使之可伸縮 */ display : inline-block; }
咱們須要一個額外類名active
,來定義組件處於激活狀態時的外觀。由於咱們的組件是能夠得到操做焦點的,因此還要將相同的樣式用於:focus
僞類,保證激活和得到焦點時的行爲一致。
.select.active, .select:focus { outline: none; /* box-shadow 屬性不是必要的,但它能夠做爲默認值保證激活狀態可見,去掉它也是能夠的。 */ box-shadow: 0 0 3px 1px #227755; }
接下來處理選項列表:
/* 這裏的 .select 選擇器,用來確保後面選擇器匹配的元素就是咱們組件中那個 */ .select .optList { /* 下面樣式確保選項列表會展現在當前值下面、並在HTML文檔流以外 */ position : absolute; top : 100%; left : 0; }
咱們須要一個額外的class來處理選項列表的隱藏狀態。爲了管理激活和展開兩個不一樣的狀態,這麼作是頗有必要的。
.select .optList.hidden { /* 下面是一個以無障礙方式來隱藏列表的簡單方法,咱們會在文末討論更多關於無障礙訪問的內容。 */ max-height: 0; visibility: hidden; }
在有了基本的功能以後,有趣的部分開始了。下面是一個可選的例子,效果和本文開頭的那個截圖一致。可是你也能夠自由探索、看看你能實現怎樣的效果。
.select { /* 全部的大小值都會採用em值來保證無障礙訪問 (保證組件在用戶使用瀏覽器純文字模式下的縮放時,還保留自適應的能力)。 在計算時,假設1em == 16px,這也是大多數瀏覽器的默認值。 若是你對px到em的轉換感到困惑,能夠訪問:http://riddle.pl/emcalc/ */ font-size : 0.625em; /* this (10px) is the new font size context for em value in this context */ font-family : Verdana, Arial, sans-serif; -moz-box-sizing : border-box; box-sizing : border-box; /* 須要額外的空間來添加向下箭頭 */ padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */ width : 10em; /* 100px */ border : .2em solid #000; /* 2px */ border-radius : .4em; /* 4px */ box-shadow : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */ /* 第一句聲明用於不支持線性漸變的瀏覽器。 第二句聲明是由於基於Webkit的瀏覽器對線性漸變屬性還要加個前綴。 若你還想支持老舊瀏覽器,可參考http://www.colorzilla.com/gradient-editor/ */ background : #F0F0F0; background : -webkit-linear-gradient(90deg, #E3E3E3, #fcfcfc 50%, #f0f0f0); background : linear-gradient(0deg, #E3E3E3, #fcfcfc 50%, #f0f0f0); } .select .value { /* 由於value元素可能會比組件還寬,因此咱們得保障這不會改變組件的寬度 */ display : inline-block; width : 100%; overflow : hidden; vertical-align: top; /* 若是內容溢出了,最好能有省略號來替代。 */ white-space : nowrap; text-overflow: ellipsis; }
咱們不須要額外的元素來設計向下箭頭,而是使用:after
僞元素。但其實這也能在select類上用一個簡單的背景圖片來實現。
.select:after { content : "▼"; /* 使用 unicode 字符 U+25BC;參見 http://www.utf8-chartable.de */ position: absolute; z-index : 1; /* 用來保證箭頭會疊在選項列表上面 */ top : 0; right : 0; -moz-box-sizing : border-box; box-sizing : border-box; height : 100%; width : 2em; /* 20px */ padding-top : .1em; /* 1px */ border-left : .2em solid #000; /* 2px */ border-radius: 0 .1em .1em 0; /* 0 1px 1px 0 */ background-color : #000; color : #FFF; text-align : center; }
接下來,給選項列表添加樣式:
.select .optList { z-index : 2; /* 代表選項列表會始終疊在向下箭頭之上 */ /* 重置ul元素的默認樣式 */ list-style: none; margin : 0; padding: 0; -moz-box-sizing : border-box; box-sizing : border-box; /* 確保即便值太少讓選項列表小於組件主體,也能讓選項列表會和組件主體同樣大 */ min-width : 100%; /* 若是列表太長了,其內容會在垂直方向上溢出(默認會自動添加一個垂直方向的滾動條), 但不會在水平方向上也這樣(由於咱們沒有設置寬度,列表會有個自適應寬度,若是不能自適應, 內容就會被截斷) */ max-height: 10em; /* 100px */ overflow-y: auto; overflow-x: hidden; border: .2em solid #000; /* 2px */ border-top-width : .1em; /* 1px */ border-radius: 0 0 .4em .4em; /* 0 0 4px 4px */ box-shadow: 0 .2em .4em rgba(0,0,0,.4); /* 0 2px 4px */ background: #f0f0f0; }
對於選項,咱們須要添加一個highlight類來標明用戶會選取(或已經選取)的值。
.select .option { padding: .2em .3em; /* 2px 3px */ } .select .highlight { background: #000; color: #FFFFFF; }
下面就是咱們三個狀態的實現效果了:
效果
如今咱們組件的結構和設計都已經作好,能夠來寫JavaScript代碼讓組件真正能運行起來了。
警告:下面的代碼是教學代碼,在實際編碼時不能直接像下面同樣使用。其中許多部分,並無將來使用的保障、並且也不能在老舊瀏覽器上使用。此外,這些代碼也有在生產環境中應該被優化掉的冗餘部分。
注意:建立可複用的組件是頗有技巧性的。W3C Web Component 草案是這個特定問題的一個解決方案。X-tag project是這一規範的實驗性實現;咱們鼓勵你好好了解下它。
在開始以前,咱們須要知道JavaScript的一個嚴重問題:在瀏覽器裏,它是一個不可靠的技術。當你在建立自定義組件的時候,你不得不依賴JavaScript,由於它是把全部東西維繫在一塊兒的繩索。可是,在許多狀況下JavaScript並不能在瀏覽器中運行:
用戶禁用了JavaScript:這已是個最不常見的狀況了,如今不多有人會禁用JavaScript。
腳本沒有加載:這是最廣泛的狀況,特別是在網絡不太可靠的移動端。
腳本有bug:你要常常考慮這一可能性。
腳本和第三方腳本衝突了:使用了追蹤腳本或用戶自用的書籤時會發生這種狀況。
用戶使用了老舊瀏覽器,而且你須要的一種特性不被支持:這一般發生在你用了很新的API時。
因爲有這些風險,咱們須要認真考慮下JavaScript不起做用時會發生什麼。深刻處理這個問題已經超出了本文的論述範圍,由於這和你但願如何讓腳本通用和可複用密切相關,咱們不會在例子中考慮這點。
在本文的例子中,若JavaScript代碼不能運行,咱們會回退到展現標準的<select>
元素。要作到這點,得先來作兩件事。
首先,咱們要在使用自定義組件以前,添加一個普通的<select>
元素。而爲了能讓自定義組件的數據和剩下的表單數據一塊兒發送,這一步也是頗有必要的。後邊咱們還會詳細介紹。
<body class="no-widget"> <form> <select name="myFruit"> <option>Cherry</option> <option>Lemon</option> <option>Banana</option> <option>Strawberry</option> <option>Apple</option> </select> <div class="select"> <span class="value">Cherry</span> <ul class="optList hidden"> <li class="option">Cherry</li> <li class="option">Lemon</li> <li class="option">Banana</li> <li class="option">Strawberry</li> <li class="option">Apple</li> </ul> </div> </form> </body>
第二,咱們還得添加兩個新的類名,實現隱藏不須要的元素(即在腳本能運行時的<select>
元素、或腳本不能運行時的自定義組件)。要注意的是在默認狀況下,此處的HTML代碼會隱藏自定義組件。
.widget select, .no-widget .select { /* 這個CSS選擇器意思是: - 要麼body的類名被設爲"widget",此處就要隱藏`<select>`元素 - 要麼body的類名沒有改變,還是"no-widget",那麼類名爲"select"的元素就要被隱藏了 */ position : absolute; left : -5000em; height : 0; overflow : hidden; }
至此,咱們只須要一個JavaScript開關來決定腳本是否能運行了。這個開關很簡單:若頁面加載了腳本並運行,就會移除no-widget
類並添加widget
類,實現對<select>
元素和自定義組件可見與否的切換。
window.addEventListener("load", function () { document.body.classList.remove("no-widget"); document.body.classList.add("widget"); });
注意:若你真的想讓你的組件變得通用和可複用,除了做類名的切換,更好的方法是(在腳本能執行時)只添加
widget
類名隱藏<select>
元素,並在頁面中的每一個<select>
元素後面指定自定義的組件、動態添加到DOM樹中。
在將要建立的代碼中,咱們會使用標準的DOM API來完成工做。然而,儘管瀏覽器對DOM API的支持已經愈來愈好,但在老舊瀏覽器上仍存在一些問題(特別在很老的IE上)。
若你想避免老舊瀏覽器上的麻煩,有兩種方法能夠作到:使用諸如jQuery, $dom, prototype, Dojo, YUI之類的穩定框架;或者補充那些缺失的但你要用的特性(經過條件加載能夠很容易作到這點,好比可使用yepnope庫)。
咱們計劃使用的特性以下(從風險最大到最安全排列):
forEach(不屬於DOM可是現代JavaScript的特性)
除了上述特性的可用性,在開發以前仍存在一個問題。querySelector()
方法返回的是一個NodeList
而不是數組。Array
對象支持forEach
方法、但NodeList
不支持。由於NodeList
看起來像數組、也由於forEach
方法用起來很方便,因此咱們能夠很簡單地就給NodeList
添加forEach
支持、讓咱們的工做輕鬆些,就像下面這樣:
NodeList.prototype.forEach = function (callback) { Array.prototype.forEach.call(this, callback); }
咱們說這很簡單可不是瞎說的哦。
前期工做已經作好了,咱們如今能夠來定義用戶和咱們的組件交互時要用到的全部函數了。
/* 這個函數會在取消激活自定義組件時被使用 須要一個參數: select: 類名爲`select`且要被取消激活的DOM節點 */ function deactivateSelect(select) { /* 若組件未被激活,則什麼都不作 */ if (!select.classList.contains('active')) return; /* 獲取自定義組件的選項列表 */ var optList = select.querySelector('.optList'); /* 關閉選項列表 */ optList.classList.add('hidden'); /* 取消自定義組件的激活狀態 */ select.classList.remove('active'); } /* 該函數用於讓用戶(取消)激活組件 須要兩個參數: select:類名爲`select`且要被激活的DOM節點 selectList:類名爲`select`的全部DOM節點的列表 */ function activeSelect(select, selectList) { /* 若組件已經激活,則什麼都不作 */ if (select.classList.contains('active')) return; /* 全部自定義組件的激活狀態都得取消, 由於deactivateSelect函數知足了做爲forEach回調函數的要求, 因此咱們會直接使用它而不是用一箇中間的匿名函數 */ selectList.forEach(deactivateSelect); /* 開啓該組件的激活狀態 */ select.classList.add('active'); } /* 該函數用於讓用戶打開和關閉選項列表 須要一個參數: select:有一個列表要切換狀態的DOM節點 */ function toggleOptList(select) { /* 選項列表能夠從組件那得到 */ var optList = select.querySelector('.optList'); /* 改變列表的類名來展現和隱藏它 */ optList.classList.toggle('hidden'); } /* 該函數用於高亮一個選項 須要兩個參數: select:類名爲`select`且包含要被高亮選項的DOM節點 option:類名爲`option`且要被高亮的DOM節點 */ function highlightOption(select, option) { /* 得到自定義select元素的全部可用選項 */ var optionList = select.querySelectorAll('.option'); /* 移除全部選項的高亮 */ optionList.forEach(function (other) { other.classList.remove('highlight'); }); /* 高亮正確的選項 */ option.classList.add('highlight'); };
上面就是處理自定義組件的多個狀態所需的全部函數。
接下來,咱們把這些函數綁到合適的事件上:
/* 在文檔加載出來後處理下事件綁定 */ window.addEventListener('load', function () { var selectList = document.querySelectorAll('.select'); /* 每一個自定義組件都要被初始化 */ selectList.forEach(function (select) { /* 全部的`select`元素也要被初始化 */ var optionList = select.querySelectorAll('.option'); /* 用戶把鼠標放到一個選項上時,高亮該選項 */ optionList.forEach(function (option) { option.addEventListener('mouseover', function () { /* 注意:在咱們的函數調用內,`select`和`option`變量都是局部的 */ highlightOption(select, option); }); }); /* 用戶點擊了自定義的select元素 */ select.addEventListener('click', function (event) { /* 注意:在咱們的函數調用內,`select`變量是局部的 */ /* 改變選項列表的可見狀態 */ toggleOptList(select); }); /* 組件得到焦點時 /* 用戶點擊組件或用tab鍵訪問組件時,組件會得到焦點 */ select.addEventListener('focus', function (event) { /* 注意:在咱們的函數調用內,`select`和`selectList`變量都是局部的 */ /* 激活該組件 */ activeSelect(select, selectList); }); /* 組件失去焦點時 */ select.addEventListener('blur', function (event) { /* 注意:在咱們的函數調用內,`select`變量是局部的 */ /* 取消激活該組件 */ deactivateSelect(select); }); }); });
至此,組件已經能根據咱們的設計來改變其狀態了,但它的值目前還不會更新,接下來咱們就會處理這點。
如今組件已經能用了,但咱們還得加點代碼,根據用戶的輸入更新它的值、並讓其能隨着表單數據一塊兒發送它的值。
要作到這點,最簡單的方式就是在私底下用一個原生組件。這樣一來,自定義組件就會跟蹤瀏覽器提供的內置控件的值,並和平時同樣在表單提交時發送它的值。在瀏覽器已經爲咱們作好這一切時,沒有必要來從新發明輪子了。
如前所示,出於可訪問性的緣由,咱們已經用了一個原生的select組件來做爲回退;同步這個組件的值和自定義組件的值是很容易的:
// 該函數用於更新展現的值,並和原生組件做同步 // 須要兩個參數: // select:類名爲`select`且值要更新的DOM節點 // index:選定的值的索引 function updateValue(select, index) { // 咱們得爲給定的自定義組件獲取原生組件 // 本例中,原生組件是自定義組件的兄弟節點 var nativeWidget = select.previousElementSibling; // 得到自定義組件的值容器 var value = select.querySelector('.value'); // 得到完整的選項列表 var optionList = select.querySelectorAll('.option'); // 設置選中索引爲咱們選擇的選項的索引 nativeWidget.selectedIndex = index; // 更新對應的值容器 value.innerHTML = optionList[index].innerHTML; // 高亮自定義組件中關聯的選項 highlightOption(select, optionList[index]); }; // 該函數返回原生組件當前選中的索引 // 須要一個參數: // select:類名爲`select`且和原生組件關聯的DOM節點 function getIndex(select) { // 咱們得爲給定的自定義組件獲取原生組件 // 本例中,原生組件是自定義組件的兄弟節點 var nativeWidget = select.previousElementSibling; return nativeWidget.selectedIndex; };
咱們能夠用上面這兩個函數來綁定原生組件和自定義組件:
// 在文檔加載出來後處理下事件綁定 window.addEventListener('load', function () { var selectList = document.querySelectorAll('.select'); // 每一個自定義組件都要被初始化 selectList.forEach(function (select) { var optionList = select.querySelectorAll('.option'), selectedIndex = getIndex(select); // 讓自定義組件能聚焦 select.tabIndex = 0; // 讓原生組件不可聚焦 select.previousElementSibling.tabIndex = -1; // 確保默認選擇的值被正確展現 updateValue(select, selectedIndex); // 用戶點擊選項時,更新對應的值 optionList.forEach(function (option, index) { option.addEventListener('click', function (event) { updateValue(select, index); }); }); // 用戶在聚焦的組件上按鍵盤時,更新對應的值 select.addEventListener('keyup', function (event) { var length = optionList.length, index = getIndex(select); // 當用戶按下箭頭時,跳到後一選項 if (event.keyCode === 40 && index < length - 1) { index++; } // 當用戶按上箭頭時,跳到前一選項 if (event.keyCode === 38 && index > 0) { index--; } updateValue(select, index); }); }); });
上面的代碼裏,要注意tabIndex屬性的使用。該屬性用來確保原生組件不會得到焦點,並確保自定義組件能在用戶用鍵盤或鼠標訪問時得到焦點。
經過上面的工做,咱們已經完成任務了!下面就是結果:
等等,咱們真的完成了嗎?
咱們已經構建了一個能夠運行的組件,雖然距離獲得一個具備完整特性的選擇框還很遠,但它運行得還不錯。然而,咱們以前所作的只是在處理DOM而已,這個組件並非真正語義化的,並且雖然它看起來像個選擇框,但在瀏覽器的角度它卻並非這樣,所以無障礙技術也不會認爲它是個選擇框。簡而言之,它就是個無障礙性不好的漂亮選擇框!
幸運的是,咱們有個解決方案叫ARIA。ARIA表示「無障礙的富Internet應用」,它是個W3C規範,用來讓web應用和自定義組件變得無障礙。基本上這個規範就是一系列拓展了HTML的特性,用這些特性,咱們能夠更好地描述角色、狀態和屬性,讓咱們剛纔設計的元素變得像其盡力模仿的原生元素同樣。使用這些特性很簡單,下面咱們來試試。
ARIA使用的關鍵特性是role。該特性會接收一個定義了元素用途的值,每一個值都表明了元素的特色和行爲。在本例中,咱們會使用一個listbox做爲role值,這個值是個「複合的role」,指定的元素能夠包含多個特定role的子元素(本例中,至少有一個元素role值爲option
)。
值得注意的是,ARIA定義的role默認會自動用於標準的HTML標籤中。好比說,<table>
元素對應grid
,<ul>
元素對應list
。由於咱們的組件使用了<ul>
元素,因此得確保組件的listbox
role能覆蓋掉<ul>
元素的list
值。爲此,可使用presentation
這個role值,該值用來指明一個沒有特殊含義的元素,並且該元素只用來展現信息而已。這裏咱們會給<ul>
應用presentation
值。
要使用listbox
這個role值,得像下面同樣修改HTML:
<!-- 給最外層元素指定role="listbox" --> <div class="select" role="listbox"> <span class="value">Cherry</span> <!-- 給ul元素指定role="presentation" --> <ul class="optList" role="presentation"> <!-- 給全部li元素指定role="presentation" --> <li role="option" class="option">Cherry</li> <li role="option" class="option">Lemon</li> <li role="option" class="option">Banana</li> <li role="option" class="option">Strawberry</li> <li role="option" class="option">Apple</li> </ul> </div>
注意:若是你想兼容那些不支持CSS特性選擇器的老舊瀏覽器,同時使用
role
特性和class
特性這種作法是必須的。
僅使用role特性是不夠的,ARIA自己也提供了不少許多狀態和屬性特性。對這些特性用得越多和越恰當,網頁就越能被無障礙技術所理解。在咱們的例子中,只會用到一個特性:aria-selected
。
aria-selected
特性用於標記當前選中的選項,這樣無障礙技術就能提示用戶當前選中項是什麼。咱們會在JavaScript中動態地使用它,在用戶選中一個選項時能標記該選中項。爲此,得修改下updateValue()
函數:
function updateValue(select, index) { var nativeWidget = select.previousElementSibling; var value = select.querySelector('.value'); var optionList = select.querySelectorAll('.option'); // 確保全部的選項未被選中 optionList.forEach(function (other) { other.setAttribute('aria-selected', 'false'); }); // 確保選擇的那個選項被選中 optionList[index].setAttribute('aria-selected', 'true'); nativeWidget.selectedIndex = index; value.innerHTML = optionList[index].innerHTML; highlightOption(select, optionList[index]); };
上述修改的最終效果以下(訪問該組件時使用無障礙技術,譬如NVDA或VoiceOver,會有更好的體驗):
至此咱們已經瞭解了建立定製表單組件的全部基本知識,但如你所見,這麼作並不簡單,若是使用第三方庫的話會比本身從頭寫起更好、更簡單(固然除非你是想構建這樣一個庫)。
下面是你在本身開發以前應該參考下的庫:
若你想更進一步使用本例,爲讓其中的代碼變得通用和可複用,還要對代碼作一些改進。這個練習你能夠本身嘗試下,這裏有兩個提示:首先,全部函數的第一個參數都相同,這就意味着這些函數須要有同一個執行上下文,使用一個對象來共享執行上下文是很明智的。此外,代碼還得保證兼容,即代碼最好能在兼容不一樣Web標準的多種瀏覽器下運行。