Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(dialog): trap focus within dialog. Fixes #4105.
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn committed Dec 2, 2015
1 parent ad4ccba commit fbb1192
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 2 deletions.
26 changes: 25 additions & 1 deletion src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
*/

function MdDialogProvider($$interimElementProvider) {
// Elements to capture and redirect focus when the user presses tab at the dialog boundary.
var topFocusTrap, bottomFocusTrap;

return $$interimElementProvider('$mdDialog')
.setDefaults({
Expand Down Expand Up @@ -588,9 +590,12 @@ function MdDialogProvider($$interimElementProvider) {
options.unlockScreenReader();
options.hideBackdrop(options.$destroy);

// Remove the focus traps that we added earlier for keeping focus within the dialog.
topFocusTrap.parentNode.removeChild(topFocusTrap);
bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);

// For navigation $destroy events, do a quick, non-animated removal,
// but for normal closes (from clicks, etc) animate the removal

return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );

/**
Expand Down Expand Up @@ -823,6 +828,25 @@ function MdDialogProvider($$interimElementProvider) {
return words.join(' ');
});
}

// Set up elements before and after the dialog content to capture focus and
// redirect back into the dialog.
topFocusTrap = document.createElement('div');
topFocusTrap.classList.add('md-dialog-focus-trap');
topFocusTrap.tabIndex = 0;

bottomFocusTrap = topFocusTrap.cloneNode(false);

// When focus is about to move out of the dialog, we want to intercept it and redirect it
// back to the dialog element.
var focusHandler = angular.bind(element, element.focus);
topFocusTrap.addEventListener('focus', focusHandler);
bottomFocusTrap.addEventListener('focus', focusHandler);

// The top focus trap inserted immeidately before the md-dialog element (as a sibling).
// The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
element[0].parentNode.insertBefore(topFocusTrap, element[0]);
element.append(bottomFocusTrap);
}

/**
Expand Down
49 changes: 48 additions & 1 deletion src/components/dialog/dialog.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
describe('$mdDialog', function() {
var $mdDialog, $rootScope;
var runAnimation;

beforeEach(module('material.components.dialog', 'ngSanitize'));
beforeEach(inject(function spyOnMdEffects($$q, $animate) {
beforeEach(inject(function($$q, $animate, $injector) {
$mdDialog = $injector.get('$mdDialog');
$rootScope = $injector.get('$rootScope');

// Spy on animation effects.
spyOn($animate, 'leave').and.callFake(function(element) {
element.remove();
return $$q.when();
Expand Down Expand Up @@ -1172,6 +1176,49 @@ describe('$mdDialog', function() {
var sibling = angular.element(parent[0].querySelector('.sibling'));
expect(sibling.attr('aria-hidden')).toBe('true');
}));

it('should trap focus inside of the dialog', function() {
var template = '<md-dialog>Hello <input></md-dialog>';
var parent = document.createElement('div');

// Append the parent to the DOM so that we can test focus behavior.
document.body.appendChild(parent);

$mdDialog.show({template: template, parent: parent});
$rootScope.$apply();

// It should add two focus traps to the document around the dialog content.
var focusTraps = parent.querySelectorAll('.md-dialog-focus-trap');
expect(focusTraps.length).toBe(2);

var topTrap = focusTraps[0];
var bottomTrap = focusTraps[1];

var dialog = parent.querySelector('md-dialog');
var isDialogFocused = false;
dialog.addEventListener('focus', function() {
isDialogFocused = true;
});

// Both of the focus traps should be in the normal tab order.
expect(topTrap.tabIndex).toBe(0);
expect(bottomTrap.tabIndex).toBe(0);

// TODO(jelbourn): Find a way to test that focusing the traps redirects focus to the
// md-dialog element. Firefox is problematic here, as calling element.focus() inside of
// a focus event listener seems not to immediately update the document.activeElement.
// This is a behavior better captured by an e2e test.

$mdDialog.hide();
runAnimation();

// All of the focus traps should be removed when the dialog is closed.
focusTraps = document.querySelectorAll('.md-dialog-focus-trap');
expect(focusTraps.length).toBe(0);

// Clean up our modifications to the DOM.
document.body.removeChild(parent);
});
});

function hasConfigurationMethods(preset, methods) {
Expand Down

0 comments on commit fbb1192

Please sign in to comment.