Angular
とSemantic-UI
を使用してアプリを開発しています。アプリはアクセス可能である必要があります。つまり、WCAG 2.0に準拠している必要があります。この目的を達成するために、モーダルはダイアログ内でフォーカスを維持し、ユーザーが外に出たり、モーダルの下にあるページの要素間の「タブ」で移動したりすることを防ぎます。
次のような実用的な例を見つけました。
dialog
HTML 5.1要素: https://demo.agektmr.com/dialogSemantic-UIを使用してアクセス可能なモーダルを作成する私の試みは次のとおりです。 https://plnkr.co/edit/HjhkZg
ご覧のとおり、次の属性を使用しました。
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
しかし、彼らは私の問題を解決しません。ユーザーがキャンセル/確認ボタンをクリックしたときにのみ私のモーダルがフォーカスを維持し、それを失う方法を知っていますか?
現在、これを実現する簡単な方法はありません。 不活性属性 は、属性を持つ要素とそのすべての子にアクセスできないようにすることでこの問題を解決しようとするために提案されました。しかし、採用は遅く、つい最近になりました Chrome Canary in a flag 。
別の提案された解決策は モーダルスタックを追跡するネイティブAPIを作成する であり、本質的に現在スタックの最上位ではないすべてのものを不活性にします。提案のステータスはわかりませんが、すぐに実装されるとは思えません。
残念ながら、良い解決策はありません。人気のあるソリューションの1つは、 既知のすべてのフォーカス可能な要素のクエリセレクターを作成する で、モーダルの最後と最初の要素にkeydownイベントを追加して、モーダルにフォーカスをトラップすることです。ただし、WebコンポーネントとシャドウDOMの台頭により、このソリューションは すべてのフォーカス可能な要素を見つけることができない になります。
ダイアログ内のすべての要素を常に制御している場合(そして汎用ダイアログライブラリを作成していない場合)、おそらく最も簡単な方法は、フォーカス可能な最初と最後の要素にキーダウン用のイベントリスナーを追加することです。シフトタブが使用され、フォーカスをトラップするために最初または最後の要素にフォーカスしました。
汎用ダイアログライブラリを作成している場合、合理的に機能することがわかった唯一のことは、不活性ポリフィルを使用するか、モーダル以外のすべてにtabindex=-1
。
var nonModalNodes;
function openDialog() {
var modalNodes = Array.from( document.querySelectorAll('dialog *') );
// by only finding elements that do not have tabindex="-1" we ensure we don't
// corrupt the previous state of the element if a modal was already open
nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (!modalNodes.includes(node)) {
// save the previous tabindex state so we can restore it on close
node._prevTabindex = node.getAttribute('tabindex');
node.setAttribute('tabindex', -1);
// tabindex=-1 does not prevent the mouse from focusing the node (which
// would show a focus outline around the element). prevent this by disabling
// outline styles while the modal is open
// @see https://www.sitepoint.com/when-do-elements-take-the-focus/
node.style.outline = 'none';
}
}
}
function closeDialog() {
// close the modal and restore tabindex
if (this.type === 'modal') {
document.body.style.overflow = null;
// restore or remove tabindex from nodes
for (var i = 0; i < nonModalNodes.length; i++) {
var node = nonModalNodes[i];
if (node._prevTabindex) {
node.setAttribute('tabindex', node._prevTabindex);
node._prevTabindex = null;
}
else {
node.removeAttribute('tabindex');
}
node.style.outline = null;
}
}
}
異なる「実際の例」は、スクリーンリーダーでは期待どおりに機能しません。
彼らは、モーダル内のスクリーンリーダーの視覚的なフォーカスをトラップしません。
これを機能させるには、次のことを行う必要があります。
aria-hidden
他のノードの属性それらのツリー内のキーボードフォーカス可能な要素を無効にします(tabindex=-1
、disabled
を使用して制御、...)
:focusable
疑似セレクターは、フォーカス可能な要素を見つけるのに役立ちます。ページ上に透明レイヤーを追加して、マウス選択を無効にします。
pointer-events: none
プロパティは、ブラウザがIEではなく、SVG以外の要素で処理した場合このフォーカストラッププラグイン は、ダイアログ要素内にフォーカスが閉じ込められていることを確認するのに優れています。
問題は2つのカテゴリに分類できるようです。
メインコンテナに-1のtabindexを追加します。これは、role = "dialog"を持つDOM要素です。コンテナーにフォーカスを設定します。
ダイアログボックス内のタブ可能な要素を取得して、キーダウンでそれをリッスンする以外に、これを行う他の方法は見つかりませんでした。フォーカスされている要素(document.activeElement)がリストの最後の要素であることを確認したら、ラップします
「タブ可能な」要素の検索を要求するソリューションは使用しないでください。代わりに、keydown
とclick
イベントまたはbackdropを効果的なマナーで使用します。
(Angular1)
以下で私がしようとしているものと同様のものについては、 https://stackoverflow.com/a/31292097/1754995 にあるAsheesh Kumarの回答を参照してください。
(Angular2-x、Angular1をしばらく実行していません)
3つのコンポーネントがあるとします:BackdropComponent、ModalComponent(入力があります)、AppComponent(入力、BackdropComponent、ModalComponentがあります)。正しいz-indexでBackdropComponentとModalComponentを表示すると、どちらも現在表示/表示されています。
あなたがする必要があるのは、背景/モーダルコンポーネントが表示されているときにすべてのタブ移動を停止するためのpreventDefault()
を伴う一般的な_window.keydown
_イベントを用意することです。 BackdropComponentに配置することをお勧めします。次に、ModalComponentのタブ移動を処理するstopPropagation()
を含む_keydown.tab
_イベントが必要です。 _window.keydown
_と_keydown.tab
_の両方がModalComponentに存在する可能性がありますが、BackdropComponentにはモーダルだけではなく目的があります。
これにより、AppComponent入力へのクリックおよびタブ移動が防止され、モーダルが表示されている場合にのみ、ModalComponent入力(およびブラウザの要素)をクリックまたはタブ移動します。
背景を使用してクリックを防止したくない場合は、上記のclick
イベントと同様にkeydown
イベントを使用できます。
背景コンポーネント:
_@Component({
selector: 'my-backdrop',
Host: {
'tabindex': '-1',
'(window:keydown)': 'preventTabbing($event)'
},
...
})
export class BackdropComponent {
...
private preventTabbing(event: KeyboardEvent) {
if (event.keyCode === 9) { // && backdrop shown?
event.preventDefault();
}
}
...
}
_
モーダルコンポーネント:
_@Component({
selector: 'my-modal',
Host: {
'tabindex': '-1',
'(keydown.tab)': 'onTab($event)'
},
...
})
export class ModalComponent {
...
private onTab(event: KeyboardEvent) {
event.stopPropagation();
}
...
}
_
これが私の解決策です。必要に応じて、モーダルダイアログの最初/最後の要素でTabまたはShift + Tabをトラップします(私の場合、role="dialog"
)。チェック対象の要素はすべて、HTMLがinput,select,textarea,button
。
$(document).on('keydown', function(e) {
var target = e.target;
var shiftPressed = e.shiftKey;
// If TAB key pressed
if (e.keyCode == 9) {
// If inside a Modal dialog (determined by attribute role="dialog")
if ($(target).parents('[role=dialog]').length) {
// Find first or last input element in the dialog parent (depending on whether Shift was pressed).
// Input elements must be visible, and can be Input/Select/Button/Textarea.
var borderElem = shiftPressed ?
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').first()
:
$(target).closest('[role=dialog]').find('input:visible,select:visible,button:visible,textarea:visible').last();
if ($(borderElem).length) {
if ($(target).is($(borderElem))) {
return false;
} else {
return true;
}
}
}
}
return true;
});
Steven Lambertが提案した方法の1つを使用しました。つまり、キーダウンイベントをリッスンし、「タブ」キーと「Shift +タブ」キーをインターセプトしました。これが私のサンプルコードです(Angular 5):
import { Directive, ElementRef, Attribute, HostListener, OnInit } from '@angular/core';
/**
* This directive allows to override default tab order for page controls.
* Particularly useful for working around the modal dialog TAB issue
* (when tab key allows to move focus outside of dialog).
*
* Usage: add "custom-taborder" and "tab-next='next_control'"/"tab-prev='prev_control'" attributes
* to the first and last controls of the dialog.
*
* For example, the first control is <input type="text" name="ctlName">
* and the last one is <button type="submit" name="btnOk">
*
* You should modify the above declarations as follows:
* <input type="text" name="ctlName" custom-taborder tab-prev="btnOk">
* <button type="submit" name="btnOk" custom-taborder tab-next="ctlName">
*/
@Directive({
selector: '[custom-taborder]'
})
export class CustomTabOrderDirective {
private elem: HTMLInputElement;
private nextElemName: string;
private prevElemName: string;
private nextElem: HTMLElement;
private prevElem: HTMLElement;
constructor(
private elemRef: ElementRef
, @Attribute('tab-next') public tabNext: string
, @Attribute('tab-prev') public tabPrev: string
) {
this.elem = this.elemRef.nativeElement;
this.nextElemName = tabNext;
this.prevElemName = tabPrev;
}
ngOnInit() {
if (this.nextElemName) {
var elems = document.getElementsByName(this.nextElemName);
if (elems && elems.length && elems.length > 0)
this.nextElem = elems[0];
}
if (this.prevElemName) {
var elems = document.getElementsByName(this.prevElemName);
if (elems && elems.length && elems.length > 0)
this.prevElem = elems[0];
}
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
if (event.key !== "Tab")
return;
if (!event.shiftKey && this.nextElem) {
this.nextElem.focus();
event.preventDefault();
}
if (event.shiftKey && this.prevElem) {
this.prevElem.focus();
event.preventDefault();
}
}
}
このディレクティブを使用するには、モジュールにインポートして宣言セクションに追加するだけです。