.is-disabled(class属性)ではなく:disabledまたはaria-disabledを使おう!
UI開発者 寒川この記事はミツエーリンクスアドベントカレンダー2019 - Qiitaの16日目の記事です。
突然ですが、みなさんはボタンなどを非活性状態にする場合はどのように行っていますか?
button
要素の場合はdisabled
属性で以下のようにします。
<button type="button" disabled>メニュー</button>
上記のようにマークアップするとTab
キー操作によるフォーカスはされなくなり、スクリーンリーダーでは「ボタン使用不能 メニュー」や「メニュー 無効 ボタン」といった形で読み上げられ、非活性状態であることを伝えることができます。
では、disabled
属性を使えない要素の場合はどのように対処すべきでしょうか。
以下はa
要素でよく見かける例ですが、class
属性を付与して対処していませんか?
<a href="#menu" class="is-disabled">メニュー</a>
上記のようにマークアップするとTab
キー操作によるフォーカスができてしまいます。加えてスクリーンリーダーでは「メニュー リンク」と読み上げられてしまい、視覚的表現以外ではリンクが非活性状態であることを伝えることができません。
これではユーザーによって「button
要素にdisabled
属性を設定した」場合と伝わり方が異なってしまうので、あまりよい方法とは言えないでしょう。
そこで、本記事では非活性状態であることを伝えるためのよりよい方法をご紹介します。
a要素をプレースホルダ化する
a
要素はhref
属性を設定することでハイパーリンクになりますが、href
属性を設定しない場合はプレースホルダとなります。
<a>メニュー</a>
上記のようにマークアップするとTab
キー操作によるフォーカスはされなくなり、スクリーンリーダーでは「メニュー」と読み上げられ、リンクとは伝えなくなります。
プレースホルダ化はhref
属性を削除しても支障がない場合やJavaScriptイベントが実装されていない場合に活用できるのではないでしょうか。
aria-disabled属性を設定する
href
属性を削除すると都合が悪い場合やJavaScriptイベントに対して非活性処理を実装したい場合は、aria-disabled
属性を用いる方法で対処しましょう。
まずはaria-disabled
属性の大まかな紹介です。
- 編集や操作ができないことを示します。
aria-disabled
属性を設定した要素およびその子孫にあたるフォーカス可能な要素に適応されます。disabled
属性が使用できない要素に対して使用してください。- 非活性処理自体はJavaScriptで実装する必要があります。
より詳しくは以下のドキュメントを参照してください。
主に以下のような場面で使用します。
- WAI-ARIAを利用してフォーム系の要素を自作する場合
- 上記以外にも(基本的には
button
要素を使いますが)role="button"
やrole="tab"
などを使用している場合 - スムーススクロール機能やダイアログ機能の起動元に
a
要素を使用している場合
基本的にaria-disabled
属性はtrue
を設定して非活性状態を示し、解除する場合には属性自体を取り除いてしまいましょう。同時に非活性状態の場合はTab
キー操作によるフォーカスがされないようにtabindex
属性に-1
を設定します。
それでは実際にマークアップを書いてみます。
<a href="#menu" aria-disabled="true" tabindex="-1">メニュー</a>
上記のようにマークアップするとTab
キー操作によるフォーカスはされなくなり、スクリーンリーダーでは「使用不能リンク メニュー」や「メニュー 無効 リンク」といった形で読み上げられ、非活性状態であることを伝えることができます。
非活性処理を実装する
マークアップは書けましたが、このままではハイパーリンクやJavaScriptイベントが動作してしまうので、非活性用の処理をJavaScriptで実装します。
以下はクリックした場合に「要素が非活性状態か確認した」例になります。
elem.addEventListener('click', (event) => {
const btn = event.currentTarget;
// 対象が非活性状態か確認
if (btn.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
event.stopPropagation();
return;
}
});
こうすることでaria-disabled
属性がtrue
で設定されている場合はイベントが実行されなくなりました。
JavaScriptで使いやすくする
disabled
属性は使用可能な場面と使い分けが必要です。
disabled
属性が設定可能な要素は主に以下の要素になります。
button
fieldset
input
link[rel="stylesheet"]
option
optgroup
select
textarea
要素に応じて非活性用の属性を適切に設定できるように、「非活性を設定」「非活性を解除」「要素が非活性状態か確認する」3つの関数を用意します。
((doc) => {
'use strict';
const ARIA_DISABLED_ELEMS = 'a[href], area[href], [aria-controls], [role="button"], [role="tab"]';
const DISABLED_ELEMS = 'button, fieldset, input, link[rel="stylesheet"], option, optgroup, select, textarea';
/**
* 非活性を設定
*
* @param {Object} elem 対象の要素
* @return {void}
* @example
* addDisabled(elem);
*/
const addDisabled = (elem) => {
if (elem.matches(DISABLED_ELEMS)) {
elem.disabled = true;
} else if (elem.matches(ARIA_DISABLED_ELEMS)) {
elem.setAttribute('aria-disabled', 'true');
}
};
/**
* 非活性を解除
*
* @param {Object} elem 対象の要素
* @return {void}
* @example
* removeDisabled(elem);
*/
const removeDisabled = (elem) => {
if (elem.matches(DISABLED_ELEMS)) {
elem.disabled = false;
} else if (elem.matches(ARIA_DISABLED_ELEMS)) {
elem.removeAttribute('aria-disabled');
}
};
/**
* 要素が非活性状態か確認する
*
* @param {Object} elem 確認対象の要素
* @return {Boolean} 非活性状態かの判定結果
* @example
* if (isDisabled(elem)) {
* // ...
* }
*/
const isDisabled = (elem) => {
if (
elem.matches(DISABLED_ELEMS) &&
elem.disabled
) {
return true;
} else if (
elem.matches(ARIA_DISABLED_ELEMS) &&
elem.getAttribute('aria-disabled') === 'true'
) {
return true;
}
return false;
};
})(window.document);
aria-disabled
属性を設定したい要素を定数ARIA_DISABLED_ELEMS
として宣言しておきます。disabled
属性が設定可能な要素を定数DISABLED_ELEMS
として宣言しておきます。- 非活性を設定する際は、
addDisabled(elem);
のようにすることでJavaScriptが対象要素を判定してdisabled
またはaria-disabled
のどちらか適切な属性を設定します。 - 非活性を解除する際は、
removeDisabled(elem);
のようにすることでJavaScriptが対象要素を判定してdisabled
またはaria-disabled
の属性を取り除きます。 - 要素が非活性状態か確認する場合は、
if (isDisabled(elem)) {
のようにすることでJavaScriptが対象要素を判定可能になります。
以下はクリックした場合に「要素が非活性状態か確認する」関数を使用した例になります。
elem.addEventListener('click', (event) => {
const btn = event.currentTarget;
// 対象が非活性状態か確認
if (isDisabled(btn)) {
event.preventDefault();
event.stopPropagation();
return;
}
});
これでdisabled
属性とaria-disabled
属性の使い分けができるようになりました。
aria-disabled属性を監視してtabindex属性を設定する
先ほど紹介した「非活性を設定」と「非活性を解除」の関数にはあえてtabindex
属性の処理を書きませんでした。
aria-disabled
属性を静的に設定した場合にtabindex
属性を付け忘れるかもしれないので、MutationObserver
を使って対象の要素を監視しaria-disabled
属性の有無に合わせてtabindex
属性の設定する処理を書いてみます。
MutationObserver
についてはObserverを使って要素を監視してみよう! | フロントエンドBlog | ミツエーリンクスを参考にしてみてください。
((doc) => {
'use strict';
/**
* 指定した要素および属性を監視して、aria-disabled属性に対するtabindexを制御する機能
*
* @param {String} selectors querySelectorAll() に指定する値
* @param {Object} options 設定
* @return {void}
*/
const tabindexToAriaDisabled = (selectors, options) => {
const targets = doc.querySelectorAll(selectors);
if (targets.length <= 0) {
return;
}
const config = Object.assign({
customDataAttrSuffixName: 'keep-tabindex' // tabindexを保持しておくためのdata-*属性
}, options);
const observer = new MutationObserver((mutations) => {
mutations.forEach((m) => {
const elem = m.target;
const watchAttrName = m.attributeName;
if (watchAttrName === 'aria-disabled') {
if (elem.getAttribute('aria-disabled') === 'true') {
elem.tabIndex = -1;
} else {
const keepTabindex = elem.getAttribute('data-' + config.customDataAttrSuffixName);
// data-keep-tabindex属性に保管しておいたtabindex属性値が適切な値(-1か0以上の整数)か確認する
if (/^(-1|\d+)$/.test(keepTabindex)) {
elem.tabIndex = keepTabindex;
} else {
elem.removeAttribute('tabindex');
}
}
}
if (watchAttrName === 'tabindex') {
if (elem.getAttribute('aria-disabled') !== 'true') {
// tabindex属性が更新された場合に、設定されているaria-disabled属性値が'true'ではない場合はdata-keep-tabindex属性に保管し直す
elem.setAttribute('data-' + config.customDataAttrSuffixName, elem.getAttribute('tabindex'));
}
}
});
});
targets.forEach((elem) => {
const isAriDisabledTrue = elem.getAttribute('aria-disabled') === 'true';
// 既存のtabindex属性をdata-keep-tabindex属性に保管しておく
elem.setAttribute('data-' + config.customDataAttrSuffixName, isAriDisabledTrue ? null : elem.getAttribute('tabindex'));
observer.observe(elem, {
attributes: true,
attributeFilter: ['aria-disabled', 'tabindex'] // 監視対象となる属性リスト
});
if (isAriDisabledTrue) {
elem.tabIndex = -1;
}
});
};
// 実行
doc.addEventListener('DOMContentLoaded', () => {
tabindexToAriaDisabled('[href*="#"], [role="button"]');
});
})(window.document);
tabindexToAriaDisabled();
の引数に指定したセレクタを監視対象にします。- 監視を開始する前に、ページ読み込みまでに監視対象に設定されてた
tabindex
属性値をdata-keep-tabindex
属性に保管しておきます。存在しない場合はnull
を設定します。 - その後、引数に指定したセレクタに加え
attributeFilter
に指定した属性(aria-disabled
とtabindex
)を対象に監視を開始します。 - ページ読み込みまでに
aria-disabled
がtrue
に設定されている対象にMutationObserver
がtabindex="-1"
を設定します。 - ページ読み込み後は、関数での処理や開発者ツールで設定した
aria-disabled
属性に合わせてtabindex
属性が自動で設定されます。
以下が実際に動作した例になります。
元からtabindex属性が存在していた場合は以下のように動作します。
今回の監視処理はあくまでaria-disabled
属性を設定した要素のみにしかtabindex
属性を設定できません。子孫要素のフォーカス可能な要素に対して行いたい場合はまた別の処理が必要になります。
まとめ
- 非活性状態にする場合は、見た目だけではなくその振る舞いや意味も伝えられるようにHTMLをマークアップしましょう。
- そのために
disabled
属性を使いましょう。 disabled
属性が使えない要素の場合はaria-disabled
属性を使いましょう。- JavaScriptで工夫すれば、たくさんある対象要素を分類し適切な処理が実現できるでしょう。
a
要素のプレースホルダ化で済ませられる場合は低コストで対処できるので、選択肢の1つになるのではないでしょうか。- 対象がJavaScriptに依存するかどうかが使い分けのポイントになるでしょう。
以上になります。
早いもので、今年も残すところあと半月になりましたね。それではよい年の瀬をお過ごしください。