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

Commit a5d84c3

Browse files
fix(utils): extractElementByName() and findFocusTarget() logic improved
findFocusTarget() scans deep and properly uses `$eval()` on possible focus expression. extractElementByName() includes optional argument to scan deep in all child nodes. added unit tests Fixes #4532. Fixes #4497.
1 parent 46c7b18 commit a5d84c3

File tree

5 files changed

+181
-25
lines changed

5 files changed

+181
-25
lines changed

src/components/sidenav/sidenav.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function SidenavFocusDirective() {
155155
* By default, upon opening it will slide out on top of the main content area.
156156
*
157157
* For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
158-
* It can be overridden with the `md-sidenav-focus` directive on the child element you want focused.
158+
* It can be overridden with the `md-autofocus` directive on the child element you want focused.
159159
*
160160
* @usage
161161
* <hljs lang="html">
@@ -178,7 +178,7 @@ function SidenavFocusDirective() {
178178
* <md-input-container>
179179
* <label for="testInput">Test input</label>
180180
* <input id="testInput" type="text"
181-
* ng-model="data" md-sidenav-focus>
181+
* ng-model="data" md-autofocus>
182182
* </md-input-container>
183183
* </form>
184184
* </md-sidenav>

src/components/toast/toast.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ function MdToastProvider($$interimElementProvider) {
231231
function onShow(scope, element, options) {
232232
activeToastContent = options.content;
233233

234-
element = $mdUtil.extractElementByName(element, 'md-toast');
234+
element = $mdUtil.extractElementByName(element, 'md-toast', true);
235235
options.onSwipe = function(ev, gesture) {
236236
//Add swipeleft/swiperight class to element so it can animate correctly
237237
element.addClass('md-' + ev.type.replace('$md.',''));

src/core/util/animation/animateCss.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ if (angular.version.minor >= 4) {
315315

316316
function parseMaxTime(str) {
317317
var maxValue = 0;
318-
var values = str.split(/\s*,\s*/);
318+
var values = (str || "").split(/\s*,\s*/);
319319
forEach(values, function(value) {
320320
// it's always safe to consider only second values and omit `ms` values since
321321
// getComputedStyle will always handle the conversion for us

src/core/util/util.js

+89-21
Original file line numberDiff line numberDiff line change
@@ -104,23 +104,50 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
104104
* </md-list>
105105
* </md-bottom-sheet>
106106
*</hljs>
107-
*
108107
**/
109108
findFocusTarget: function(containerEl, attributeVal) {
110-
var elToFocus, items = containerEl[0].querySelectorAll(attributeVal || '[md-autofocus]');
109+
var AUTO_FOCUS = '[md-autofocus]';
110+
var elToFocus;
111111

112-
// Find the last child element with the focus attribute
113-
items.length && angular.forEach(items, function(it) {
114-
it = angular.element(it);
112+
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
115113

116-
// If the expression evaluates to FALSE, then it is not focusable target
117-
var focusExpression = it[0].getAttribute('md-autofocus');
118-
var isFocusable = focusExpression ? (it.scope().$eval(focusExpression) !== false ) : true;
114+
if ( !elToFocus && attributeVal != AUTO_FOCUS) {
115+
// Scan for deprecated attribute
116+
elToFocus = scanForFocusable(containerEl, '[md-auto-focus]');
119117

120-
if (isFocusable) elToFocus = it;
121-
});
118+
if ( !elToFocus ) {
119+
// Scan for fallback to 'universal' API
120+
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
121+
}
122+
}
122123

123124
return elToFocus;
125+
126+
/**
127+
* Can target and nested children for specified Selector (attribute)
128+
* whose value may be an expression that evaluates to True/False.
129+
*/
130+
function scanForFocusable(target, selector) {
131+
var elFound, items = target[0].querySelectorAll(selector);
132+
133+
// Find the last child element with the focus attribute
134+
if ( items && items.length ){
135+
var EXP_ATTR = /\s*\[?([\-a-z]*)\]?\s*/i;
136+
var matches = EXP_ATTR.exec(selector);
137+
var attribute = matches ? matches[1] : null;
138+
139+
items.length && angular.forEach(items, function(it) {
140+
it = angular.element(it);
141+
142+
// If the expression evaluates to FALSE, then it is not focusable target
143+
var focusExpression = it[0].getAttribute(attribute);
144+
var isFocusable = focusExpression ? (it.scope().$eval(focusExpression) !== false ) : true;
145+
146+
if (isFocusable) elFound = it;
147+
});
148+
}
149+
return elFound;
150+
}
124151
},
125152

126153
// Disables scroll around the passed element.
@@ -168,11 +195,10 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
168195
// (arrow keys, spacebar, tab, etc).
169196
function disableKeyNav(e) {
170197
//-- temporarily removed this logic, will possibly re-add at a later date
171-
return;
172-
if (!element[0].contains(e.target)) {
173-
e.preventDefault();
174-
e.stopImmediatePropagation();
175-
}
198+
//if (!element[0].contains(e.target)) {
199+
// e.preventDefault();
200+
// e.stopImmediatePropagation();
201+
//}
176202
}
177203

178204
function preventDefault(e) {
@@ -455,16 +481,58 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
455481
/**
456482
* Functional equivalent for $element.filter(‘md-bottom-sheet’)
457483
* useful with interimElements where the element and its container are important...
484+
*
485+
* @param {[]} elements to scan
486+
* @param {string} name of node to find (e.g. 'md-dialog')
487+
* @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
458488
*/
459-
extractElementByName: function(element, nodeName) {
460-
for (var i = 0, len = element.length; i < len; i++) {
461-
if (element[i].nodeName.toLowerCase() === nodeName) {
462-
return angular.element(element[i]);
489+
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
490+
var found = scanTree(element);
491+
if (!found && !!warnNotFound) {
492+
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element.",[nodeName]) );
493+
}
494+
495+
return angular.element(found || element);
496+
497+
/**
498+
* Breadth-First tree scan for element with matching `nodeName`
499+
*/
500+
function scanTree(element) {
501+
return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
502+
}
503+
504+
/**
505+
* Case-insensitive scan of current elements only (do not descend).
506+
*/
507+
function scanLevel(element) {
508+
if ( element ) {
509+
for (var i = 0, len = element.length; i < len; i++) {
510+
if (element[i].nodeName.toLowerCase() === nodeName) {
511+
return element[i];
512+
}
513+
}
514+
}
515+
return null;
516+
}
517+
518+
/**
519+
* Scan children of specified node
520+
*/
521+
function scanChildren(element) {
522+
var found;
523+
if ( element ) {
524+
for (var i = 0, len = element.length; i < len; i++) {
525+
var target = element[i];
526+
if ( !found ) {
527+
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
528+
found = found || scanTree([target.childNodes[j]]);
529+
}
530+
}
531+
}
463532
}
533+
return found;
464534
}
465535

466-
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element.",[nodeName]) );
467-
return element;
468536
},
469537

470538
/**

src/core/util/util.spec.js

+88
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,94 @@ describe('util', function() {
4646

4747
});
4848

49+
50+
describe('findFocusTarget', function() {
51+
52+
it('should not find valid focus target', inject(function($rootScope, $compile, $mdUtil) {
53+
var widget = $compile('<div class="autoFocus"><button><img></button></div>')($rootScope);
54+
$rootScope.$apply();
55+
var target = $mdUtil.findFocusTarget(widget);
56+
57+
expect(target).toBeFalsy();
58+
}));
59+
60+
it('should find valid a valid focusTarget with "md-autofocus"', inject(function($rootScope, $compile, $mdUtil) {
61+
var widget = $compile('<div class="autoFocus"><button md-autofocus><img></button></div>')($rootScope);
62+
$rootScope.$apply();
63+
var target = $mdUtil.findFocusTarget(widget);
64+
65+
expect(target[0].nodeName).toBe("BUTTON");
66+
}));
67+
68+
it('should find valid a valid focusTarget with "md-auto-focus"', inject(function($rootScope, $compile, $mdUtil) {
69+
var widget = $compile('<div class="autoFocus"><button md-auto-focus><img></button></div>')($rootScope);
70+
$rootScope.$apply();
71+
var target = $mdUtil.findFocusTarget(widget);
72+
73+
expect(target[0].nodeName).toBe("BUTTON");
74+
}));
75+
76+
it('should find valid a valid focusTarget with "md-auto-focus" argument', inject(function($rootScope, $compile, $mdUtil) {
77+
var widget = $compile('<div class="autoFocus"><button md-autofocus><img></button></div>')($rootScope);
78+
$rootScope.$apply();
79+
var target = $mdUtil.findFocusTarget(widget,'[md-auto-focus]');
80+
81+
expect(target[0].nodeName).toBe("BUTTON");
82+
}));
83+
84+
it('should find valid a valid focusTarget with a deep "md-autofocus" argument', inject(function($rootScope, $compile, $mdUtil) {
85+
var widget = $compile('<div class="autoFocus"><md-sidenav><button md-autofocus><img></button></md-sidenav></div>')($rootScope);
86+
$rootScope.$apply();
87+
var target = $mdUtil.findFocusTarget(widget);
88+
89+
expect(target[0].nodeName).toBe("BUTTON");
90+
}));
91+
92+
it('should find valid a valid focusTarget with a deep "md-sidenav-focus" argument', inject(function($rootScope, $compile, $mdUtil) {
93+
var template = '' +
94+
'<div class="autoFocus">' +
95+
' <md-sidenav>' +
96+
' <button md-sidenav-focus>' +
97+
' <img>' +
98+
' </button>' +
99+
' </md-sidenav>' +
100+
'</div>';
101+
var widget = $compile(template)($rootScope);
102+
$rootScope.$apply();
103+
var target = $mdUtil.findFocusTarget(widget,'[md-sidenav-focus]');
104+
105+
expect(target[0].nodeName).toBe("BUTTON");
106+
}));
107+
});
108+
109+
describe('extractElementByname', function() {
110+
111+
it('should not find valid element', inject(function($rootScope, $compile, $mdUtil) {
112+
var widget = $compile('<div><md-button1><img></md-button1></div>')($rootScope);
113+
$rootScope.$apply();
114+
var target = $mdUtil.extractElementByName(widget, 'md-button');
115+
116+
// Returns same element
117+
expect( target === widget ).toBe(true);
118+
}));
119+
120+
it('should not find valid element for shallow scan', inject(function($rootScope, $compile, $mdUtil) {
121+
var widget = $compile('<div><md-button><img></md-button></div>')($rootScope);
122+
$rootScope.$apply();
123+
var target = $mdUtil.extractElementByName(widget, 'md-button');
124+
125+
expect( target !== widget ).toBe(false);
126+
}));
127+
128+
it('should find valid element for deep scan', inject(function($rootScope, $compile, $mdUtil) {
129+
var widget = $compile('<div><md-button><img></md-button></div>')($rootScope);
130+
$rootScope.$apply();
131+
var target = $mdUtil.extractElementByName(widget, 'md-button', true);
132+
133+
expect( target !== widget ).toBe(true);
134+
}));
135+
});
136+
49137
describe('throttle', function() {
50138
var delay = 500;
51139
var nowMockValue;

0 commit comments

Comments
 (0)