現在のプロジェクトでは、特定のアクションで開くモーダル ペインがいくつかあります。そのモーダルペインが開いているときに、その外側の要素にタブで移動できないようにしようとしています。jQuery UIダイアログボックスとMalsup jQueryブロックプラグインはこれを行うようですが、私はその機能を1つだけ取得してプロジェクトに適用しようとしています.
タブ操作を無効にすべきではないという意見の人がいるのを見てきましたが、その見方はわかりますが、無効にするように指示されています。
現在のプロジェクトでは、特定のアクションで開くモーダル ペインがいくつかあります。そのモーダルペインが開いているときに、その外側の要素にタブで移動できないようにしようとしています。jQuery UIダイアログボックスとMalsup jQueryブロックプラグインはこれを行うようですが、私はその機能を1つだけ取得してプロジェクトに適用しようとしています.
タブ操作を無効にすべきではないという意見の人がいるのを見てきましたが、その見方はわかりますが、無効にするように指示されています。
モーダル ペインが開いているときにモーダル ペイン内の最初のフォーム要素にフォーカスを与え、モーダル ペイン内の最後のフォーム要素にフォーカスがあるときに Tab キーを押すと、これを少なくともある程度達成することができました。フォーカスは、そうでなければフォーカスを受け取る DOM 内の次の要素ではなく、最初のフォーム要素に戻ります。このスクリプトの多くはjQuery から来ています: How to capture the TAB keypress within a Textbox :
$('#confirmCopy :input:first').focus();
$('#confirmCopy :input:last').on('keydown', function (e) {
if ($("this:focus") && (e.which == 9)) {
e.preventDefault();
$('#confirmCopy :input:first').focus();
}
});
矢印キーなどの他のキーが押されたことを確認するには、これをさらに改良する必要があるかもしれませんが、基本的な考え方はそこにあります。
Christianとjfutchによる良い解決策。
タブのキーストロークをハイジャックすると、いくつかの落とし穴があることに注意してください。
:visible
リフローをトリガーするかどうかを確認しますより堅牢な解決策は、すべてのタブ可能なコンテンツで tabindex を -1 に設定してページの残りの部分を「非表示」にしてから、閉じるときに「再表示」することだと思います。これにより、モーダル ウィンドウ内のタブ オーダーが保持され、tabindex によって設定された順序が尊重されます。
var focusable_selector = 'a[href], area[href], input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
var hide_rest_of_dom = function( modal_selector ) {
var hide = [], hide_i, tabindex,
focusable = document.querySelectorAll( focusable_selector ),
focusable_i = focusable.length,
modal = document.querySelector( modal_selector ),
modal_focusable = modal.querySelectorAll( focusable_selector );
/*convert to array so we can use indexOf method*/
modal_focusable = Array.prototype.slice.call( modal_focusable );
/*push the container on to the array*/
modal_focusable.push( modal );
/*separate get attribute methods from set attribute methods*/
while( focusable_i-- ) {
/*dont hide if element is inside the modal*/
if ( modal_focusable.indexOf(focusable[focusable_i]) !== -1 ) {
continue;
}
/*add to hide array if tabindex is not negative*/
tabindex = parseInt(focusable[focusable_i].getAttribute('tabindex'));
if ( isNaN( tabindex ) ) {
hide.push([focusable[focusable_i],'inline']);
} else if ( tabindex >= 0 ) {
hide.push([focusable[focusable_i],tabindex]);
}
}
/*hide the dom elements*/
hide_i = hide.length;
while( hide_i-- ) {
hide[hide_i][0].setAttribute('data-tabindex',hide[hide_i][1]);
hide[hide_i][0].setAttribute('tabindex',-1);
}
};
dom を再表示するには、'data-tabindex' 属性を使用してすべての要素をクエリし、tabindex を属性値に設定します。
var unhide_dom = function() {
var unhide = [], unhide_i, data_tabindex,
hidden = document.querySelectorAll('[data-tabindex]'),
hidden_i = hidden.length;
/*separate the get and set attribute methods*/
while( hidden_i-- ) {
data_tabindex = hidden[hidden_i].getAttribute('data-tabindex');
if ( data_tabindex !== null ) {
unhide.push([hidden[hidden_i], (data_tabindex == 'inline') ? 0 : data_tabindex]);
}
}
/*unhide the dom elements*/
unhide_i = unhide.length;
while( unhide_i-- ) {
unhide[unhide_i][0].removeAttribute('data-tabindex');
unhide[unhide_i][0].setAttribute('tabindex', unhide[unhide_i][1] );
}
}
モーダルが開いているときに残りの dom を aria から非表示にする方が少し簡単です。モーダル ウィンドウのすべての関係を循環し、aria-hidden 属性を true に設定します。
var aria_hide_rest_of_dom = function( modal_selector ) {
var aria_hide = [],
aria_hide_i,
modal_relatives = [],
modal_ancestors = [],
modal_relatives_i,
ancestor_el,
sibling, hidden,
modal = document.querySelector( modal_selector );
/*get and separate the ancestors from the relatives of the modal*/
ancestor_el = modal;
while ( ancestor_el.nodeType === 1 ) {
modal_ancestors.push( ancestor_el );
sibling = ancestor_el.parentNode.firstChild;
for ( ; sibling ; sibling = sibling.nextSibling ) {
if ( sibling.nodeType === 1 && sibling !== ancestor_el ) {
modal_relatives.push( sibling );
}
}
ancestor_el = ancestor_el.parentNode;
}
/*filter out relatives that aren't already hidden*/
modal_relatives_i = modal_relatives.length;
while( modal_relatives_i-- ) {
hidden = modal_relatives[modal_relatives_i].getAttribute('aria-hidden');
if ( hidden === null || hidden === 'false' ) {
aria_hide.push([modal_relatives[modal_relatives_i]]);
}
}
/*hide the dom elements*/
aria_hide_i = aria_hide.length;
while( aria_hide_i-- ) {
aria_hide[aria_hide_i][0].setAttribute('data-ariahidden','false');
aria_hide[aria_hide_i][0].setAttribute('aria-hidden','true');
}
};
同様の手法を使用して、モーダルが閉じたときに aria dom 要素を再表示します。ここでは、 aria-hidden 属性を false に設定するよりも削除する方が良いでしょう。これは、優先される要素に競合する css 可視性/表示ルールが存在する可能性があるためです。そのような場合の aria-hidden の実装は、ブラウザー間で一貫性がありません ( https: //www.w3.org/TR/2016/WD-wai-aria-1.1-20160721/#aria-hidden )
var aria_unhide_dom = function() {
var unhide = [], unhide_i, data_ariahidden,
hidden = document.querySelectorAll('[data-ariahidden]'),
hidden_i = hidden.length;
/*separate the get and set attribute methods*/
while( hidden_i-- ) {
data_ariahidden = hidden[hidden_i].getAttribute('data-ariahidden');
if ( data_ariahidden !== null ) {
unhide.push(hidden[hidden_i]);
}
}
/*unhide the dom elements*/
unhide_i = unhide.length;
while( unhide_i-- ) {
unhide[unhide_i].removeAttribute('data-ariahidden');
unhide[unhide_i].removeAttribute('aria-hidden');
}
}
最後に、要素のアニメーションが終了した後にこれらの関数を呼び出すことをお勧めします。以下は、transition_end で関数を呼び出す抽象化された例です。
ロード時の遷移時間を検出するために modernizr を使用しています。transition_end イベントは dom をバブルアップして、モーダル ウィンドウが開いたときに複数の要素が遷移している場合に複数回起動できるようにするため、非表示の dom 関数を呼び出す前に event.target を確認してください。
/* this can be run on page load, abstracted from
* http://dbushell.com/2012/12/22/a-responsive-off-canvas-menu-with-css-transforms-and-transitions/
*/
var transition_prop = Modernizr.prefixed('transition'),
transition_end = (function() {
var props = {
'WebkitTransition' : 'webkitTransitionEnd',
'MozTransition' : 'transitionend',
'OTransition' : 'oTransitionEnd otransitionend',
'msTransition' : 'MSTransitionEnd',
'transition' : 'transitionend'
};
return props.hasOwnProperty(transition_prop) ? props[transition_prop] : false;
})();
/*i use something similar to this when the modal window is opened*/
var on_open_modal_window = function( modal_selector ) {
var modal = document.querySelector( modal_selector ),
duration = (transition_end && transition_prop) ? parseFloat(window.getComputedStyle(modal, '')[transition_prop + 'Duration']) : 0;
if ( duration > 0 ) {
$( document ).on( transition_end + '.modal-window', function(event) {
/*check if transition_end event is for the modal*/
if ( event && event.target === modal ) {
hide_rest_of_dom();
aria_hide_rest_of_dom();
/*remove event handler by namespace*/
$( document ).off( transition_end + '.modal-window');
}
} );
} else {
hide_rest_of_dom();
aria_hide_rest_of_dom();
}
}
私のように最近この問題に取り組んでいる人のために、私は上記で概説したアプローチを採用し、もう少し消化しやすいようにそれらを少し単純化しました。ここで提案されたアプローチについて@niall.campbellに感謝します。
以下のコードは、この CodeSandboxにあり、さらに参照したり、実際の例を確認したりできます。
let tabData = [];
const modal = document.getElementById('modal');
preventTabOutside(modal);
// should be called when modal opens
function preventTabOutside(modal) {
const tabbableElements = document.querySelectorAll(selector);
tabData = Array.from(tabbableElements)
// filter out any elements within the modal
.filter((elem) => !modal.contains(elem))
// store refs to the element and its original tabindex
.map((elem) => {
// capture original tab index, if it exists
const tabIndex = elem.hasAttribute("tabindex")
? elem.getAttribute("tabindex")
: null;
// temporarily set the tabindex to -1
elem.setAttribute("tabindex", -1);
return { elem, tabIndex };
});
}
// should be called when modal closes
function enableTabOutside() {
tabData.forEach(({ elem, tabIndex }) => {
if (tabIndex === null) {
elem.removeAttribute("tabindex");
} else {
elem.setAttribute("tabindex", tabIndex);
}
});
tabData = [];
}