Подтвердить что ты не робот

Сохранять табуляцию только в пределах модальной панели

В моем текущем проекте у нас есть некоторые модальные панели, которые открываются для определенных действий. Я пытаюсь получить его так, что, когда эта модальная панель открыта, вы не можете вставить элемент вне его. Диалоговые окна jQuery UI и плагины блока Malsup jQuery, похоже, делают это, но я пытаюсь получить только одну функцию и применить ее в своем проекте, и мне не сразу становится ясно, как они это делают.

Я видел, что некоторые люди считают, что табуляция не должна быть отключена, и я вижу эту точку зрения, но мне дается директива о ее отключении.

4b9b3361

Ответ 1

Это просто расширяет христианский ответ, добавляя дополнительные типы ввода, а также принимая во внимание сдвиг + вкладку.

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();
    }
});

Ответ 2

Наконец, я смог выполнить это, сосредоточившись на первом элементе формы в модальной панели, когда эта модальная панель открыта, а затем, если нажата клавиша 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();
    }
});

Мне может понадобиться дополнительно уточнить это, чтобы проверить нажатие некоторых других клавиш, таких как клавиши со стрелками, но основная идея есть.

Ответ 3

Хорошие решения от Christian и jfutch.

Стоит отметить, что есть несколько ловушек с захватом нажатия клавиши вкладок:

  • атрибут tabindex может быть установлен на некоторых элементах внутри модальной панели таким образом, чтобы dom порядок элементов не соответствовал порядку табуляции. (Например, установка tabindex = "10" в последнем элементе tabbable может сделать его первым в порядке табуляции)
  • Если пользователь взаимодействует с элементом вне модальности, который не запускает модальный, чтобы закрыть, вы можете заходить вне модального окна. (Например, нажмите на панель местоположения и начните переходить на страницу назад или откройте страницы в виде экрана, например VoiceOver, и перейдите к другой части страницы).
  • проверка наличия элементов :visible приведет к перепланировке, если dom загрязнен.
  • В документе может не быть элемента с фокусом. В хроме можно изменить положение "каретки", щелкнув на неприемлемом элементе, а затем нажимая вкладку. Возможно, пользователь может установить позицию каретки за последний элемент tabbable.

Я думаю, что более надежным решением было бы "скрыть" остальную страницу, установив tabindex на -1 во все содержимое tabbable, а затем "unhide" при закрытии. Это сохранит порядок вкладок внутри модального окна и будет уважать порядок, заданный 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] ); 
    }

}

Создание остальной части домика, скрытой от арии, когда модальный открыт, немного легче. Цикл через все  родственники модального окна и установите для атрибута, скрытого от арии, значение 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, когда модальная функция закрывается. Здесь его лучше для удаления атрибута, скрытого от арии, вместо того, чтобы устанавливать его в false, поскольку могут быть некоторые конфликтующие css правила видимости/отображения элемента, которые имеют приоритет и реализуют скрытую арию в таких случаях непоследователен в браузерах (см. 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');
    }

}

Наконец, я бы рекомендовал вызывать эти функции после того, как анимация закончилась над элементом. Ниже приведена абстрагированный пример вызова функций на странице перехода.

Я использую modernizr для определения продолжительности перехода при загрузке. Событие transition_end пузырится вверх dom, чтобы он мог срабатывать более одного раза, если более одного элемента переходит, когда модальные откроется окно, поэтому перед вызовом функций hide 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();
    }
}

Ответ 4

Я только что внес несколько изменений в решение Alexander Puchkov и сделал его плагином 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);