将焦点添加到弹出窗口/模式点击选项卡/辅助功能 - JavaScript

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

我有一个 pop-up/overlay 出现在元素的 'click' 上。因为弹出窗口后面有很多 HTML 内容,弹出窗口中的 buttons/input 元素自然不会有 focus/tabindex 行为。出于可访问性的原因,我希望这样当我们显示模态框内的元素时,它具有 focus/tab 索引优先级而不是其背后的主要内容。

在下面的简单演示中 - 单击 'click-me' 按钮后,当您使用 Tab 键时,浏览器仍会在叠加层后面的输入元素之间切换。

任何关于如何在显示时为覆盖提供选项卡行为的建议将不胜感激。

在模态上创建 focus 事件似乎不起作用?

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

编辑

我几乎可以让 George Chapman 的 Codepen 答案起作用,但是当您按住回车键时,它会在叠加层出现和不出现之间来回闪烁,而且它似乎在 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>

在使模态对话框易于访问时,需要考虑一些事项,而不仅仅是将焦点设置到模态或限制模态内的 Tab 键顺序。您还必须考虑到屏幕 readers 仍然可以感知底层页面元素,如果它们没有使用 aria-hidden="true" 从屏幕 reader 隐藏,那么您还需要 un-hide 模式关闭和底层页面恢复时的那些元素。

所以,总而言之,您需要做的是:

  1. 当模态框出现时,将焦点设置到模态框内的第一个可聚焦元素。
  2. 确保底层页面元素在屏幕上隐藏 reader。
  3. 确保 Tab 键顺序在模式内受到限制。
  4. 确保实现预期的键盘行为,例如,按 Escape 将关闭或关闭模式对话框。
  5. 确保在模式关闭时恢复底层页面元素。
  6. 确保在模式对话框打开之前具有焦点的元素已恢复焦点。

您还需要确保您的模态对话框具有 ARIA role="dialog" 属性,以便屏幕 readers 将宣布焦点已移至对话框,理想情况下您应该使用 aria-labelledby and/or aria-describedby 属性为您的模式提供可访问的名称 and/or 描述。

这是一个相当长的列表,但这是通常推荐用于可访问模式对话框的列表。参见 WAI-ARIA Modal Dialog Example.

我已经为您的模式编写了一个解决方案,部分基于 Hidde de Vries's original code 用于限制模式对话框内的 Tab 键顺序。

trapFocusInModal 函数生成所有可聚焦元素的节点列表,并为 TabShift+ 添加一个键监听器Tab 键以确保焦点不会超出模式中的可聚焦元素。按键侦听器还绑定到 Escape 键以关闭模式。

openModal 函数显示模态对话框,隐藏底层页面元素,在打开模态之前最后保持焦点的元素上放置一个 class 名称,并将焦点设置为第一个可聚焦的元素模态中的元素。

closeModal 函数关闭模式,un-hide 底层页面,并将焦点恢复到打开模式之前最后保持焦点的元素。

domIsReady函数等待DOM准备就绪,然后将Enter键和鼠标点击事件绑定到openModalcloseModal 函数。

代码笔: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;
}

您必须在出现后立即将焦点添加到 pop-up,当您与 closeButton.focus() 同时执行时,它不会起作用,这就是我使用 setTimeout(() => closeButton.focus(), 1) 的原因,这将在 1 毫秒后添加焦点。

起初,焦点在一个按钮上是不可见的,当按下箭头键时它变得可见,所以我让它可见的样式:

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

全部代码:

      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>

更新:焦点仅在模态内跳跃:

     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>

将焦点移至模态

要将焦点放在模态中,您必须将焦点放在模态中的可聚焦元素上,这就是为什么 modal.focus(); 不会导致焦点像您希望的那样移动到模态中,因为模态本身不是可聚焦的元素。相反,您可能想要执行诸如 $(modal).find("button").first().focus(); 之类的操作。

User2495207 向您展示了另一种方法,但 setTimeout 容易出现错误且不必要。理想情况下,我们也不想规定它应该专注于特定按钮,而只是以 Tab 键顺序中找到的第一个按钮为准。

但是,这仅解决了最初将焦点移至模态的问题。它不会将焦点捕获在模态框内,因此当您选择最后一个按钮时,它会将焦点移动到模态框后面的元素。

在模态中捕获焦点

这里的想法是你想检查下一个可聚焦元素是否在模态中,如果不在模态中则意味着你在模态中的最后一个元素上并且需要将焦点放在第一个元素上在模式中。您还应该反转这个逻辑,如果第一个按钮获得焦点并且有人按下 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 是一个很棒的工具,它允许您拦截 focus 事件以确定焦点的去向。所以在上面的代码中,我们正在检查即将被聚焦的元素,也就是 relatedTarget,是否在模态内,如果不在,那么我们强制聚焦到我们想要它去的地方。

关于辅助功能的最后一点说明

您还希望确保在 Escapekeydown 上关闭模态。在这一点上,e.keyCode 已弃用,我们都应该使用 e.key.

如果您需要支持IE,首先很抱歉。其次,它需要 e.keyCode 才能正常运行,因此需要与 e.key 检查结合使用,例如 e.key === "Escape" && e.keyCode === "27"。但是,我确实建议,也许只是制作一个接受事件作为参数的函数,并将这些检查保留在该函数中,这样当 IE 最终支持 e.key 时,您就可以在一个地方清理所有代码。

我正在尝试向您提供最简单的解决方案。

所以我的解决方案是这样的:

1。查找模态中的所有 focus-able 个元素。

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 改变网页焦点。

3。在焦点侦听器方法中,检查如果模态打开并且 focus-able 元素列表中不存在焦点元素,[的第一个元素=43=] 元素列表必须是焦点。

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

最终代码:

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>