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

Commit 8f756fc

Browse files
Marcy Suttonajoslin
authored andcommitted
feat(material-dialog): on open focus .dialog-close or the last button
Closes #222
1 parent b27ed36 commit 8f756fc

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

src/base/test-utils.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,28 @@ var TestUtil = {
99
e.which = eventData.keyCode;
1010
}
1111
element.trigger(e);
12+
},
13+
14+
/**
15+
* Mocks angular.element#focus for the duration of the test
16+
* @example
17+
* it('some focus test', inject(function($document) {
18+
* TestUtil.mockFocus(this); // 'this' is the test instance
19+
* doSomething();
20+
* expect($document.activeElement).toBe(someElement[0]);
21+
* }));
22+
*/
23+
mockElementFocus: function(test) {
24+
var focus = angular.element.prototype.focus;
25+
inject(function($document) {
26+
angular.element.prototype.focus = function() {
27+
$document.activeElement = this[0];
28+
};
29+
});
30+
// Un-mock focus after the test is done
31+
test.after(function() {
32+
angular.element.prototype.focus = focus;
33+
});
1234
}
35+
1336
};

src/components/buttons/buttons.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,24 @@ function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $aria ) {
7575

7676
innerElement
7777
.addClass('material-button-inner')
78-
.append(element.contents());
79-
element.append(innerElement);
78+
.append(element.contents())
79+
// Since we're always passing focus to the inner element,
80+
// add a focus class to the outer element so we can still style
81+
// it with focus.
82+
.on('focus', function() {
83+
element.addClass('focus');
84+
})
85+
.on('blur', function() {
86+
element.removeClass('focus');
87+
});
88+
89+
element.
90+
append(innerElement)
91+
.attr('tabIndex', -1)
92+
//Always pass focus to innerElement
93+
.on('focus', function() {
94+
innerElement.focus();
95+
});
8096

8197
return function postLink(scope, element, attr) {
8298
$aria.expect(element, 'aria-label', element.text());
@@ -85,13 +101,6 @@ function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $aria ) {
85101
element.on('focus', function() {
86102
innerElement.focus();
87103
});
88-
innerElement
89-
.on('focus', function() {
90-
element.addClass('focus');
91-
})
92-
.on('blur', function() {
93-
element.removeClass('focus');
94-
});
95104
};
96105
}
97106
};

src/components/dialog/dialog.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ function MaterialDialogDirective($$rAF) {
4343
*
4444
* The $materialDialog service opens a dialog over top of the app.
4545
*
46-
* See the overview page for an example.
47-
*
4846
* The `$materialDialog` service can be used as a function, which when called will open a
4947
* dialog. Note: the dialog is always given an isolate scope.
5048
*
5149
* It takes one argument, `options`, which is defined below.
5250
*
51+
* Note: the dialog's template must have an outer `<material-dialog>` element.
52+
* Inside, use an element with class `dialog-content` for the dialog's content, and use
53+
* an element with class `dialog-actions` for the dialog's actions.
54+
*
55+
* When opened, the `dialog-actions` area will attempt to focus the first button found with
56+
* class `dialog-close`. If no button with `dialog-close` class is found, it will focus the
57+
* last button in the `dialog-actions` area.
58+
*
5359
* @usage
5460
* <hljs lang="html">
5561
* <div ng-controller="MyController">
@@ -137,6 +143,7 @@ function MaterialDialogService($timeout, $materialCompiler, $rootElement, $rootS
137143
var element = compileData.link(scope);
138144
var popInTarget = options.targetEvent && options.targetEvent.target &&
139145
angular.element(options.targetEvent.target);
146+
var closeButton = findCloseButton();
140147
var backdrop;
141148

142149
if (options.hasBackdrop) {
@@ -150,10 +157,21 @@ function MaterialDialogService($timeout, $materialCompiler, $rootElement, $rootS
150157
if (options.clickOutsideToClose) {
151158
element.on('click', dialogClickOutside);
152159
}
160+
closeButton.focus();
153161
});
154162

155163
return destroyDialog;
156164

165+
function findCloseButton() {
166+
//If no element with class dialog-close, try to find the last
167+
//button child in dialog-actions and assume it is a close button
168+
var closeButton = element[0].querySelector('.dialog-close');
169+
if (!closeButton) {
170+
var actionButtons = element[0].querySelectorAll('.dialog-actions button');
171+
closeButton = actionButtons[ actionButtons.length - 1 ];
172+
}
173+
return angular.element(closeButton);
174+
}
157175
function destroyDialog() {
158176
if (destroyDialog.called) return;
159177
destroyDialog.called = true;

src/components/dialog/dialog.spec.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ describe('$materialDialog', function() {
33
beforeEach(module('material.components.dialog'));
44

55
beforeEach(inject(function spyOnMaterialEffects($materialEffects) {
6-
spyOn($materialEffects, 'popOut').andCallFake(function(element, parent, cb) {
7-
cb();
8-
});
96
spyOn($materialEffects, 'popIn').andCallFake(function(element, parent, targetEvent, cb) {
107
parent.append(element);
118
cb();
129
});
10+
spyOn($materialEffects, 'popOut').andCallFake(function(element, parent, cb) {
11+
cb();
12+
});
1313
}));
1414

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

138+
it('should focus `material-button.dialog-close` on open', inject(function($materialDialog, $rootScope, $document) {
139+
TestUtil.mockElementFocus(this);
140+
141+
var parent = angular.element('<div>');
142+
$materialDialog({
143+
template:
144+
'<material-dialog>' +
145+
'<div class="dialog-actions">' +
146+
'<button class="dialog-close">Close</button>' +
147+
'</div>' +
148+
'</material-dialog>',
149+
appendTo: parent
150+
});
151+
152+
$rootScope.$apply();
153+
154+
expect($document.activeElement).toBe(parent.find('.dialog-close')[0]);
155+
}));
156+
157+
it('should focus the last `material-button` in dialog-actions open if no `.dialog-close`', inject(function($materialDialog, $rootScope, $document) {
158+
TestUtil.mockElementFocus(this);
159+
160+
var parent = angular.element('<div>');
161+
$materialDialog({
162+
template:
163+
'<material-dialog>' +
164+
'<div class="dialog-actions">' +
165+
'<button id="a">A</material-button>' +
166+
'<button id="focus-target">B</material-button>' +
167+
'</div>' +
168+
'</material-dialog>',
169+
appendTo: parent
170+
});
171+
172+
$rootScope.$apply();
173+
174+
expect($document.activeElement).toBe(parent.find('#focus-target')[0]);
175+
}));
176+
138177
it('should only allow one open at a time', inject(function($materialDialog, $rootScope) {
139178
var parent = angular.element('<div>');
140179
$materialDialog({

0 commit comments

Comments
 (0)