Сохранять табуляцию только в пределах модальной панели
В моем текущем проекте у нас есть некоторые модальные панели, которые открываются для определенных действий. Я пытаюсь получить его так, что, когда эта модальная панель открыта, вы не можете вставить элемент вне его. Диалоговые окна jQuery UI и плагины блока Malsup jQuery, похоже, делают это, но я пытаюсь получить только одну функцию и применить ее в своем проекте, и мне не сразу становится ясно, как они это делают.
Я видел, что некоторые люди считают, что табуляция не должна быть отключена, и я вижу эту точку зрения, но мне дается директива о ее отключении.
Ответы
Ответ 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);