javascript 埋め込み モーダル ペイン内でのみタブを保持する




jquery html 出力 (4)

私の現在のプロジェクトでは、いくつかのモーダルペインがあります。 私はそれを取得しようとしているので、そのモーダルペインが開いているときに、その外側の要素にタブすることはできません。 jQueryのUIダイアログボックスとMalsupのjQueryブロックプラグインはこれを行うようですが、私はその1つの機能を取得してプロジェクトに適用しようとしています。

私は、一部の人々は、タブでタブを無効にしてはならないと考えていることを見てきましたが、私はその視点を見ることができますが、無効にする指示が与えられています。


Answer #1

モーダルウィンドウが開いているときにモーダルウィンドウ内の最初のフォーム要素にフォーカスを置くことで、これを最終的に達成することができました。そして、モーダルウィンドウ内の最後のフォーム要素にフォーカスがある間にTabキーを押すと、フォーカスは、フォーカスを受け取るDOM内の次の要素ではなく、最初のフォーム要素に戻ります。 このスクリプトの多くは、 jQueryのテキストボックス内でTABキー押下をキャプチャする方法です:

$('#confirmCopy :input:first').focus();

$('#confirmCopy :input:last').on('keydown', function (e) { 
    if ($("this:focus") && (e.which == 9)) {
        e.preventDefault();
        $('#confirmCopy :input:first').focus();
    }
});

私は矢印キーのようないくつかの他のキーの押下を確認するためにこれをさらに精緻化する必要があるかもしれませんが、基本的な考え方はそこにあります。


Answer #2

クリスチャンとjfutchの良いソリューション。

タブキーを打ち負かすことでいくつかの落とし穴があることに言及する価値がある:

  • tabindex属性は、要素のdom順序がタブ順序に従わないように、モーダルペイン内のいくつかの要素に設定することができます。 (たとえば、最後のtabbable要素のtabindex = "10"を設定すると、タブ順で最初にタブリングすることができます)
  • ユーザーがモーダル以外の要素と対話してモーダルをトリガーしないようにすると、モーダルウィンドウの外にタブすることができます。 (たとえば、ロケーションバーをクリックしてページに戻るタブを開始するか、VoiceOverのようなスクリーンリーダーのページランドマークを開き、ページの別の部分に移動します)
  • 要素が:visibleかどうかをチェックすると、DOMが汚れている場合にリフローされます
  • ドキュメントには、フォーカスされた要素がない可能性があります。 クロムでは、非フォーカス要素をクリックしてタブを押すことで 'キャレット'の位置を変更することが可能です。 ユーザが最後のtabbable要素を越えてキャレット位置を設定できる可能性があります。

より堅牢な解決策は、tabindexをすべてのtabbableコンテンツで-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] ); 
    }

}

モーダルが開いているときに、残りのドームをアリアから隠しておくのはやや簡単です。 モーダルウィンドウのすべての親戚を繰り返し、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の優先順位と実装を取る要素のcssの可視性/表示規則が競合する可能性があるため、falseに設定するのではなく、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をバブルアップして、モーダルウィンドウが開いたときに複数の要素が遷移している場合は複数回起動できますので、hide dom関数を呼び出す前にev​​ent.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();
    }
}

Answer #3

これは、追加の入力タイプを追加し、シフト+タブも考慮に入れて、クリスチャンの答えを拡張するだけです。

var inputs = $element.find('select, input, textarea, button, a').filter(':visible');
var firstInput = inputs.first();
var lastInput = inputs.last();

/*set focus on first input*/
firstInput.focus();

/*redirect last tab to first input*/
lastInput.on('keydown', function (e) {
   if ((e.which === 9 && !e.shiftKey)) {
       e.preventDefault();
       firstInput.focus();
   }
});

/*redirect first shift+tab to last input*/
firstInput.on('keydown', function (e) {
    if ((e.which === 9 && e.shiftKey)) {
        e.preventDefault();
        lastInput.focus();
    }
});

Answer #4

私はAlexander Puchkovのsolutionに少し変更を加え、JQueryプラグインにしました。 これは、コンテナ内の動的DOM変更の問題を解決します。 条件付きでコンテナにコントロールを追加すると、これは機能します。

(function($) {

    $.fn.modalTabbing = function() {

        var tabbing = function(jqSelector) {
            var inputs = $(jqSelector).find('select, input, textarea, button, a[href]').filter(':visible').not(':disabled');

            //Focus to first element in the container.
            inputs.first().focus();

            $(jqSelector).on('keydown', function(e) {
                if (e.which === 9) {

                    var inputs = $(jqSelector).find('select, input, textarea, button, a[href]').filter(':visible').not(':disabled');

                    /*redirect last tab to first input*/
                    if (!e.shiftKey) {
                        if (inputs[inputs.length - 1] === e.target) {
                            e.preventDefault();
                            inputs.first().focus();
                        }
                    }
                    /*redirect first shift+tab to last input*/
                    else {
                        if (inputs[0] === e.target) {
                            e.preventDefault();
                            inputs.last().focus();
                        }
                    }
                }
            });
        };

        return this.each(function() {
            tabbing(this);
        });

    };
})(jQuery);




html