摘要:若你想在諸如元素的組件上應(yīng)用高級樣式或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。
系列文章說明
原文
在許多情況下,[可用的HTML表單組件]()是不夠的。若你想在諸如元素的組件上[應(yīng)用高級樣式]()、或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。
我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。為達到目的,我們選擇重構(gòu)元素作為例子。
設(shè)計,結(jié)構(gòu)和語義注意:我們會專注于構(gòu)建組件,但不會關(guān)注如何保證代碼的通用和可重用。構(gòu)建組件時會涉及到一些特殊的JavaScript代碼和未知上下文中的DOM操作,而這些內(nèi)容已經(jīng)超出了本文的討論范圍。
在構(gòu)建一個定制組件前,應(yīng)先從明確你想要達到的效果開始,這會節(jié)省你寶貴的時間。具體來講,清晰地定義組件的所有狀態(tài)是很重要的。要做到這點,最好從一個已經(jīng)存在的、狀態(tài)和行為已經(jīng)為人所熟知的組件開始,這樣你就只需盡可能地模仿該組件即可。
在我們的例子中,我們會重構(gòu)元素。下面是我們期望達到的結(jié)果:
上面的截屏展示了我們組件的三個主要狀態(tài):普通狀態(tài)(左)、激活狀態(tài)(中)和打開狀態(tài)(右)。
至于組件的行為,我們希望可以像其他原生組件一樣,通過鼠標(biāo)和鍵盤來操控它。先從定義組件如何到達各個狀態(tài)開始:
組件變?yōu)槠胀顟B(tài):
頁面加載
組件激活且用戶點擊了組件外任意地方
組件激活且用戶用鍵盤把焦點移動到別的組件
注意:在頁面上移動焦點通常是通過敲tab鍵來實現(xiàn)的,但不是所有地方都遵循這個慣例。比如Safari上默認是用Option+Tab組合鍵來實現(xiàn)在頁面上移動焦點。
組件變?yōu)榧せ顮顟B(tài):
用戶點擊了組件
用戶按tab鍵且組件獲得了焦點
組件處于打開狀態(tài)且用戶點擊了組件
組件變?yōu)榇蜷_狀態(tài):
組件處于其他非打開狀態(tài)且用戶點擊了它
在知道如何改變狀態(tài)后,定義組件的值如何被改變也是很重要的:
組件的值改變:
在組件處于打開狀態(tài)時,用戶點擊了一個選項
在組件處于激活狀態(tài)時,用戶按了上下方向鍵
最后我們來定義下組件選項的行為:
當(dāng)組件處于打開狀態(tài)時,被選中的選項會高亮
當(dāng)鼠標(biāo)移到一個選項上,該選項會高亮且原先高亮狀態(tài)的選項會恢復(fù)到普通狀態(tài)
考慮例子的演示目的,我們的分析就到此為止;然而如果你認真讀過上文,會發(fā)現(xiàn)我們漏了一些效果。比如,當(dāng)組件處于打開狀態(tài)時,如果用戶按了tab鍵會發(fā)生什么呢?答案是--什么都不會發(fā)生。正確的效果雖然顯而易見(譯注:參考select原生組件,也是什么都不會發(fā)生),但事實是我們沒有在上述說明中定義它,這個效果很容易就會被忽視。在團隊協(xié)作中,如果設(shè)計組件的人和實現(xiàn)它的人不同,這是特別容易出現(xiàn)的問題。
另一個有趣的問題是:組件處于打開狀態(tài)時,用戶按上下方向鍵會發(fā)生什么?要回答它,需要一點技巧。若考慮激活狀態(tài)和打開狀態(tài)是完全不相干的,那答案就還是“什么都不會發(fā)生”,因為我們并未給打開狀態(tài)定義任何鍵盤交互。另一方面,如果考慮激活狀態(tài)和打開狀態(tài)有部分重疊,那答案就是:值可能會改變但選項也因此不會被高亮(譯注:大概因為組件已經(jīng)處于激活狀態(tài)了吧),這也是因為當(dāng)組件處于打開狀態(tài)時,我們并未給選項未定義任何鍵盤交互(只是定義了組件打開時應(yīng)該發(fā)生什么,卻沒定義打開后要干嘛)。
在我們的例子中,缺失的特性還是比較明顯的,所以我們還能處理得了它;但當(dāng)面對來自外部的新組件時,由于沒人知道正確的行為是什么,這時就會造成真正的麻煩。因此,花些時間在設(shè)計階段是很有必要的,如果你此時定義了一個不佳的交互,或忘記了去定義,后續(xù)在用戶使用了該交互時再去重定義是很困難的。若(處理交互時)你有疑問,應(yīng)積極尋求他人的幫助;而若你心中有數(shù),則應(yīng)毫不猶豫地進行用戶測試。上面討論的過程,可稱之為UX(譯注:用戶體驗)設(shè)計。如果你想了解更多這方面的內(nèi)容,可以參考下面這些資源:
UXMatters.com
UXDesign.com
The UX Design section of SmashingMagazine
定義HTML結(jié)構(gòu)和語義注意:在多數(shù)系統(tǒng)中,還有有一種方法可以打開元素以查看所有可用的選項(這和用鼠標(biāo)點擊元素是一樣的)。這個方法在Windows下是用Alt+下方向鍵來實現(xiàn)的,我們的例子中并未實現(xiàn)它--但要這樣做也很簡單,因為整個操作的機制已經(jīng)被用于實現(xiàn)click事件了。
上面我們確定了組建的基本功能,現(xiàn)在可以來構(gòu)建我們的組件了。第一步我們要定義其HMLT結(jié)構(gòu),并為其添加基本的語義。下面是我們重構(gòu)元素所需的代碼:
Cherry
- Cherry
- Lemon
- Banana
- Strawberry
- Apple
要注意此處class名的使用;這些class標(biāo)記了每個相關(guān)的元素,而不需要依賴其實際使用的HTML元素。這么做能確保我們不會把CSS和JavaScript與HTML結(jié)構(gòu)作強關(guān)聯(lián),從而做到改變后續(xù)的組件代碼實現(xiàn)時,不破壞使用該組件的代碼。比如你想實現(xiàn)一個同樣的元素時,可用直接用相同的代碼來調(diào)用。
用CSS創(chuàng)建樣式和交互現(xiàn)在我們已經(jīng)有了組件的結(jié)構(gòu)了,接下來要來設(shè)計組件了。創(chuàng)建這個自定義組件的目的,是為了用我們想要的形式來給該組件添加樣式。要做到這點,我們要把CSS的編碼工作拆為兩部分:第一部分是讓我們組件和元素看起來一致的必要CSS規(guī)則,第二部分是用來讓組件變成我們想要的樣子的樣式。
必要的樣式必要的樣式是用來處理我們組件的三個狀態(tài)的。
.select { /* 給選項列表創(chuàng)建一個定位上下文 */ position: relative; /* 讓我們的組件成為文本流的一部分,并使之可伸縮 */ display : inline-block; }
我們需要一個額外類名active,來定義組件處于激活狀態(tài)時的外觀。因為我們的組件是可以獲得操作焦點的,所以還要將相同的樣式用于:focus偽類,保證激活和獲得焦點時的行為一致。
.select.active, .select:focus { outline: none; /* box-shadow 屬性不是必要的,但它可以作為默認值保證激活狀態(tài)可見,去掉它也是可以的。 */ box-shadow: 0 0 3px 1px #227755; }
接下來處理選項列表:
/* 這里的 .select 選擇器,用來確保后面選擇器匹配的元素就是我們組件中那個 */ .select .optList { /* 下面樣式確保選項列表會展示在當(dāng)前值下面、并在HTML文檔流之外 */ position : absolute; top : 100%; left : 0; }
我們需要一個額外的class來處理選項列表的隱藏狀態(tài)。為了管理激活和展開兩個不同的狀態(tài),這么做是很有必要的。
.select .optList.hidden { /* 下面是一個以無障礙方式來隱藏列表的簡單方法,我們會在文末討論更多關(guān)于無障礙訪問的內(nèi)容。 */ max-height: 0; visibility: hidden; }美化
在有了基本的功能之后,有趣的部分開始了。下面是一個可選的例子,效果和本文開頭的那個截圖一致。但是你也可以自由探索、看看你能實現(xiàn)怎樣的效果。
.select { /* 所有的大小值都會采用em值來保證無障礙訪問 (保證組件在用戶使用瀏覽器純文字模式下的縮放時,還保留自適應(yīng)的能力)。 在計算時,假設(shè)1em == 16px,這也是大多數(shù)瀏覽器的默認值。 如果你對px到em的轉(zhuǎn)換感到困惑,可以訪問: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; /* 如果內(nèi)容溢出了,最好能有省略號來替代。 */ white-space : nowrap; text-overflow: ellipsis; }
我們不需要額外的元素來設(shè)計向下箭頭,而是使用:after偽元素。但其實這也能在select類上用一個簡單的背景圖片來實現(xiàn)。
.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%; /* 如果列表太長了,其內(nèi)容會在垂直方向上溢出(默認會自動添加一個垂直方向的滾動條), 但不會在水平方向上也這樣(因為我們沒有設(shè)置寬度,列表會有個自適應(yīng)寬度,如果不能自適應(yīng), 內(nèi)容就會被截斷) */ 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類來標(biāo)明用戶會選?。ɑ蛞呀?jīng)選?。┑闹怠?/p>
.select .option { padding: .2em .3em; /* 2px 3px */ } .select .highlight { background: #000; color: #FFFFFF; }
下面就是我們?nèi)齻€狀態(tài)的實現(xiàn)效果了:
效果
現(xiàn)在我們組件的結(jié)構(gòu)和設(shè)計都已經(jīng)做好,可以來寫JavaScript代碼讓組件真正能運行起來了。
為什么不起作用?警告:下面的代碼是教學(xué)代碼,在實際編碼時不能直接像下面一樣使用。其中許多部分,并沒有未來使用的保障、而且也不能在老舊瀏覽器上使用。此外,這些代碼也有在生產(chǎn)環(huán)境中應(yīng)該被優(yōu)化掉的冗余部分。
注意:創(chuàng)建可復(fù)用的組件是很有技巧性的。W3C Web Component 草案是這個特定問題的一個解決方案。X-tag project是這一規(guī)范的實驗性實現(xiàn);我們鼓勵你好好了解下它。
在開始之前,我們需要知道JavaScript的一個嚴(yán)重問題:在瀏覽器里,它是一個不可靠的技術(shù)。當(dāng)你在創(chuàng)建自定義組件的時候,你不得不依賴JavaScript,因為它是把所有東西維系在一起的繩索。但是,在許多情況下JavaScript并不能在瀏覽器中運行:
用戶禁用了JavaScript:這已經(jīng)是個最不常見的情況了,現(xiàn)在很少有人會禁用JavaScript。
腳本沒有加載:這是最普遍的情況,特別是在網(wǎng)絡(luò)不太可靠的移動端。
腳本有bug:你要經(jīng)??紤]這一可能性。
腳本和第三方腳本沖突了:使用了追蹤腳本或用戶自用的書簽時會發(fā)生這種情況。
腳本和瀏覽器拓展(如火狐的NoScript拓展或Chrome的NoScripts拓展)發(fā)生沖突、或受到干擾。
用戶使用了老舊瀏覽器,并且你需要的一種特性不被支持:這通常發(fā)生在你用了很新的API時。
由于有這些風(fēng)險,我們需要認真考慮下JavaScript不起作用時會發(fā)生什么。深入處理這個問題已經(jīng)超出了本文的論述范圍,因為這和你希望如何讓腳本通用和可復(fù)用密切相關(guān),我們不會在例子中考慮這點。
在本文的例子中,若JavaScript代碼不能運行,我們會回退到展示標(biāo)準(zhǔn)的元素。要做到這點,得先來做兩件事。
首先,我們要在使用自定義組件之前,添加一個普通的元素。而為了能讓自定義組件的數(shù)據(jù)和剩下的表單數(shù)據(jù)一起發(fā)送,這一步也是很有必要的。后邊我們還會詳細介紹。
第二,我們還得添加兩個新的類名,實現(xiàn)隱藏不需要的元素(即在腳本能運行時的元素、或腳本不能運行時的自定義組件)。要注意的是在默認情況下,此處的HTML代碼會隱藏自定義組件。
.widget select, .no-widget .select { /* 這個CSS選擇器意思是: - 要么body的類名被設(shè)為"widget",此處就要隱藏`
至此,我們只需要一個JavaScript開關(guān)來決定腳本是否能運行了。這個開關(guān)很簡單:若頁面加載了腳本并運行,就會移除no-widget類并添加widget類,實現(xiàn)對元素和自定義組件可見與否的切換。
window.addEventListener("load", function () { document.body.classList.remove("no-widget"); document.body.classList.add("widget"); });
效果
讓工作輕松些注意:若你真的想讓你的組件變得通用和可復(fù)用,除了作類名的切換,更好的方法是(在腳本能執(zhí)行時)只添加widget類名隱藏元素,并在頁面中的每個元素后面指定自定義的組件、動態(tài)添加到DOM樹中。
在將要創(chuàng)建的代碼中,我們會使用標(biāo)準(zhǔn)的DOM API來完成工作。然而,盡管瀏覽器對DOM API的支持已經(jīng)越來越好,但在老舊瀏覽器上仍存在一些問題(特別在很老的IE上)。
若你想避免老舊瀏覽器上的麻煩,有兩種方法可以做到:使用諸如jQuery, $dom, prototype, Dojo, YUI之類的穩(wěn)定框架;或者補充那些缺失的但你要用的特性(通過條件加載可以很容易做到這點,比如可以使用yepnope庫)。
我們計劃使用的特性如下(從風(fēng)險最大到最安全排列):
classList
addEventListener
forEach(不屬于DOM但是現(xiàn)代JavaScript的特性)
querySelector和querySelectorAll
除了上述特性的可用性,在開發(fā)之前仍存在一個問題。querySelector()方法返回的是一個NodeList而不是數(shù)組。Array對象支持forEach方法、但NodeList不支持。因為NodeList看起來像數(shù)組、也因為forEach方法用起來很方便,所以我們可以很簡單地就給NodeList添加forEach支持、讓我們的工作輕松些,就像下面這樣:
NodeList.prototype.forEach = function (callback) { Array.prototype.forEach.call(this, callback); }
我們說這很簡單可不是瞎說的哦。
建立事件回調(diào)前期工作已經(jīng)做好了,我們現(xiàn)在可以來定義用戶和我們的組件交互時要用到的所有函數(shù)了。
/* 這個函數(shù)會在取消激活自定義組件時被使用 需要一個參數(shù): select: 類名為`select`且要被取消激活的DOM節(jié)點 */ function deactivateSelect(select) { /* 若組件未被激活,則什么都不做 */ if (!select.classList.contains("active")) return; /* 獲取自定義組件的選項列表 */ var optList = select.querySelector(".optList"); /* 關(guān)閉選項列表 */ optList.classList.add("hidden"); /* 取消自定義組件的激活狀態(tài) */ select.classList.remove("active"); } /* 該函數(shù)用于讓用戶(取消)激活組件 需要兩個參數(shù): select:類名為`select`且要被激活的DOM節(jié)點 selectList:類名為`select`的所有DOM節(jié)點的列表 */ function activeSelect(select, selectList) { /* 若組件已經(jīng)激活,則什么都不做 */ if (select.classList.contains("active")) return; /* 所有自定義組件的激活狀態(tài)都得取消, 因為deactivateSelect函數(shù)滿足了作為forEach回調(diào)函數(shù)的要求, 所以我們會直接使用它而不是用一個中間的匿名函數(shù) */ selectList.forEach(deactivateSelect); /* 開啟該組件的激活狀態(tài) */ select.classList.add("active"); } /* 該函數(shù)用于讓用戶打開和關(guān)閉選項列表 需要一個參數(shù): select:有一個列表要切換狀態(tài)的DOM節(jié)點 */ function toggleOptList(select) { /* 選項列表可以從組件那獲得 */ var optList = select.querySelector(".optList"); /* 改變列表的類名來展示和隱藏它 */ optList.classList.toggle("hidden"); } /* 該函數(shù)用于高亮一個選項 需要兩個參數(shù): select:類名為`select`且包含要被高亮選項的DOM節(jié)點 option:類名為`option`且要被高亮的DOM節(jié)點 */ function highlightOption(select, option) { /* 獲得自定義select元素的所有可用選項 */ var optionList = select.querySelectorAll(".option"); /* 移除所有選項的高亮 */ optionList.forEach(function (other) { other.classList.remove("highlight"); }); /* 高亮正確的選項 */ option.classList.add("highlight"); };
上面就是處理自定義組件的多個狀態(tài)所需的所有函數(shù)。
接下來,我們把這些函數(shù)綁到合適的事件上:
/* 在文檔加載出來后處理下事件綁定 */ window.addEventListener("load", function () { var selectList = document.querySelectorAll(".select"); /* 每個自定義組件都要被初始化 */ selectList.forEach(function (select) { /* 所有的`select`元素也要被初始化 */ var optionList = select.querySelectorAll(".option"); /* 用戶把鼠標(biāo)放到一個選項上時,高亮該選項 */ optionList.forEach(function (option) { option.addEventListener("mouseover", function () { /* 注意:在我們的函數(shù)調(diào)用內(nèi),`select`和`option`變量都是局部的 */ highlightOption(select, option); }); }); /* 用戶點擊了自定義的select元素 */ select.addEventListener("click", function (event) { /* 注意:在我們的函數(shù)調(diào)用內(nèi),`select`變量是局部的 */ /* 改變選項列表的可見狀態(tài) */ toggleOptList(select); }); /* 組件獲得焦點時 /* 用戶點擊組件或用tab鍵訪問組件時,組件會獲得焦點 */ select.addEventListener("focus", function (event) { /* 注意:在我們的函數(shù)調(diào)用內(nèi),`select`和`selectList`變量都是局部的 */ /* 激活該組件 */ activeSelect(select, selectList); }); /* 組件失去焦點時 */ select.addEventListener("blur", function (event) { /* 注意:在我們的函數(shù)調(diào)用內(nèi),`select`變量是局部的 */ /* 取消激活該組件 */ deactivateSelect(select); }); }); });
至此,組件已經(jīng)能根據(jù)我們的設(shè)計來改變其狀態(tài)了,但它的值目前還不會更新,接下來我們就會處理這點。
效果
處理組件的值現(xiàn)在組件已經(jīng)能用了,但我們還得加點代碼,根據(jù)用戶的輸入更新它的值、并讓其能隨著表單數(shù)據(jù)一起發(fā)送它的值。
要做到這點,最簡單的方式就是在私底下用一個原生組件。這樣一來,自定義組件就會跟蹤瀏覽器提供的內(nèi)置控件的值,并和平時一樣在表單提交時發(fā)送它的值。在瀏覽器已經(jīng)為我們做好這一切時,沒有必要來重新發(fā)明輪子了。
如前所示,出于可訪問性的原因,我們已經(jīng)用了一個原生的select組件來作為回退;同步這個組件的值和自定義組件的值是很容易的:
// 該函數(shù)用于更新展示的值,并和原生組件作同步 // 需要兩個參數(shù): // select:類名為`select`且值要更新的DOM節(jié)點 // index:選定的值的索引 function updateValue(select, index) { // 我們得為給定的自定義組件獲取原生組件 // 本例中,原生組件是自定義組件的兄弟節(jié)點 var nativeWidget = select.previousElementSibling; // 獲得自定義組件的值容器 var value = select.querySelector(".value"); // 獲得完整的選項列表 var optionList = select.querySelectorAll(".option"); // 設(shè)置選中索引為我們選擇的選項的索引 nativeWidget.selectedIndex = index; // 更新對應(yīng)的值容器 value.innerHTML = optionList[index].innerHTML; // 高亮自定義組件中關(guān)聯(lián)的選項 highlightOption(select, optionList[index]); }; // 該函數(shù)返回原生組件當(dāng)前選中的索引 // 需要一個參數(shù): // select:類名為`select`且和原生組件關(guān)聯(lián)的DOM節(jié)點 function getIndex(select) { // 我們得為給定的自定義組件獲取原生組件 // 本例中,原生組件是自定義組件的兄弟節(jié)點 var nativeWidget = select.previousElementSibling; return nativeWidget.selectedIndex; };
我們可以用上面這兩個函數(shù)來綁定原生組件和自定義組件:
// 在文檔加載出來后處理下事件綁定 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); // 用戶點擊選項時,更新對應(yīng)的值 optionList.forEach(function (option, index) { option.addEventListener("click", function (event) { updateValue(select, index); }); }); // 用戶在聚焦的組件上按鍵盤時,更新對應(yīng)的值 select.addEventListener("keyup", function (event) { var length = optionList.length, index = getIndex(select); // 當(dāng)用戶按下箭頭時,跳到后一選項 if (event.keyCode === 40 && index < length - 1) { index++; } // 當(dāng)用戶按上箭頭時,跳到前一選項 if (event.keyCode === 38 && index > 0) { index--; } updateValue(select, index); }); }); });
上面的代碼里,要注意tabIndex屬性的使用。該屬性用來確保原生組件不會獲得焦點,并確保自定義組件能在用戶用鍵盤或鼠標(biāo)訪問時獲得焦點。
通過上面的工作,我們已經(jīng)完成任務(wù)了!下面就是結(jié)果:
效果
等等,我們真的完成了嗎?
讓組件變得無障礙我們已經(jīng)構(gòu)建了一個可以運行的組件,雖然距離得到一個具有完整特性的選擇框還很遠,但它運行得還不錯。然而,我們之前所做的只是在處理DOM而已,這個組件并不是真正語義化的,而且雖然它看起來像個選擇框,但在瀏覽器的角度它卻并不是這樣,因此無障礙技術(shù)也不會認為它是個選擇框。簡而言之,它就是個無障礙性很差的漂亮選擇框!
幸運的是,我們有個解決方案叫ARIA。ARIA表示“無障礙的富Internet應(yīng)用”,它是個W3C規(guī)范,用來讓web應(yīng)用和自定義組件變得無障礙?;旧线@個規(guī)范就是一系列拓展了HTML的特性,用這些特性,我們可以更好地描述角色、狀態(tài)和屬性,讓我們剛才設(shè)計的元素變得像其盡力模仿的原生元素一樣。使用這些特性很簡單,下面我們來試試。
role特性ARIA使用的關(guān)鍵特性是role。該特性會接收一個定義了元素用途的值,每個值都代表了元素的特點和行為。在本例中,我們會使用一個listbox作為role值,這個值是個“復(fù)合的role”,指定的元素可以包含多個特定role的子元素(本例中,至少有一個元素role值為option)。
值得注意的是,ARIA定義的role默認會自動用于標(biāo)準(zhǔn)的HTML標(biāo)簽中。比如說, 要使用listbox這個role值,得像下面一樣修改HTML: 注意:如果你想兼容那些不支持CSS特性選擇器的老舊瀏覽器,同時使用role特性和class特性這種做法是必須的。 僅使用role特性是不夠的,ARIA本身也提供了很多許多狀態(tài)和屬性特性。對這些特性用得越多和越恰當(dāng),網(wǎng)頁就越能被無障礙技術(shù)所理解。在我們的例子中,只會用到一個特性:aria-selected。 aria-selected特性用于標(biāo)記當(dāng)前選中的選項,這樣無障礙技術(shù)就能提示用戶當(dāng)前選中項是什么。我們會在JavaScript中動態(tài)地使用它,在用戶選中一個選項時能標(biāo)記該選中項。為此,得修改下updateValue()函數(shù): 上述修改的最終效果如下(訪問該組件時使用無障礙技術(shù),譬如NVDA或VoiceOver,會有更好的體驗): 效果 至此我們已經(jīng)了解了創(chuàng)建定制表單組件的所有基本知識,但如你所見,這么做并不簡單,如果使用第三方庫的話會比自己從頭寫起更好、更簡單(當(dāng)然除非你是想構(gòu)建這樣一個庫)。 下面是你在自己開發(fā)之前應(yīng)該參考下的庫: jQuery UI msDropDown Nice Forms 更多的庫 若你想更進一步使用本例,為讓其中的代碼變得通用和可復(fù)用,還要對代碼做一些改進。這個練習(xí)你可以自己嘗試下,這里有兩個提示:首先,所有函數(shù)的第一個參數(shù)都相同,這就意味著這些函數(shù)需要有同一個執(zhí)行上下文,使用一個對象來共享執(zhí)行上下文是很明智的。此外,代碼還得保證兼容,即代碼最好能在兼容不同Web標(biāo)準(zhǔn)的多種瀏覽器下運行。 文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。 轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/50903.html 摘要:示例多選框和單選框給多選框和單選框添加樣式是很讓人凌亂的。通向漂亮表單之路一些有用的庫和拓展工具盡管在復(fù)選框和單選框上的表現(xiàn)力已經(jīng)夠用了,但離其支持高級表單組件仍然遙遙無期。其表單部分挺有用的。
系列文章說明
原文
在本文中,我們將了解如何在HTML表單上使用CSS,為那些難于自定義的表單組件加以樣式。如前文所述,文本框和按鈕很適合使用CSS,而現(xiàn)在我們得來探索HTML表單樣式的那... 摘要:示例多選框和單選框給多選框和單選框添加樣式是很讓人凌亂的。通向漂亮表單之路一些有用的庫和拓展工具盡管在復(fù)選框和單選框上的表現(xiàn)力已經(jīng)夠用了,但離其支持高級表單組件仍然遙遙無期。其表單部分挺有用的。
系列文章說明
原文
在本文中,我們將了解如何在HTML表單上使用CSS,為那些難于自定義的表單組件加以樣式。如前文所述,文本框和按鈕很適合使用CSS,而現(xiàn)在我們得來探索HTML表單樣式的那... 摘要:若你想在諸如元素的組件上應(yīng)用高級樣式或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。
系列文章說明
原文
在許多情況下,[可用的HTML表單組件]()是不夠的。若你想在諸如元素的組件上[應(yīng)用高級樣式]()、或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。
我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。為達到目的,我們選擇重構(gòu)元素作為例子... 摘要:若你想在諸如元素的組件上應(yīng)用高級樣式或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。
系列文章說明
原文
在許多情況下,[可用的HTML表單組件]()是不夠的。若你想在諸如元素的組件上[應(yīng)用高級樣式]()、或者想定制組件的行為,你就只能選擇創(chuàng)建自己的表單組件。
我們將通過本文學(xué)習(xí)如何構(gòu)建一個表單組件。為達到目的,我們選擇重構(gòu)元素作為例子... 摘要:當(dāng)你構(gòu)建表單時,可以試著聽一下屏幕閱讀器如何讀取它,若聽起來很奇怪,那就有必要改進你的表單結(jié)構(gòu)了。該規(guī)則必須在表單頭部以保證在用戶找到必填元素之前,屏幕閱讀器等無障礙設(shè)備能將其展示或讀給用戶。
系列文章說明
原文
在建立HTML表單時,最重要的一件事就是如何用正確的方式構(gòu)建它。而之所以重要,原因有二:一是保證表單能被正確使用、二是這能保證你的表單是無障礙的(可以被能力不同的人使用)... 閱讀 1947·2021-11-25 09:43 閱讀 1441·2021-11-22 14:56 閱讀 3306·2021-11-22 09:34 閱讀 2050·2021-11-15 11:37 閱讀 2321·2021-09-01 10:46 閱讀 1428·2019-08-30 15:44 閱讀 2323·2019-08-30 13:15 閱讀 2418·2019-08-29 13:07元素對應(yīng)grid,
元素對應(yīng)list。因為我們的組件使用了
元素,所以得確保組件的listbox role能覆蓋掉
元素的list值。為此,可以使用presentation這個role值,該值用來指明一個沒有特殊含義的元素,而且該元素只用來展示信息而已。這里我們會給
應(yīng)用presentation值。
aria-selected特性
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]);
};
相關(guān)文章
【譯】HTML表單高級樣式
【譯】HTML表單高級樣式
【譯】怎樣創(chuàng)建定制表單組件
【譯】怎樣創(chuàng)建定制表單組件
【譯】怎么樣構(gòu)建HTML表單
發(fā)表評論
0條評論
wanghui
男|高級講師
TA的文章
閱讀更多
代理模式C++實現(xiàn)
Hadoop 入門筆記—核心組件 YARN
【C++】list詳解
Python爬蟲案例50篇-第8篇- 抓取某訊招聘的北京工作崗位
WPTao淘寶客插件配置淘寶聯(lián)盟和京東聯(lián)盟API信息
react知識點整理50問(未完待續(xù))
CSS背景與邊框
使用原生js實現(xiàn)輪播圖效果