question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Modal needs to reliably trap focus so that Carbon users don't have to

See original GitHub issue

Three test cases to show the problem:

  1. https://codepen.io/team/carbon/pen/dZVoPj

    • open the modal
    • type tab a number of times, and focus will exit the modal (shift+tab exits modal and does not wrap)
  2. https://www.carbondesignsystem.com/components/modal/code

    • open any of the modals
    • type tab until it looks as if “nothing” has focus
    • now use the up/down arrow keys… the background scrolls…
  3. https://www.carbondesignsystem.com/components/modal/code

    • open any of the modals
    • type shift+tab until it looks as if “nothing” has focus (keep typing shift+tab - it does not wrap)
    • now use the up/down arrow keys… the background scrolls…

Related issues: https://github.com/IBM/carbon-components/issues/1075, https://github.com/IBM/carbon-components/issues/1474

Note that this also comes up in the forums from time to time, typically after accessibility testing.

Users of Carbon Modal eventually find out that traversing with tab or shift+tab escapes the modal. Carbon should take care of this in a robust way, so that all Carbon users get the proper behavior for free, and they don’t have to implement their own focus trap, or override Carbon code, to fix the problem.

One way to handle this is to handle the tab and shift+tab keys in a keydown event handler:

function confineTabToDialog(firstElement, lastElement) {    
        lastElement.keydown(function(evt) {
            if(evt.keyCode === 9 && !evt.shiftKey) {
                evt.preventDefault();
                firstElement.focus();
            }
        });
        firstElement.keydown(function(evt) {
            if(evt.keyCode === 9 && evt.shiftKey) {
                evt.preventDefault();
                lastElement.focus();
            }
        });
}

You can specify a way for the Carbon Modal user to tell you which elements firstElement and lastElement are. (i.e. with props or class names). This is the easiest way to get the first tabbable element and the last tabbable element in the dialog.

Alternatively, you can do the work automatically for the Carbon Modal user, and loop through the Modal’s descendant elements to find the first and last tabbable element in the Modal. Note: Elements with a negative tab index are focusable, but not tabbable.

Here’s some code from the APG’s Modal Dialog example (found in their utils.js which is called from dialog.js):

aria.Utils.isFocusable = function (element) {
  if (element.tabIndex > 0 || (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)) {
    return true;
  }

  if (element.disabled) {
    return false;
  }

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

Note: The APG isFocusable code may be missing certain cases of OBJECT and AREA, so the jQuery code below may cover more types of element.

Here is similar code from jQuery’s :tabbable and :focusable selectors in their jquery-ui.js file:

var tabbable = $.extend( $.expr[ ":" ], {
	tabbable: function( element ) {
		var tabIndex = $.attr( element, "tabindex" ),
			hasTabindex = tabIndex != null;
		return ( !hasTabindex || tabIndex >= 0 ) && $.ui.focusable( element, hasTabindex );
	}
} );
$.ui.focusable = function( element, hasTabindex ) {
	var map, mapName, img, focusableIfVisible, fieldset,
		nodeName = element.nodeName.toLowerCase();

	if ( "area" === nodeName ) {
		map = element.parentNode;
		mapName = map.name;
		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
			return false;
		}
		img = $( "img[usemap='#" + mapName + "']" );
		return img.length > 0 && img.is( ":visible" );
	}

	if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) {
		focusableIfVisible = !element.disabled;

		if ( focusableIfVisible ) {

			// Form controls within a disabled fieldset are disabled.
			// However, controls within the fieldset's legend do not get disabled.
			// Since controls generally aren't placed inside legends, we skip
			// this portion of the check.
			fieldset = $( element ).closest( "fieldset" )[ 0 ];
			if ( fieldset ) {
				focusableIfVisible = !fieldset.disabled;
			}
		}
	} else if ( "a" === nodeName ) {
		focusableIfVisible = element.href || hasTabindex;
	} else {
		focusableIfVisible = hasTabindex;
	}

	return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) );
};

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:10 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
joshblackcommented, Jan 15, 2019

The current Material Components method allows accessibility failure by default, and hopes that the developer notices the focus trap api and understands why they need to use it (or find an alternative). Even their own dialog example does not implement the focus trap.

Even when looking for it on the page, I had a hard time locating the focus trap guidance, unfortunately.

I do think there is a good opportunity, at least in React, to provide this stuff by default. For Vanilla.js, would love to find a way to support it by default with an opt-out. As @carmacleod mentioned, we should try and aim for accessible by default. What kinds of things can we offer that could work here to accomplish that?

1reaction
carmacleodcommented, Jan 15, 2019

Very interesting, @asudoh - thanks.

So, I think you could go one better than Material Components if you create the focus trap by default, and provide an API called disableFocusTrap that has a comment saying something like, “Use this to disable the focus trap if you are using another way to trap focus, like inert (which would/will solve this problem completely, by the way 😉 or some other framework.”

The current Material Components method allows accessibility failure by default, and hopes that the developer notices the focus trap api and understands why they need to use it (or find an alternative). Even their own dialog example does not implement the focus trap.

In case it’s helpful, here is the focus-trap code that they are using, and I see that this code uses yet another implementation of tabbable.

If you decide to implement it the same way as Material Components (i.e. trapFocus and releaseFocus), I would be ok with that, as long as your examples use it, and you have a nice obvious comment that says a focus trap is required for accessibility. 😃

By the way, I just noticed that your React Modal does not have aria-modal="true" - that’s a bug.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to trap focus inside modal to make it ADA compliant
As you can see, once the modal is opened, when we keep pressing tab, each element of the modal is focused but as...
Read more >
Vital Signs of the Planet - Evidence | Facts – Climate Change
Scientists demonstrated the heat-trapping nature of carbon dioxide and other gases in the mid-19th century. Many of the science instruments NASA uses to...
Read more >
How To Trap Focus in a Modal in Vue 3 - Telerik
Learn how to create an animated modal with trapped focus in Vue 3. ... First, we need to set up a new Vue...
Read more >
Will the Inflation Reduction Act jumpstart carbon capture? - Grist
The Inflation Reduction Act makes 45Q tax credits for carbon capture more lucrative and easier to access. But its future is still uncertain....
Read more >
Chapter 4 — Global Warming of 1.5 ºC - IPCC
for example 19 million people in Bangladesh now have solar-battery ... Climate policy in many Amazonian nations has focused on forests as carbon...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found