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

Commit

Permalink
feat(material-dialog): on open focus .dialog-close or the last button
Browse files Browse the repository at this point in the history
Closes #222
  • Loading branch information
Marcy Sutton authored and ajoslin committed Aug 30, 2014
1 parent b27ed36 commit 8f756fc
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 14 deletions.
23 changes: 23 additions & 0 deletions src/base/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,28 @@ var TestUtil = {
e.which = eventData.keyCode;
}
element.trigger(e);
},

/**
* Mocks angular.element#focus for the duration of the test
* @example
* it('some focus test', inject(function($document) {
* TestUtil.mockFocus(this); // 'this' is the test instance
* doSomething();
* expect($document.activeElement).toBe(someElement[0]);
* }));
*/
mockElementFocus: function(test) {
var focus = angular.element.prototype.focus;
inject(function($document) {
angular.element.prototype.focus = function() {
$document.activeElement = this[0];
};
});
// Un-mock focus after the test is done
test.after(function() {
angular.element.prototype.focus = focus;
});
}

};
27 changes: 18 additions & 9 deletions src/components/buttons/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,24 @@ function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $aria ) {

innerElement
.addClass('material-button-inner')
.append(element.contents());
element.append(innerElement);
.append(element.contents())
// Since we're always passing focus to the inner element,
// add a focus class to the outer element so we can still style
// it with focus.
.on('focus', function() {
element.addClass('focus');
})
.on('blur', function() {
element.removeClass('focus');
});

element.
append(innerElement)
.attr('tabIndex', -1)
//Always pass focus to innerElement
.on('focus', function() {
innerElement.focus();
});

return function postLink(scope, element, attr) {
$aria.expect(element, 'aria-label', element.text());
Expand All @@ -85,13 +101,6 @@ function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $aria ) {
element.on('focus', function() {
innerElement.focus();
});
innerElement
.on('focus', function() {
element.addClass('focus');
})
.on('blur', function() {
element.removeClass('focus');
});
};
}
};
Expand Down
22 changes: 20 additions & 2 deletions src/components/dialog/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@ function MaterialDialogDirective($$rAF) {
*
* The $materialDialog service opens a dialog over top of the app.
*
* See the overview page for an example.
*
* The `$materialDialog` service can be used as a function, which when called will open a
* dialog. Note: the dialog is always given an isolate scope.
*
* It takes one argument, `options`, which is defined below.
*
* Note: the dialog's template must have an outer `<material-dialog>` element.
* Inside, use an element with class `dialog-content` for the dialog's content, and use
* an element with class `dialog-actions` for the dialog's actions.
*
* When opened, the `dialog-actions` area will attempt to focus the first button found with
* class `dialog-close`. If no button with `dialog-close` class is found, it will focus the
* last button in the `dialog-actions` area.
*
* @usage
* <hljs lang="html">
* <div ng-controller="MyController">
Expand Down Expand Up @@ -137,6 +143,7 @@ function MaterialDialogService($timeout, $materialCompiler, $rootElement, $rootS
var element = compileData.link(scope);
var popInTarget = options.targetEvent && options.targetEvent.target &&
angular.element(options.targetEvent.target);
var closeButton = findCloseButton();
var backdrop;

if (options.hasBackdrop) {
Expand All @@ -150,10 +157,21 @@ function MaterialDialogService($timeout, $materialCompiler, $rootElement, $rootS
if (options.clickOutsideToClose) {
element.on('click', dialogClickOutside);
}
closeButton.focus();
});

return destroyDialog;

function findCloseButton() {
//If no element with class dialog-close, try to find the last
//button child in dialog-actions and assume it is a close button
var closeButton = element[0].querySelector('.dialog-close');
if (!closeButton) {
var actionButtons = element[0].querySelectorAll('.dialog-actions button');
closeButton = actionButtons[ actionButtons.length - 1 ];
}
return angular.element(closeButton);
}
function destroyDialog() {
if (destroyDialog.called) return;
destroyDialog.called = true;
Expand Down
45 changes: 42 additions & 3 deletions src/components/dialog/dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ describe('$materialDialog', function() {
beforeEach(module('material.components.dialog'));

beforeEach(inject(function spyOnMaterialEffects($materialEffects) {
spyOn($materialEffects, 'popOut').andCallFake(function(element, parent, cb) {
cb();
});
spyOn($materialEffects, 'popIn').andCallFake(function(element, parent, targetEvent, cb) {
parent.append(element);
cb();
});
spyOn($materialEffects, 'popOut').andCallFake(function(element, parent, cb) {
cb();
});
}));

it('should append dialog with container', inject(function($materialDialog, $rootScope) {
Expand Down Expand Up @@ -135,6 +135,45 @@ describe('$materialDialog', function() {
expect(parent.find('material-backdrop').length).toBe(0);
}));

it('should focus `material-button.dialog-close` on open', inject(function($materialDialog, $rootScope, $document) {
TestUtil.mockElementFocus(this);

var parent = angular.element('<div>');
$materialDialog({
template:
'<material-dialog>' +
'<div class="dialog-actions">' +
'<button class="dialog-close">Close</button>' +
'</div>' +
'</material-dialog>',
appendTo: parent
});

$rootScope.$apply();

expect($document.activeElement).toBe(parent.find('.dialog-close')[0]);
}));

it('should focus the last `material-button` in dialog-actions open if no `.dialog-close`', inject(function($materialDialog, $rootScope, $document) {
TestUtil.mockElementFocus(this);

var parent = angular.element('<div>');
$materialDialog({
template:
'<material-dialog>' +
'<div class="dialog-actions">' +
'<button id="a">A</material-button>' +
'<button id="focus-target">B</material-button>' +
'</div>' +
'</material-dialog>',
appendTo: parent
});

$rootScope.$apply();

expect($document.activeElement).toBe(parent.find('#focus-target')[0]);
}));

it('should only allow one open at a time', inject(function($materialDialog, $rootScope) {
var parent = angular.element('<div>');
$materialDialog({
Expand Down

0 comments on commit 8f756fc

Please sign in to comment.