Javascript 仅在模式窗格中保留选项卡

Javascript 仅在模式窗格中保留选项卡,javascript,jquery,html,Javascript,Jquery,Html,在我当前的项目中,我们有一些模式窗格,可以打开某些操作。我正在尝试获取它,这样当模式窗格打开时,您就不能用tab键指向它之外的元素。jQueryUI对话框和Malsup jQuery块插件似乎可以做到这一点,但我正试图获得这一功能并将其应用到我的项目中,我并不清楚它们是如何做到这一点的 我看到一些人认为不应该禁用tabing,我也看到了这一观点,但我得到了禁用它的指令。我终于能够做到这一点,至少在模式窗格打开时,通过将焦点放在模式窗格中的第一个表单元素上,然后如果Tab键打开,我就可以做到这一点

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


我看到一些人认为不应该禁用tabing,我也看到了这一观点,但我得到了禁用它的指令。

我终于能够做到这一点,至少在模式窗格打开时,通过将焦点放在模式窗格中的第一个表单元素上,然后如果Tab键打开,我就可以做到这一点当焦点位于模式窗格中的最后一个表单元素上时,按下该键,则焦点将返回到那里的第一个表单元素,而不是DOM中接收焦点的下一个元素。这种脚本的大部分来自:


我可能需要进一步完善此功能,以检查是否按了其他一些键,如箭头键,但基本思想已经存在。

这只是在Christian answer上进行扩展,添加了其他输入类型,并考虑了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();
    }
});

Christian和jfutch的优秀解决方案

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

  • tabindex属性可以在模式窗格中的某些元素上设置,以使元素的dom顺序不遵循tab顺序。(例如,在最后一个选项卡元素上设置tabindex=“10”可以使其在选项卡顺序中位于第一位)
  • 如果用户与未触发模态关闭的模态外部的元素交互,则可以在模态窗口外部创建选项卡。(例如,单击位置栏并开始切换回页面,或在屏幕阅读器(如VoiceOver)中打开页面标记并导航到页面的其他部分)
  • 如果dom变脏,检查元素是否可见将触发回流
  • 文档可能没有:focused元素。在chrome中,可以通过单击不可聚焦的元素,然后按tab键来更改“插入符号”的位置。用户可能会设置插入符号位置,使其超过最后一个可选项卡元素
我认为一个更强大的解决方案是通过在所有可选项卡内容上将tabindex设置为-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隐藏dom的其余部分稍微容易一些。循环浏览所有 模态窗口的相关项&将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的实现 在这种情况下,浏览器之间不一致(请参阅)

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

我正在使用Modernizer来检测加载时的转换持续时间。过渡结束事件出现气泡 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为我们推荐的方法

以下代码可在中找到,以供进一步参考和工作示例

让tabData=[];
const modal=document.getElementById('modal');
外部(模态);
//应在模态打开时调用
外部功能(模态){
const tabbablelements=document.querySelectorAll(选择器);
tabData=Array.from(tabbablelements)
//过滤掉模态中的任何元素
.filter((elem)=>!modal.contains(elem))
//存储对元素及其原始选项卡索引的引用
.map((元素)=>{
//捕获原始选项卡索引(如果存在)
常量tabIndex=elem.hasAttribute(“tabIndex”)
?元素getAttribute(“tabindex”)
:null;
//暂时将tabindex设置为-1
元素setAttribute(“tabindex”,-1);
返回{elem,tabIndex};
});
}
//应在modal关闭时调用
函数enableTabOutside(){
tabData.forEach({elem,tabIndex})=>{
if(tabIndex==null){
元素删除属性(“tabindex”);
}否则{
元素setAttribute(“tabindex”,tabindex);
}
});
tabData=[];
}

你认为你可以使用
:tabbable
完美!正是我需要的!如果最后一个元素,即按钮,一开始被禁用了,我该怎么办?列表末尾的禁用元素将破坏此解决方案。如果禁用了
lastInput
,则按倒数第二个元素上的tab键将跳出模式。使用
:tabbable
收集tabbable将通过排除禁用的按钮来解决这一问题,但一旦启用按钮,您必须执行其他逻辑,否则用户将无法使用tab。Vanilla JS解决方案:谢谢Rajesh!这很好,解决了我在动态DOM更改中遇到的问题
/* 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();
    }
}
(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);