最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

wordpress - How do I trap focus in Javascript to a popup? - Stack Overflow

programmeradmin1浏览0评论

I have created the below JavaScript code for a popup that needs to be accessible.

The problem is that during keyboard-browsing the tab button selects all the buttons behind the popup on the landing page first. I need it to only select the buttons on the popup and the close button.

I understand that focus trapping the popup is a method that might work but I don't know how to add that to my code.

<< Here is the code

/ << You can view the popup in action on this website

<script>
    // Check if the popup has already been shown during the current session
    if (!sessionStorage.getItem('popupShown')) {
        setTimeout(function() {
            var popup = document.createElement("div");
            popup.style.position = "fixed";
            popup.style.top = "0";
            popup.style.left = "0";
            popup.style.zIndex = "999";
            popup.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
            popup.style.width = "100vw";
            popup.style.height = "100vh";
            popup.style.display = "flex";
            popup.style.alignItems = "center";
            popup.style.justifyContent = "center";
            var innerPopup = document.createElement("div");
            innerPopup.style.backgroundColor = "black";
            innerPopup.style.display = "flex";
            innerPopup.style.flexDirection = "column";
            innerPopup.style.alignItems = "center";
            innerPopup.style.justifyContent = "center";
            innerPopup.style.padding = "40px";
            innerPopup.style.margin = "35px";
            innerPopup.style.borderRadius = "25px";

            var img = document.createElement("img");
img.src = ".png ";
img.alt = "Find out about Kerridge’s core solution ";
img.style.width = "25%";
img.style.cursor = "pointer";
innerPopup.appendChild(img);

            var text = document.createElement("p");
            text.innerHTML = "Interested to find out more about our core solution?";
            text.style.color = "white";
            text.style.marginTop = "50px";
            text.style.textAlign = "center";
            text.style.fontSize = "20px";
            innerPopup.appendChild(text);

            var button = document.createElement("button");
            button.innerHTML = "Book a free demo today";
            button.style.backgroundColor = "#E8017B";
            button.style.color = "white";
            button.style.fontSize = "20px";
            button.style.padding = "15px";
            button.style.borderRadius = "50px"
            button.style.border = "none"
            button.onclick = function() {
                location.href = "/ ";
            }
            innerPopup.appendChild(button);
            
            var secondText = document.createElement("p");
            secondText.innerHTML = "Stay up to date with the latest Kerridge product updates and existing announcements";
            secondText.style.color = "white";
            secondText.style.marginTop = "50px";
            secondText.style.textAlign = "center";
            secondText.style.fontSize = "20px";
            innerPopup.appendChild(secondText);
            
            var secondButton = document.createElement("button");
                secondButton.innerHTML = "Sign up to our Newsletter";
                secondButton.style.color = "#E8017B";
                secondButton.style.background = "transparent";
                secondButton.style.border = "none";
                secondButton.style.fontSize = "20px";
                secondButton.onclick = function() {
                 location.href = "/ ";
                }
                
                innerPopup.appendChild(secondButton);

            var closeBtn = document.createElement("div");
            closeBtn.style.position = "absolute";
            closeBtn.style.top = "50px";
            closeBtn.style.right = "50px";
            closeBtn.style.cursor = "pointer";
            closeBtn.style.width = "60px";
            closeBtn.style.height = "60px";
            closeBtn.style.borderRadius = "50%";
            closeBtn.style.backgroundColor = "#E8017B";
            closeBtn.style.display = "flex";
            closeBtn.style.alignItems = "center";
            closeBtn.style.justifyContent = "center";

closeBtn.setAttribute("tabindex", "0");
closeBtn.setAttribute("role", "button");
closeBtn.setAttribute("aria-label", "Close");

closeBtn.addEventListener("click", function() {
  // Add logic to close something here
  popup.remove();
});

closeBtn.addEventListener("keydown", function(event) {
  if (event.key === "Enter" || event.key === " ") {
    // Add logic to close something here
    popup.remove();
  }
});

            var closeX = document.createElement("div");
            closeX.innerHTML = "X";
            closeX.style.color = "white";
            closeX.style.fontWeight = "bold";

            closeBtn.appendChild(closeX);
            closeBtn.onclick = function() {
                popup.remove();
            }
            innerPopup.appendChild(closeBtn);

            popup.appendChild(innerPopup);
            document.body.appendChild(popup);
            // Set a value in sessionStorage indicating that the popup has been shown
            sessionStorage.setItem('popupShown', 'true');
        }, 90000 );
    }
</script>

I have created the below JavaScript code for a popup that needs to be accessible.

The problem is that during keyboard-browsing the tab button selects all the buttons behind the popup on the landing page first. I need it to only select the buttons on the popup and the close button.

I understand that focus trapping the popup is a method that might work but I don't know how to add that to my code.

https://codepen.io/aryanotstark/pen/KKBjLXY << Here is the code

https://digitalcloud.co.za/kiron/ << You can view the popup in action on this website

<script>
    // Check if the popup has already been shown during the current session
    if (!sessionStorage.getItem('popupShown')) {
        setTimeout(function() {
            var popup = document.createElement("div");
            popup.style.position = "fixed";
            popup.style.top = "0";
            popup.style.left = "0";
            popup.style.zIndex = "999";
            popup.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
            popup.style.width = "100vw";
            popup.style.height = "100vh";
            popup.style.display = "flex";
            popup.style.alignItems = "center";
            popup.style.justifyContent = "center";
            var innerPopup = document.createElement("div");
            innerPopup.style.backgroundColor = "black";
            innerPopup.style.display = "flex";
            innerPopup.style.flexDirection = "column";
            innerPopup.style.alignItems = "center";
            innerPopup.style.justifyContent = "center";
            innerPopup.style.padding = "40px";
            innerPopup.style.margin = "35px";
            innerPopup.style.borderRadius = "25px";

            var img = document.createElement("img");
img.src = "https://digitalcloud.co.za/wp-content/uploads/2023/01/kerridge_pop_up_illustration.png ";
img.alt = "Find out about Kerridge’s core solution ";
img.style.width = "25%";
img.style.cursor = "pointer";
innerPopup.appendChild(img);

            var text = document.createElement("p");
            text.innerHTML = "Interested to find out more about our core solution?";
            text.style.color = "white";
            text.style.marginTop = "50px";
            text.style.textAlign = "center";
            text.style.fontSize = "20px";
            innerPopup.appendChild(text);

            var button = document.createElement("button");
            button.innerHTML = "Book a free demo today";
            button.style.backgroundColor = "#E8017B";
            button.style.color = "white";
            button.style.fontSize = "20px";
            button.style.padding = "15px";
            button.style.borderRadius = "50px"
            button.style.border = "none"
            button.onclick = function() {
                location.href = "https://digitalcloud.co.za/ ";
            }
            innerPopup.appendChild(button);
            
            var secondText = document.createElement("p");
            secondText.innerHTML = "Stay up to date with the latest Kerridge product updates and existing announcements";
            secondText.style.color = "white";
            secondText.style.marginTop = "50px";
            secondText.style.textAlign = "center";
            secondText.style.fontSize = "20px";
            innerPopup.appendChild(secondText);
            
            var secondButton = document.createElement("button");
                secondButton.innerHTML = "Sign up to our Newsletter";
                secondButton.style.color = "#E8017B";
                secondButton.style.background = "transparent";
                secondButton.style.border = "none";
                secondButton.style.fontSize = "20px";
                secondButton.onclick = function() {
                 location.href = "https://digitalcloud.co.za/ ";
                }
                
                innerPopup.appendChild(secondButton);

            var closeBtn = document.createElement("div");
            closeBtn.style.position = "absolute";
            closeBtn.style.top = "50px";
            closeBtn.style.right = "50px";
            closeBtn.style.cursor = "pointer";
            closeBtn.style.width = "60px";
            closeBtn.style.height = "60px";
            closeBtn.style.borderRadius = "50%";
            closeBtn.style.backgroundColor = "#E8017B";
            closeBtn.style.display = "flex";
            closeBtn.style.alignItems = "center";
            closeBtn.style.justifyContent = "center";

closeBtn.setAttribute("tabindex", "0");
closeBtn.setAttribute("role", "button");
closeBtn.setAttribute("aria-label", "Close");

closeBtn.addEventListener("click", function() {
  // Add logic to close something here
  popup.remove();
});

closeBtn.addEventListener("keydown", function(event) {
  if (event.key === "Enter" || event.key === " ") {
    // Add logic to close something here
    popup.remove();
  }
});

            var closeX = document.createElement("div");
            closeX.innerHTML = "X";
            closeX.style.color = "white";
            closeX.style.fontWeight = "bold";

            closeBtn.appendChild(closeX);
            closeBtn.onclick = function() {
                popup.remove();
            }
            innerPopup.appendChild(closeBtn);

            popup.appendChild(innerPopup);
            document.body.appendChild(popup);
            // Set a value in sessionStorage indicating that the popup has been shown
            sessionStorage.setItem('popupShown', 'true');
        }, 90000 );
    }
</script>

Share Improve this question asked Feb 10, 2023 at 12:09 Arya PremArya Prem 111 silver badge3 bronze badges 1
  • Use dialog element and open dialog as showModal() – Anil kumar Commented Feb 10, 2023 at 12:21
Add a ment  | 

3 Answers 3

Reset to default 5

To render the question more plete, for a modal dialog, you need two things:

  1. Trap focus inside the modal dialog
  2. Hide all other contents from assistive technology

Of course there is more to it, but this is the part relevant to the question.

Navigation via Tab is only one means of navigation with screen readers. TalkBack and VoiceOver, the screen readers on Android and iOS, make no difference between real focus and reading focus. That is what the latter is about: Avoiding reading access to contents outside the dialog.

In a lot of solutions this is badly done, and one can move outside a modal dialog on mobile platforms, for example in Bootstrap 5.

Native, standards methods

The <dialog> element was standardised to solve these issues.

Obviously, this is the best way to go, since it means you have all the momentum of browser vendors’ development in favour of your dialog.

According to Scott O'hara two weeks ago, you should Use it

If you don’t want to rely on this native element, and implement a custom modal dialog, there is two things the native one does, concerning your question.

<dialog> elements invoked by the showModal() method will have an implicit aria-modal="true" […]

[…] everything other than the <dialog> and its contents should be rendered inert using the inert attribute.

In other words this would guide us to

<main inert>
  …
  <p><a href="#">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
  <h2 id="d-title">Dialog title</h2>
  …
  <p><button>Can you focus me?</button></p>
</div>

Unfortunately, also here, browser support is not great yet.

Alternative for aria-modal

That attribute is supposed to hide everything else (the <main> in above example) from assistive technology. For browsers that don’t support this, aria-hidden can be applied to the other contents:

<main aria-hidden="true">
…
</main>
<div role="dialog" …>

This has no effect on focus, but on reading through assistive technology.

Trap focus

There are two strategies.

Intercept focusin

This is the mothed used in Bootstrap’s Modal and the APG’s Modal Dialog Example.

One strategy is to bind a listener to the focusin event of contents outside the dialog, and put focus back on the first or last interactive element in the dialog, depending on which one was last focused.

You cannot use the focusout event because the Focus order of user triggered focus will be fired after focusout, and the latter is not cancelable.

document.getElementsByTagName('main')[0].addEventListener('focusin', () => document.querySelector('[role=dialog] button').focus());
<main>
  …
  <p><a href="#">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
  <h2 id="d-title">Dialog title</h2>
  …
  <p><button autofocus>Can you focus me?</button></p>
</div>

Make everything else not focusable

You can try to render all other interactive elements not focusable or clickable.

This is obviously more fragile, especially if the interactivity is not exposed through use of native HTML elements or ARIA roles, so it bees hard to identify these.

<main style="pointer-events: none">
  …
  <p><a href="#" tabindex="-1">Can you focus me?</a></p>
</main>
<div role="dialog" aria-modal="true" aria-labelledby="d-title">
  <h2 id="d-title">Dialog title</h2>
  …
  <p><button autofocus>Can you focus me?</button></p>
</div>

One cool idea is to attach hidden, tab-able elements before and after your dialog. Once those receive focus, pass it to the first or last element inside dialog:

[PREV HANDLE]
DIALOG -----
|          |
| INPUT A  |
| INPUT B  |
| CHECKBOX |
| BUTTON   |
------------
[NEXT HANDLE]

Pseudo code would be as follows:

<div tabindex="0" class="hidden" />
<div role="dialog" aria-modal="true">
  <input />
  <button />
</div>
<div tabindex="0" class="hidden" id="nextHandle" />
const trapFocus = (e) => {
  const isDialogBlured = !dialogElement.contains(e.target);

  if (isDialogBlured) {
    document.activeElement === nextHandle
      ? focusFirstDescendant(dialogElement) // Tab clicked - focus first descendant
      : focusLastDescendant(dialogElement); // Shift + Tab clicked - focus last descendant
  }
}

document.addEventListener("focus", trapFocus, true);
focusFirstDescendant(dialogElement);

Example snippet, basing on https://www.w3/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/

const wrapper = document.getElementById("wrapper");
const dialog = document.getElementById("dialog");
const triggerButton = document.getElementById("trigger");
const nextHandle = document.getElementById("nextHandle");

let preventFocusTrap = false;
  
const showDialog = () => {
  document.addEventListener("focus", trapFocus, true);
  wrapper.classList.remove("hidden");
  focusFirstDescendant(dialog);
};

const closeDialog = () => {
  document.removeEventListener("focus", trapFocus, true);
  wrapper.classList.add("hidden");
  triggerButton.focus();
}

const attemptFocus = (element) => {
  if (!isFocusable(element)) {
    return false;
  }

  preventFocusTrap = true; // We're about to manually focus element inside dialog. Tell the event handler not to take action 
  element.focus();
  preventFocusTrap = false;

  return document.activeElement === element;
};

const focusFirstDescendant = (element) => {
    for (let i = 0; i < element.childNodes.length; i++) {
      const child = element.childNodes[i];
      if (attemptFocus(child) || focusFirstDescendant(child)) {
        return true; // escape recursive function
      }
    }
  };

const focusLastDescendant = (element) => {
    for (let i = element.childNodes.length - 1; i >= 0; i--) {
      const child = element.childNodes[i];
      if (attemptFocus(child) || focusLastDescendant(child)) {
        return true; // escape recursive function
      }
    }
  };

const trapFocus = (e) => {
  if (preventFocusTrap) {
    return;
  }

  const isDialogBlured = !dialog.contains(e.target);

  if (isDialogBlured) {
    document.activeElement === nextHandle
      ? focusFirstDescendant(dialog) // Tab clicked - focus first descendant
      : focusLastDescendant(dialog); // Shift + Tab clicked - focus last descendant
  }
};

const isFocusable = (element) => {
  if (element.tabIndex < 0) {
    return false;
  }

  if (element.disabled) {
    return false;
  }

  switch (element.nodeName) {
    case "A":
      return !!element.href && element.rel !== "ignore";
    case "INPUT":
      return element.type !== "hidden";
    case "BUTTON":
    case "SELECT":
    case "TEXTAREA":
      return true;
    default:
      return false;
  }
};
.hidden {
  display: none;
}

/* http://a11yproject./posts/how-to-hide-content/ */
.invisible {
  position: absolute;
  width: 0;
  height: 0;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

/* Visual styles, insignificant */

#dialog {
  border: 2px solid #14acca;
  background-color: #eef8ff;
  border-radius: 10px;
  margin: 10px 0;
  padding: 0 15px;
}

a, button, label {
  display: block;
  margin: 10px 0;
}
<button onclick="showDialog()" id="trigger">
  Display dialog and trap focus
</button>

<div id="wrapper" class="hidden">
  <div tabindex="0" class="invisible">Prev handle</div>
  <div role="dialog" aria-modal="true" id="dialog">
    <h3>Hi! I am a dialog. Hit Tab or Shift+Tab few times to test focus trap</h3>
    <label>User: <input type="text" /></label>
    <label>Password: <input type="text" /></label>
    <label><input type="checkbox" /> Remember me</label>
    <button onclick="closeDialog()">Close dialog and untrap focus</button>
  </div>
  <div tabindex="0" class="invisible" id="nextHandle">Next handle</div>
</div>

<a href="#">Link after dialog</a>
<button>Button after dialog</button>

you could dynamically add With <div id="div1" tabindex="-1"> content you'd like to prevent tab</div> with js logic

发布评论

评论列表(0)

  1. 暂无评论