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