【譯】怎樣建立定製表單組件

在許多狀況下,[可用的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事件了。

定義HTML結構和語義

上面咱們肯定了組建的基本功能,如今能夠來構建咱們的組件了。第一步咱們要定義其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建立樣式和交互

如今咱們已經有了組件的結構了,接下來要來設計組件了。建立這個自定義組件的目的,是爲了用咱們想要的形式來給該組件添加樣式。要作到這點,咱們要把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讓組件「活」起來

如今咱們組件的結構和設計都已經作好,能夠來寫JavaScript代碼讓組件真正能運行起來了。

警告:下面的代碼是教學代碼,在實際編碼時不能直接像下面同樣使用。其中許多部分,並無將來使用的保障、並且也不能在老舊瀏覽器上使用。此外,這些代碼也有在生產環境中應該被優化掉的冗餘部分。

注意:建立可複用的組件是頗有技巧性的。W3C Web Component 草案是這個特定問題的一個解決方案。X-tag project是這一規範的實驗性實現;咱們鼓勵你好好了解下它。

爲何不起做用?

在開始以前,咱們須要知道JavaScript的一個嚴重問題:在瀏覽器裏,它是一個不可靠的技術。當你在建立自定義組件的時候,你不得不依賴JavaScript,由於它是把全部東西維繫在一塊兒的繩索。可是,在許多狀況下JavaScript並不能在瀏覽器中運行:

  • 用戶禁用了JavaScript:這已是個最不常見的狀況了,如今不多有人會禁用JavaScript。

  • 腳本沒有加載:這是最廣泛的狀況,特別是在網絡不太可靠的移動端。

  • 腳本有bug:你要常常考慮這一可能性。

  • 腳本和第三方腳本衝突了:使用了追蹤腳本或用戶自用的書籤時會發生這種狀況。

  • 腳本和瀏覽器拓展(如火狐的NoScript拓展或Chrome的NoScripts拓展)發生衝突、或受到干擾。

  • 用戶使用了老舊瀏覽器,而且你須要的一種特性不被支持:這一般發生在你用了很新的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庫)。

咱們計劃使用的特性以下(從風險最大到最安全排列):

  1. classList

  2. addEventListener

  3. forEach(不屬於DOM可是現代JavaScript的特性)

  4. querySelectorquerySelectorAll

除了上述特性的可用性,在開發以前仍存在一個問題。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的特性,用這些特性,咱們能夠更好地描述角色、狀態和屬性,讓咱們剛纔設計的元素變得像其盡力模仿的原生元素同樣。使用這些特性很簡單,下面咱們來試試。

role特性

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特性這種作法是必須的。

aria-selected特性

僅使用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]);
};

上述修改的最終效果以下(訪問該組件時使用無障礙技術,譬如NVDAVoiceOver,會有更好的體驗):

效果

結論

至此咱們已經瞭解了建立定製表單組件的全部基本知識,但如你所見,這麼作並不簡單,若是使用第三方庫的話會比本身從頭寫起更好、更簡單(固然除非你是想構建這樣一個庫)。

下面是你在本身開發以前應該參考下的庫:

若你想更進一步使用本例,爲讓其中的代碼變得通用和可複用,還要對代碼作一些改進。這個練習你能夠本身嘗試下,這裏有兩個提示:首先,全部函數的第一個參數都相同,這就意味着這些函數須要有同一個執行上下文,使用一個對象來共享執行上下文是很明智的。此外,代碼還得保證兼容,即代碼最好能在兼容不一樣Web標準的多種瀏覽器下運行。

相關文章
相關標籤/搜索