Vanilla javascript Trap Focus in modal (accessibility tabbing )
Asked Answered
D

5

6

This should be pretty simple but for some reason it isn't working, I'm getting the proper console.logs at the right time, but the focus isn't going to the correct place, please refer to my jsfiddle

https://jsfiddle.net/bqt0np9d/

function checkTabPress(e) {
    "use strict";
    // pick passed event of global event object
    e = e || event;

    if (e.keyCode === 9) {
        if (e.shiftKey) {
            console.log('back tab pressed');
            firstItem.onblur=function(){
                console.log('last a focus left');
                lastItem.focus();
            };
            e.preventDefault();
        }
        console.log('tab pressed');
        lastItem.onblur=function(){
            console.log('last a focus left');
            firstItem.focus();
        };
        e.preventDefault();
    }
}
modal.addEventListener('keyup', checkTabPress);
Detergency answered 7/10, 2015 at 15:49 Comment(0)
C
2

I had to lock focus within a modal that we had used within a React component. I added eventListner for KEY DOWN and collected Tab and Shift+Tab

class Modal extends Component {
    componentDidMount() {
        window.addEventListener("keyup", this.handleKeyUp, false);
        window.addEventListener("keydown", this.handleKeyDown, false);
    }

    componentWillUnmount() {
        window.removeEventListener("keyup", this.handleKeyUp, false);
        window.removeEventListener("keydown", this.handleKeyDown, false);
    }

    handleKeyDown = (e) => {

        //Fetch node list from which required elements could be grabbed as needed.
        const modal = document.getElementById("modal_parent");
        const tags = [...modal.querySelectorAll('select, input, textarea, button, a, li')].filter(e1 => window.getComputedStyle(e1).getPropertyValue('display') === 'block');
        const focusable = modal.querySelectorAll('button, [href], input, select, textarea, li, a,[tabindex]:not([tabindex="-1"])');
        const firstFocusable = focusable[0];
        const lastFocusable = focusable[focusable.length - 1];

        if (e.ctrlKey || e.altKey) {
            return;
        }

        const keys = {
            9: () => { //9 = TAB
                if (e.shiftKey && e.target === firstFocusable) {
                    lastFocusable.focus();
                }

                if (e.target === lastFocusable) {
                    firstFocusable.focus();
                }
            }
        };

        if (keys[e.keyCode]) {
            keys[e.keyCode]();
        }
    }
}
Corinnecorinth answered 5/11, 2019 at 0:1 Comment(0)
E
1

I thought I had solved trapping the focus on a modal by using tab, shift+tab, and arrow keys detection on keyup and keydown, focus, focusin, focusout on the first and last focusable elements inside the modal and a focus event for the window to set the focus back on the first focusable element on the form in case the focus "escaped" the modal or for situations like jumping from the address bar to the document using tab, but something weird happened. I had activated "Caret Browsing" in one of my browsers accidently, and that's when I realized all methods to trap focus failed miserably. I personally went on a rabbit whole to solve this for a modal. I tried focusin, focusout on the modal, matching focus-within pseudo classes, {capture: true} on the focus event from the modal and window, nothing worked.

This is how I solved it.

I recreated the modal to have a different structure. For the sake of simplicity, I am omitting a lot of things, like the aria attributes, classes, how to get all focusable elements, etc.

<component-name>
#shadow-root (closed)
<div class="wrapper">
    <div class="backdrop"></div>
    <div class="window>
        <div tabindex="0" class="trap-focus-top">&nbsp;</div>
        <div class="content">
            <div class="controls"><!-- Close button, whatever --></div>
            <header><slot name="header"></slot></header>
            <div class="body"><slot></slot></div>
            <footer><slot name="footer"></slot></footer>
        </div>
        <div tabindex="0" class="trap-focus-bottom">&nbsp;</div>
    </div>
</div>
</component-name>
  1. Search the contents div for focusable elements, to save the first and last one. If you find only one then that one will be first and last. If you find zero, then set the div for the body (.body) tabindex to "0" so that you have at least one element to set the focus on.

  2. Before and after the content div we have two focusable divs, trap-focus-top and trap-focus-bottom, the first one when getting focus will jump the focus to the last focusable element detected on step one, and the second one will jump the focus to the first focusable element detected on step one. No need to capture any key events, just focus event on these elements. If you notice the non-breaking space &nbsp; on trap-focus elements, this is for mimicking content, because I noticed that the arrow keys went through these elements without firing any events when empty. When I realized this I added some content and everything worked, so I added a non-breaking space and styled the elements so that they do not occupy any space.

  3. Capture all focus events from the window with the use capture flag set to true, so that every focus event whose target was different to the component (focus events inside the shadow-root wont't be captured with the actual target but the component itself) will result in the focus being set on the modal elements again.

Now there's another problem, let's say there's zero focusable elements on your modal outside of any controls, like a button to close the modal, then we set tabindex to 0 on the modal's body, your focus should go from the close button to the modal's body and vice versa, now, the caret browsing won't work on the content because the div.body will have the focus, not the actual content. This means I have to create another function that places the cursor at the beginning of the content whenever the body receives the focus.

startCursor = () => {
    /* componentbody is a placeholder for the element with the actual content */
    let text = componentbody.childNodes[0];

    if (text) {
        let range = new Range();
        let selection = document.getSelection();

        range.setStart(text, 0);
        range.setEnd(text, 0);
        selection.removeAllRanges();
        selection.addRange(range);

        componentbody.scrollTop = 0;/* In case the body has a scrollbar */
    }
}

For anyone out there, this is what worked for me.

Envision answered 31/7, 2022 at 7:5 Comment(0)
L
0

The e.preventDefault() has no effect on the keyup event (as the default browser action has already been fired)

Despite this, your example works. But only if there are links before and after the modal

If you change your HTML code with the following, adding one link before and one link after the modal; you will see that your focus is trapped in the modal:

 <a href="#">other link</a>
 <div id="modal">
     <a href="#">Link One</a>
     <a href="#">Link Two</a>
 </div>
 <a href="#">other link</a>

That's because there is no default browser action in such case, and then no action to prevent.

Lacustrine answered 8/10, 2015 at 9:37 Comment(0)
D
0

Trapping focus within a modal is very hard to do it on your own. If you're able to install third-party dependencies in your project, you can use the focus-trap package.

You can easily trap focus to any component with vanilla Javascript;

import { createFocusTrap } from 'focus-trap'

const modal = document.getElementById('modal')

const focusTrap = createFocusTrap('#modal', {
    onActivate: function () {
        modal.className = 'trap is-visible'
    },
    onDeactivate: function () {
        modal.className = 'trap'
    },
})

document.getElementById('show').addEventListener('click', function () {
    focusTrap.activate()
})

document.getElementById('hide').addEventListener('click', function () {
    focusTrap.deactivate()
})

or even React;

import React from 'react'
import ReactDOM from 'react-dom'
// Use the wrapper package of `focus-trap` to use with React.
import FocusTrap from 'focus-trap-react'

const Demo = () => {
    const [showModal, setShowModal] = React.useState(false)

    return (
        <div>
            <button onClick={() => setShowModal(true)}>show modal</button>

            <FocusTrap active={showModal}>
                <div id="modal">
                    Modal with <a href="#">with</a> <a href="#">some</a>{' '}
                    <a href="#">focusable</a> elements.
                    <button onClick={() => setShowModal(false)}>
                        hide modal
                    </button>
                </div>
            </FocusTrap>
        </div>
    )
}

ReactDOM.render(<Demo />, document.getElementById('demo'))

I did a small write-up about the package here, which explains how to use it with either vanilla Javascript or React.

Dossal answered 18/10, 2020 at 11:7 Comment(0)
J
-1

One of the problems is that you are using keyup instead of keydown. The keyup will only fire after the tab has already fired. However, making that change to your code results in the keyboard being trapped on one of the links. The code is flawed.

Here is some code that does what you want (using jQuery)

http://dylanb.github.io/javascripts/periodic-1.1.js

// Add keyboard handling for TAB circling

  $modal.on('keydown', function (e) {
    var cancel = false;
    if (e.ctrlKey || e.metaKey || e.altKey) {
      return;
    }
    switch(e.which) {
      case 27: // ESC
        $modal.hide();
        lastfocus.focus();
        cancel = true;
        break;
      case 9: // TAB
        if (e.shiftKey) {
          if (e.target === links[0]) {
            links[links.length - 1].focus();
            cancel = true;
          }
        } else {
          if (e.target === links[links.length - 1]) {
            links[0].focus();
            cancel = true;
          }
        }
        break;
    }
    if (cancel) {
      e.preventDefault();
    }
  });

You can see a working version of this dialog here

http://dylanb.github.io/periodic-aria11-attributes.html

Click the text in one of the colored boxes to see the dialog pop up.

Jit answered 7/10, 2015 at 16:14 Comment(1)
Thanks, however I have already gotten it to work with jQuery. For the project I am working on I need to use plain javascript. Any ideas on how I can modify my current code to work?Detergency

© 2022 - 2024 — McMap. All rights reserved.