在我当前的项目中,我们有一些模式窗格可以打开某些操作。我正在尝试获取它,以便当该模式窗格打开时,您无法使用选项卡转到它之外的元素。jQuery UI 对话框和 Malsup jQuery 块插件似乎可以做到这一点,但我试图只获得一个功能并将其应用到我的项目中,但对我来说,他们是如何做到这一点的并不是很明显。
我已经看到有些人认为不应禁用标签,我可以看到这种观点,但我得到了禁用它的指令。
在我当前的项目中,我们有一些模式窗格可以打开某些操作。我正在尝试获取它,以便当该模式窗格打开时,您无法使用选项卡转到它之外的元素。jQuery UI 对话框和 Malsup jQuery 块插件似乎可以做到这一点,但我试图只获得一个功能并将其应用到我的项目中,但对我来说,他们是如何做到这一点的并不是很明显。
我已经看到有些人认为不应禁用标签,我可以看到这种观点,但我得到了禁用它的指令。
这只是通过添加其他输入类型并考虑 shift+tab 来扩展基督教答案。
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();
}
});
我终于能够通过在模式窗格打开时将焦点放在模式窗格中的第一个表单元素上,然后如果在焦点位于模式窗格中的最后一个表单元素上时按下 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 的好解决方案。
值得一提的是,劫持 Tab 键击存在一些陷阱:
:visible
如果dom 脏了,检查元素是否会触发重排我认为更强大的解决方案是通过将所有可选项卡内容的 tabindex 设置为 -1 来“隐藏”页面的其余部分,然后在关闭时“取消隐藏”。这将在模态窗口内保持 tab 顺序并遵守 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();
}
}
我刚刚对 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);
对于像我一样最近进入这个领域的任何人,我已经采用了上面概述的方法,并且我对它们进行了一些简化以使其更易于消化。感谢@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 = [];
}