Javascript 仅在模态窗格中保持 Tab 键

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14572084/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-24 17:14:51  来源:igfitidea点击:

Keep tabbing within modal pane only

javascriptjqueryhtml

提问by Christian Ziebarth

On my current project we have some modal panes that open up on certain actions. I am trying to get it so that when that modal pane is open you can't tab to an element outside of it. The jQuery UI dialog boxes and the Malsup jQuery block plugins seem to do this but I am trying to get just that one feature and apply it in my project and it's not immediately obvious to me how they are doing that.

在我当前的项目中,我们有一些模式窗格可以在某些操作上打开。我正在尝试获取它,以便当该模式窗格打开时,您无法切换到它之外的元素。jQuery UI 对话框和 Malsup jQuery 块插件似乎可以做到这一点,但我正试图获得该功能并将其应用到我的项目中,但我并不清楚他们是如何做到这一点的。

I've seen that some people are of the opinion that tabbing shouldn't be disabled and I can see that point of view but I am being given the directive to disable it.

我已经看到有些人认为不应禁用制表符,我可以看到这种观点,但我得到了禁用它的指令。

回答by jfutch

This is just expanding on Christian answer, by adding the additional input types and also taking into consideration the shift+tab.

这只是通过添加额外的输入类型并考虑到 shift+tab 来扩展 Christian 的答案。

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

回答by Christian Ziebarth

I was finally able to accomplish this at least somewhat by giving focus to the first form element within the modal pane when that modal pane is open and then if the Tab key is pressed while focus is on the last form element within the modal pane then the focus goes back to the first form element there rather than to the next element in the DOM that would otherwise receive focus. A lot of this scripting comes from jQuery: How to capture the TAB keypress within a Textbox:

通过在模式窗格打开时将焦点放在模态窗格中的第一个表单元素上,然后如果在焦点位于模态窗格中的最后一个表单元素时按下 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();
    }
});

I may need to further refine this to check for the pressing of some other keys, such as arrow keys, but the basic idea is there.

我可能需要进一步改进它以检查是否按下了其他一些键,例如箭头键,但基本思想就在那里。

回答by niall.campbell

Good solutions by Christian and jfutch.

Christian 和 jfutch 的好解决方案。

Its worth mentioning that there a few pitfalls with hiHymaning the tab keystroke:

值得一提的是,劫持 tab 键有一些陷阱:

  • the tabindex attribute might be set on some elements inside the modal pane in such a way that the dom order of elements does not follow the tab order. (Eg. setting tabindex="10" on the last tabbable element can make it first in the tab order)
  • If the user interacts with an element outside the modal that doesn't trigger the modal to close you can tab outside the modal window. (Eg. click the location bar and start tabbing back to the page, or open up page landmarks in a screenreader like VoiceOver & navigate to a different part of the page)
  • checking if elements are :visiblewill trigger a reflow if the dom is dirty
  • The document might not have a :focussed element. In chrome its possible to change the 'caret' position by clicking on a non-focussable element then pressing tab. Its possible that the user could set the caret position past the last tabbable element.
  • tabindex 属性可能会设置在模式窗格内的某些元素上,这样元素的 dom 顺序就不会遵循 Tab 键顺序。(例如,在最后一个 tabbable 元素上设置 tabindex="10" 可以使其在 Tab 键顺序中排在第一位)
  • 如果用户与不会触发模态关闭的模态外的元素进行交互,您可以在模态窗口外进行选项卡。(例如,单击位置栏并开始返回页面,或在诸如 VoiceOver 之类的屏幕阅读器中打开页面地标并导航到页面的不同部分)
  • 如果:visibledom是脏的,检查元素是否会触发回流
  • 该文档可能没有 :focussed 元素。在 chrome 中,可以通过单击不可聚焦的元素然后按 Tab 来更改“插入符号”的位置。用户可能会设置插入符号位置超过最后一个可选项卡元素。

I think a more robust solution would be to 'hide' the rest of the page by setting tabindex to -1 on all tabbable content, then 'unhide' on close. This will keep the tab order inside the modal window and respect the order set by tabindex.

我认为更强大的解决方案是通过在所有可选项卡内容上将 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);
    }

};

To unhide the dom you would just query all elements with the 'data-tabindex' attribute & set the tabindex to the attribute value.

要取消隐藏 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] ); 
    }

}

Making the rest of the dom hidden from aria when the modal is open is slightly easier. Cycle through all the relatives of the modal window & set the aria-hidden attribute to true.

当模态打开时,让 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');

    }       

};

Use a similar technique to unhide the aria dom elements when the modal closes. Here its better to remove the aria-hidden attribute rather than setting it to false as there might be some conflicting css visibility/display rules on the element that take precedence & implementation of aria-hidden in such cases is inconsistent across browsers (see https://www.w3.org/TR/2016/WD-wai-aria-1.1-20160721/#aria-hidden)

当模态关闭时,使用类似的技术取消隐藏 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');
    }

}

Lastly I'd recommend calling these functions after an animation has ended on the element. Below is a abstracted example of calling the functions on transition_end.

最后,我建议在元素上的动画结束后调用这些函数。下面是在 transition_end 上调用函数的抽象示例。

I'm using modernizr to detect the transition duration on load. The transition_end event bubbles up the dom so it can fire more than once if more than one element is transitioning when the modal window opens, so check against the event.target before calling the hide dom functions.

我正在使用 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();
    }
}

回答by Rajesh Jinaga

I have just made few changes to Alexander Puchkov's solution, and made it a JQuery plugin. It solves the problem of dynamic DOM changes in the container. If any control add it to the container on conditional, this works.

我刚刚对 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);