仅在模态窗格中保持标签

时间:2013-01-28 22:03:24

标签: javascript jquery html

在我目前的项目中,我们有一些模式窗格可以打开某些操作。我试图得到它,以便当该模态窗格打开时,您不能选项卡到它之外的元素。 jQuery UI对话框和Malsup jQuery块插件似乎都是这样做的,但我试图获得这一个功能并将其应用到我的项目中,并不是很明显我们是如何做到的。

我看到有些人认为标签不应该被禁用,我可以看到这个观点,但是我被指示禁用它。

5 个答案:

答案 0 :(得分:27)

这只是扩展了基督徒的答案,增加了额外的输入类型,同时也考虑了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();
    }
});

答案 1 :(得分:13)

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

我可能需要进一步细化这个来检查按下其他一些键,例如箭头键,但基本的想法就在那里。

答案 2 :(得分:8)

Christian和jfutch的好解决方案。

值得一提的是,劫持标签击键有一些陷阱:

  • tabindex属性可能在模态窗格内的某些元素上设置,使得元素的dom顺序不遵循Tab键顺序。 (例如,在最后一个tabbable元素上设置tabindex =“10”可以使它在Tab键顺序中排在第一位)
  • 如果用户与模态外部的元素交互而不触发模式关闭,则可以在模态窗口外部选项卡。 (例如,单击位置栏并开始切换回页面,或在屏幕阅读器中打开页面标记,如VoiceOver&导航到页面的其他部分)
  • 检查元素是:visible是否会在dom变脏时触发重排
  • 该文档可能没有:focussed元素。在chrome中,可以通过单击不可聚焦的元素然后按Tab键来更改“插入符号”位置。用户可以将插入位置设置为超过最后一个tabbable元素。

我认为更强大的解决方案是通过在所有可列表内容上将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-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可见性/显示优先级元素的规则&执行咏叹调隐藏 在这种情况下,浏览器不一致(请参阅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事件冒泡 如果多个元素在模态转换时,它可以多次触发 窗口打开,因此在调用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();
    }
}

答案 3 :(得分:4)

我刚刚对Alexander Puchkov的solution做了一些修改,并将其作为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);

答案 4 :(得分:0)

对于像我一样最近进入这个领域的人,我采用了上面概述的方法,并且对它们进行了一些简化以使其更易消化。感谢@ niall.campbell在这里建议的方法。

下面的代码可以在this 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 = [];
}