简体   繁体   English

添加焦点以弹出/模态单击以获取选项卡/可访问性 - JavaScript

[英]Add Focus To Pop Up / Modal On Click For Tabbing / Accessibility - JavaScript

I have a pop-up/overlay that appears on 'click' of an element.我有一个弹出/覆盖出现在元素的“点击”上。 Because there is plenty of HTML content behind the pop-up the buttons/input elements on the pop don't naturally have focus/tabindex behaviour.因为弹出窗口后面有大量 HTML 内容,所以弹出窗口上的按钮/输入元素自然不会具有焦点/tabindex 行为。 For accessibility reasons I would like it so that when this pop us shows the elements inside the modal have focus/tab index priority not the main content behind it.出于可访问性的原因,我希望这样当我们显示模态框内的元素时,它具有焦点/标签索引优先级,而不是其背后的主要内容。

In the simple demonstration below - after you click the 'click-me' button, when you use the tab key the browsers still tabs through the input elements behind the overlay.在下面的简单演示中 - 单击“click-me”按钮后,当您使用 tab 键时,浏览器仍会通过覆盖层后面的输入元素进行选项卡。

Any suggestions on how to give the overlay the tab behaviour when it shows would be greatly appreciated.任何有关如何在显示时为选项卡行为提供覆盖的建议将不胜感激。

Creating a focus event on the modal doesn't seem to work?在模式上创建focus事件似乎不起作用?

Codepen: https://codepen.io/anna_paul/pen/eYywZBz代码笔: https ://codepen.io/anna_paul/pen/eYywZBz

EDIT编辑

I can almost get George Chapman's Codepen answer to work, but when you hold the enter key down it flashes back and forth between the overlay appearing and not appearing, and it doesn't seem to work in Safari?我几乎可以让 George Chapman 的 Codepen 答案正常工作,但是当您按住 Enter 键时,它会在覆盖出现和不出现之间来回闪烁,而且它似乎在 Safari 中不起作用?

 let clickMe = document.querySelector('#click-me'), modal = document.querySelector('.modal'), closeButton = document.querySelector('.close') clickMe.addEventListener('click', () => { modal.style.display = 'flex'; // modal.focus(); }) closeButton.addEventListener('click', () => { modal.style.display = 'none'; })
 body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; flex-direction: column; } input, button { margin: 1rem; padding: .5rem; } .click-me { display: block; } .modal { display: none; flex-direction: column; width: 100%; height: 100%; justify-content: center; align-items: center; background: grey; position: absolute; } form { display: flex; }
 <button id="click-me">Click Me</button> <form action=""> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> </form> <div class="modal"> <button class="close">Close x</button> <button>More Buttons</button> <button>More Buttons</button> </div>

There are a few things to consider when making your modal dialog accessible that go beyond just setting focus to your modal or retricting tab order within the modal.在使您的模态对话框可访问时,需要考虑一些事项,而不仅仅是将焦点设置到您的模态或限制模态中的 Tab 键顺序。 You also have to consider that screen readers can still perceive the underlying page elements if they're not hidden from the screen reader using aria-hidden="true" , and then you also need to un-hide those elements when the modal is closed and the underlying page is restored.您还必须考虑,如果屏幕阅读器没有使用aria-hidden="true"对屏幕阅读器隐藏,屏幕阅读器仍然可以感知底层页面元素,然后您还需要在模式关闭时取消隐藏这些元素并且基础页面被恢复。

So, to summarise, what you need to do is:因此,总而言之,您需要做的是:

  1. Set focus to the first focusable element inside the modal when it appears.当模态框出现时,将焦点设置为模态框内的第一个可聚焦元素。
  2. Ensure that the underlying page elements are hidden from the screen reader.确保对屏幕阅读器隐藏底层页面元素。
  3. Ensure that tab order is restricted inside the modal.确保 Tab 键顺序被限制在模式内。
  4. Ensure that expected keyboard behaviour is implemented, eg, pressing Escape will close or dismiss the modal dialog.确保实现预期的键盘行为,例如,按 Escape 将关闭或关闭模式对话框。
  5. Ensure that the underlying page elements are restored when the modal is closed.确保在关闭模式时恢复底层页面元素。
  6. Ensure that the element that previously had focus prior to the modal dialog being opened has focus restored to it.确保在打开模式对话框之前先前具有焦点的元素已恢复焦点。

You also need to ensure that your modal dialog has the ARIA role="dialog" attribute so that screen readers will announce that focus has moved to a dialog, and ideally you should use the aria-labelledby and/or aria-describedby attributes to provide an accessible name and/or description to your modal.您还需要确保您的模态对话框具有 ARIA role="dialog"属性,以便屏幕阅读器将宣布焦点已移至对话框,理想情况下您应该使用aria-labelledby和/或aria-describedby属性来提供您的模态的可访问名称和/或描述。

That's quite a list, but it's what is generally recommended for accessible modal dialogs.这是一个相当多的列表,但它通常推荐用于可访问的模式对话框。 See the WAI-ARIA Modal Dialog Example .请参阅WAI-ARIA 模态对话框示例

I've written a solution for your modal, partially based on Hidde de Vries's original code for restricting tab order inside a modal dialog.我已经为您的模式编写了一个解决方案,部分基于Hidde de Vries 的原始代码,用于限制模式对话框中的制表符顺序。

The trapFocusInModal function makes a node list of all focusable elements and adds a key listener for Tab and Shift + Tab keys to ensure focus doesn't move beyond the focusable elements in the modal. trapFocusInModal函数创建所有可聚焦元素的节点列表,并为TabShift + Tab键添加键侦听器,以确保焦点不会超出模态中的可聚焦元素。 The key listener also binds to the Escape key to close the modal.键侦听器还绑定到Escape键以关闭模式。

The openModal function displays the modal dialog, hides the underlying page elements, places a class name on the element that last held focus before the modal was opened and sets focus to the first focusable element in the modal. openModal函数显示模态对话框,隐藏底层页面元素,在打开模态之前最后保持焦点的元素上放置一个类名,并将焦点设置为模态中的第一个可聚焦元素。

The closeModal function closes the modal, un-hides the underlying page, and restores focus the element that last held focus before the modal was opened. closeModal函数关闭模式,取消隐藏底层页面,并恢复焦点在模式打开之前最后保持焦点的元素。

The domIsReady function waits for the DOM to be ready and then binds the Enter key and mouse click events to the openModal and closeModal functions. domIsReady函数等待 DOM 准备就绪,然后将Enter键和鼠标单击事件绑定到openModalcloseModal函数。

Codepen: https://codepen.io/gnchapman/pen/JjMQyoP代码笔: https ://codepen.io/gnchapman/pen/JjMQyoP

const KEYCODE_TAB = 9;
const KEYCODE_ESCAPE = 27;
const KEYCODE_ENTER = 13;

// Function to open modal if closed
openModal = function (el) {

    // Find the modal, check that it's currently hidden
    var modal = document.getElementById("modal");
    if (modal.style.display === "") {
        
        // Place class on element that triggered event
        // so we know where to restore focus when the modal is closed
        el.classList.add("last-focus");

        // Hide the background page with ARIA
        var all = document.querySelectorAll("button#click-me,input");
        for (var i = 0; i < all.length; i++) {
            all[i].setAttribute("aria-hidden", "true");
        }
        
        // Add the classes and attributes to make the modal visible
        modal.style.display = "flex";
        modal.setAttribute("aria-modal", "true");
        modal.querySelector("button").focus();
    }
};

// Function to close modal if open
closeModal = function () {

    // Find the modal, check that it's not hidden
    var modal = document.getElementById("modal");
    if (modal.style.display === "flex") {

        modal.style.display = "";
        modal.setAttribute("aria-modal", "false")

        // Restore the background page by removing ARIA
        var all = document.querySelectorAll("button#click-me,input");
        for (var i = 0; i < all.length; i++) {
            all[i].removeAttribute("aria-hidden");
        }
        
        // Restore focus to the last element that had it
        if (document.querySelector(".last-focus")) {
            var target = document.querySelector(".last-focus");
            target.classList.remove("last-focus");
            target.focus();
        }
    }
};

// Function to trap focus inside the modal dialog
// Credit to Hidde de Vries for providing the original code on his website:
// https://hidde.blog/using-javascript-to-trap-focus-in-an-element/
trapFocusInModal = function (el) {

    // Gather all focusable elements in a list
    var query = "a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type='email']:not([disabled]), input[type='text']:not([disabled]), input[type='radio']:not([disabled]), input[type='checkbox']:not([disabled]), select:not([disabled]), [tabindex='0']"
    var focusableEls = el.querySelectorAll(query);
    var firstFocusableEl = focusableEls[0];
    var lastFocusableEl = focusableEls[focusableEls.length - 1];

    // Add the key listener to the modal container to listen for Tab, Enter and Escape
    el.addEventListener('keydown', function(e) {
        var isTabPressed = (e.key === "Tab" || e.keyCode === KEYCODE_TAB);
        var isEscPressed = (e.key === "Escape" || e.keyCode === KEYCODE_ESCAPE);
  
        // Define behaviour for Tab or Shift+Tab
        if (isTabPressed) {
            // Shift+Tab
            if (e.shiftKey) {
                if (document.activeElement === firstFocusableEl) {
                    lastFocusableEl.focus();
                    e.preventDefault();
                }
            }
            
            // Tab
            else {
                if (document.activeElement === lastFocusableEl) {
                    firstFocusableEl.focus();
                    e.preventDefault();
                }
            }
        }
        
        // Define behaviour for Escape
        if (isEscPressed) {
            el.querySelector("button.close").click();
        }
    });
};

// Cross-browser 'DOM is ready' function
// https://www.competa.com/blog/cross-browser-document-ready-with-vanilla-javascript/
var domIsReady = (function(domIsReady) {

    var isBrowserIeOrNot = function() {
        return (!document.attachEvent || typeof document.attachEvent === "undefined" ? 'not-ie' : 'ie');
    }

    domIsReady = function(callback) {
        if(callback && typeof callback === 'function'){
            if(isBrowserIeOrNot() !== 'ie') {
                document.addEventListener("DOMContentLoaded", function() {
                    return callback();
                });
            } else {
                document.attachEvent("onreadystatechange", function() {
                    if(document.readyState === "complete") {
                        return callback();
                    }
                });
            }
        } else {
            console.error('The callback is not a function!');
        }
    }

    return domIsReady;
})(domIsReady || {});


(function(document, window, domIsReady, undefined) {

    // Check if DOM is ready
    domIsReady(function() {

        // Write something to the console
        console.log("DOM ready...");
        
        // Attach event listener on button elements to open modal
        if (document.getElementById("click-me")) {
                
            // Add click listener
            document.getElementById("click-me").addEventListener("click", function(event) {
                // If the clicked element doesn't have the right selector, bail
                if (!event.target.matches('#click-me')) return;
                event.preventDefault();
                // Run the openModal() function
                openModal(event.target);
            }, false);

            // Add key listener
            document.getElementById("click-me").addEventListener('keydown', function(event) {
                if (event.code === "Enter" || event.keyCode === KEYCODE_ENTER) {
                    // If the clicked element doesn't have the right selector, bail
                    if (!event.target.matches('#click-me')) return;
                    event.preventDefault();
                    // Run the openModal() function
                    openModal(event.target);
                }
            });
        }

        // Attach event listener on button elements to close modal
        if (document.querySelector("button.close")) {
                
            // Add click listener
            document.querySelector("button.close").addEventListener("click", function(event) {
                // If the clicked element doesn't have the right selector, bail
                if (!event.target.matches('button.close')) return;
                event.preventDefault();
                // Run the closeModal() function
                closeModal(event.target);
            }, false);

            // Add key listener
            document.querySelector("button.close").addEventListener('keydown', function(event) {
                if (event.code === "Enter" || event.keyCode === KEYCODE_ENTER) {
                    // If the clicked element doesn't have the right selector, bail
                    if (!event.target.matches('button.close')) return;
                    event.preventDefault();
                    // Run the closeModal() function
                    closeModal(event.target);
                }
            });
        }

        // Trap tab order within modal
        if (document.getElementById("modal")) {
            var modal = document.getElementById("modal");
            trapFocusInModal(modal);
        }
        
   });
})(document, window, domIsReady);
<button id="click-me">Click Me</button>
<form action="">
    <input placeholder="An Input" type="text"> <input placeholder="An Input" type="text"> <input placeholder="An Input" type="text"> <input placeholder="An Input" type="text">
</form>
<div class="modal" id="modal" role="dialog">
    <button class="close">Close x</button> <button>More Buttons</button> <button>More Buttons</button>
</div>
body {
  margin: 0;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

input, button {
  margin: 1rem;
  padding: .5rem;
}
.click-me {
  display: block;
}

.modal {
  display: none;
  flex-direction: column;
  width: 100%;
  height: 100%;   
  justify-content: center;
  align-items: center;
  background: grey;
  position: absolute;
}

form {
  display: flex;
}

You have to add focus to the pop-up right after this appears, when you do it simultaneously with closeButton.focus() only it won't work that's why I'm using setTimeout(() => closeButton.focus(), 1) , this will added it focus after a 1 millisecond.您必须在此出现后立即将焦点添加到弹出窗口,当您同时使用closeButton.focus()时它不会起作用,这就是我使用setTimeout(() => closeButton.focus(), 1) ,这将在1毫秒后添加焦点。

At first, focus on a button isn't visible, it become visible when arrow keys are pressed, so I make it visible styling it:起初,一个按钮的焦点是不可见的,当按下箭头键时它变得可见,所以我让它可见的样式:

      .close:focus {
        border: 2px solid black;
        border-radius: 5px;
      }

The whole code:整个代码:

 let clickMe = document.querySelector("#click-me"), modal = document.querySelector(".modal"), closeButton = document.querySelector(".close"); clickMe.addEventListener("click", () => { setTimeout(() => closeButton.focus(), 1); modal.style.display = "flex"; }); closeButton.addEventListener("click", () => { modal.style.display = "none"; });
 body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; flex-direction: column; } input, button { margin: 1rem; padding: 0.5rem; } .click-me { display: block; } .modal { display: none; flex-direction: column; width: 100%; height: 100%; justify-content: center; align-items: center; background: gray; position: absolute; } form { display: flex; } .close:focus { border: 2px solid black; border-radius: 5px; }
 <button id="click-me">Click Me</button> <form action=""> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> </form> <div class="modal"> <button class="close">Close x</button> <button>More Buttons</button> <button>More Buttons</button> </div>

UPDATE: The focus jumps only within the modal:更新:焦点仅在模式内跳转:

 let clickMe = document.querySelector("#click-me"), modal = document.querySelector(".modal"), closeButton = document.querySelector(".close"); lastButton = document.querySelector(".lastButton"); clickMe.addEventListener("click", () => { setTimeout(() => closeButton.focus(), 1); modal.style.display = "flex"; }); closeButton.addEventListener("click", () => { modal.style.display = "none"; }); modal.addEventListener("keydown", function (event) { var code = event.keyCode || event.which; if (code === 9) { if (lastButton == document.activeElement) { event.preventDefault(); closeButton.focus(); } } });
 body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; flex-direction: column; } input, button { margin: 1rem; padding: 0.5rem; } .click-me { display: block; } .modal { display: none; flex-direction: column; width: 100%; height: 100%; justify-content: center; align-items: center; background: gray; position: absolute; } form { display: flex; } .close:focus { border: 2px solid black; border-radius: 5px; }
 <button id="click-me">Click Me</button> <form action=""> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> <input type="text" placeholder="An Input" /> </form> <div class="modal"> <button class="close">Close x</button> <button>More Buttons</button> <button class="lastButton">More Buttons</button> </div>

Moving focus into the modal将焦点移入模态

To put focus into the modal, you have to put focus onto a focusable element within the modal, which is why doing modal.focus();要将焦点放在模态中,您必须将焦点放在模态中的可聚焦元素上,这就是为什么要modal.focus(); did not result in the focus moving into the modal like you wished since modal itself isn't a focusable element.没有导致焦点像您希望的那样移动到模态中,因为模态本身不是可聚焦的元素。 Instead, you would want to do something such as $(modal).find("button").first().focus();相反,您可能想做一些诸如$(modal).find("button").first().focus();类的事情。 instead.反而。

User2495207 showed you another way to do this, but setTimeout is prone to bugs and unnecessary. User2495207 向您展示了另一种方法,但setTimeout容易出现错误且不必要。 We also ideally don't want to dictate that it should focus on a specific button, just whichever is the first button found in the tab order.理想情况下,我们也不想规定它应该专注于特定按钮,只要是在 Tab 键顺序中找到的第一个按钮。

This only solves the problem of moving the focus into the modal initially, however.然而,这仅解决了最初将焦点移动到模态中的问题。 It does not trap the focus within the modal, so when you tab past the last button, it will move focus to elements behind the modal.它不会在模态框内捕获焦点,因此当您通过最后一个按钮时,它会将焦点移动到模态框后面的元素。

Trapping focus in the modal在模态中捕获焦点

The idea here is that you want to check if the next focusable element is within the modal or not, and if not then that means you were on the last element in the modal and need to wrap focus to the first element in the modal.这里的想法是您要检查下一个可聚焦元素是否在模态中,如果不是,则表示您位于模态中的最后一个元素上,并且需要将焦点包裹到模态中的第一个元素。 You should also reverse this logic where if the first button is focused and someone presses shift+tab it'll wrap to the last element in the modal, but I am just going to demonstrate the first scenario:您还应该颠倒这个逻辑,如果第一个按钮被聚焦并且有人按下shift+tab ,它将换行到模态中的最后一个元素,但我只是要演示第一个场景:

let clickMe = document.querySelector('#click-me'),
    modal = document.querySelector('.modal'),
    closeButton = document.querySelector('.close')

clickMe.addEventListener('click', () =>{
  modal.style.display = 'flex';
  $(modal).find("button").first().focus();

  trapFocus(modal);
});

function trapFocus(modal) {
  $(modal).find("button").last().on('blur', (e) => {
    // found something outside the modal
    if (!$(modal).find($(e.relatedTarget)).length > 0) {
      e.preventDefault();
      $(modal).find("button").first().focus();
    }
  });
}

closeButton.addEventListener('click', () =>{
  modal.style.display = 'none';
});

RelatedTarget is a great tool that allows you to intercept focus events to determine where the focus is going. RelatedTarget是一个很棒的工具,它允许您拦截focus事件以确定焦点的去向。 So in the code above, we are checking if the element that is about to be focused, aka relatedTarget , is within the modal, if it is not, then we force focus where we want it to go.所以在上面的代码中,我们正在检查即将被聚焦的元素,也就是relatedTarget ,是否在模态框内,如果不是,那么我们强制聚焦到我们想要它去的地方。

One last note about Accessibility关于可访问性的最后一点说明

You also want to be sure to make the modal close on keydown of Escape .您还需要确保在Escapekeydown上关闭模式。 On this note, e.keyCode is deprecated, and we should all be using e.key .在此说明中,不推荐使用e.keyCode ,我们都应该使用e.key

If you need to support IE, first of all, I am sorry.如果你需要支持IE,首先很抱歉。 Second of all, it requires e.keyCode to function properly so it needs to be used in conjunction with your e.key check, such as e.key === "Escape" && e.keyCode === "27" .其次,它需要e.keyCode才能正常运行,因此需要与您的e.key检查结合使用,例如e.key === "Escape" && e.keyCode === "27" I do recommend, however, maybe just making a function that accepts the event as a parameter, and keeping these checks within that function so when IE eventually makes support for e.key then you can cleanup your code all in one spot.但是,我确实建议您创建一个接受事件作为参数的函数,并将这些检查保留在该函数中,以便当 IE 最终支持e.key时,您可以在一个地方清理所有代码。

I trying easiest solution present to you.我尝试向您展示最简单的解决方案。

So my solution is this:所以我的解决方案是这样的:

1 .finding all focus-able elements in the modal. 1 .在模态中找到所有可聚焦的元素。

let modelElement = document.getElementById("modal");
let focusableElements = modelElement.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]');

2 .listen to change focus on the web page. 2.listen改变网页的焦点。

3 .In the focus listener method, it is checked that if modal is open and focused element not exist in the focus-able elements list, first element of focus-able elements list must be focus. 3 .在焦点监听方法中,检查如果modal打开并且焦点元素列表中不存在焦点元素,则焦点元素列表的第一个元素必须是焦点。

document.addEventListener('focus', (event) => { 
    if(modal.style.display == 'flex' && !Array.from(focusableElements).includes(event.target))
        Array.from(focusableElements)[0].focus();
}, true);

Final code:最终代码:

 let clickMe = document.querySelector('#click-me'), modal = document.querySelector('.modal'), closeButton = document.querySelector('.close') console.log(clickMe) clickMe.addEventListener('click', () =>{ modal.style.display = 'flex'; // modal.focus(); }) closeButton.addEventListener('click', () =>{ modal.style.display = 'none'; }) let modelElement = document.getElementById("modal"); let focusableElements = modelElement.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]'); document.addEventListener('focus', (event) => { if(modal.style.display == 'flex' && !Array.from(focusableElements).includes(event.target)) Array.from(focusableElements)[0].focus(); }, true);
 body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; flex-direction: column; } input, button { margin: 1rem; padding: .5rem; } .click-me { display: block; } .modal { display: none; flex-direction: column; width: 100%; height: 100%; justify-content: center; align-items: center; background: grey; position: absolute; } form { display: flex; }
 <button id="click-me">Click Me</button> <form action=""> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> <input type="text" placeholder="An Input"> </form> <div id="modal" class="modal"> <button class="close">Close x</button> <button>More Buttons</button> <button>More Buttons</button> </div>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM