Skip to content

Commit

Permalink
fix(utils): extractElementByName() and findFocusTarget() logic improved
Browse files Browse the repository at this point in the history
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 angular#4532. Fixes angular#4497.
  • Loading branch information
ThomasBurleson authored and kennethcachia committed Sep 23, 2015
1 parent 882765c commit 346737e
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/components/sidenav/sidenav.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function SidenavFocusDirective() {
* By default, upon opening it will slide out on top of the main content area.
*
* For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
* It can be overridden with the `md-sidenav-focus` directive on the child element you want focused.
* It can be overridden with the `md-autofocus` directive on the child element you want focused.
*
* @usage
* <hljs lang="html">
Expand All @@ -178,7 +178,7 @@ function SidenavFocusDirective() {
* <md-input-container>
* <label for="testInput">Test input</label>
* <input id="testInput" type="text"
* ng-model="data" md-sidenav-focus>
* ng-model="data" md-autofocus>
* </md-input-container>
* </form>
* </md-sidenav>
Expand Down
2 changes: 1 addition & 1 deletion src/components/toast/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ function MdToastProvider($$interimElementProvider) {
function onShow(scope, element, options) {
activeToastContent = options.content;

element = $mdUtil.extractElementByName(element, 'md-toast');
element = $mdUtil.extractElementByName(element, 'md-toast', true);
options.onSwipe = function(ev, gesture) {
//Add swipeleft/swiperight class to element so it can animate correctly
element.addClass('md-' + ev.type.replace('$md.',''));
Expand Down
2 changes: 1 addition & 1 deletion src/core/util/animation/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ if (angular.version.minor >= 4) {

function parseMaxTime(str) {
var maxValue = 0;
var values = str.split(/\s*,\s*/);
var values = (str || "").split(/\s*,\s*/);
forEach(values, function(value) {
// it's always safe to consider only second values and omit `ms` values since
// getComputedStyle will always handle the conversion for us
Expand Down
110 changes: 89 additions & 21 deletions src/core/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,50 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
* </md-list>
* </md-bottom-sheet>
*</hljs>
*
**/
findFocusTarget: function(containerEl, attributeVal) {
var elToFocus, items = containerEl[0].querySelectorAll(attributeVal || '[md-autofocus]');
var AUTO_FOCUS = '[md-autofocus]';
var elToFocus;

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

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

if (isFocusable) elToFocus = it;
});
if ( !elToFocus ) {
// Scan for fallback to 'universal' API
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
}
}

return elToFocus;

/**
* Can target and nested children for specified Selector (attribute)
* whose value may be an expression that evaluates to True/False.
*/
function scanForFocusable(target, selector) {
var elFound, items = target[0].querySelectorAll(selector);

// Find the last child element with the focus attribute
if ( items && items.length ){
var EXP_ATTR = /\s*\[?([\-a-z]*)\]?\s*/i;
var matches = EXP_ATTR.exec(selector);
var attribute = matches ? matches[1] : null;

items.length && angular.forEach(items, function(it) {
it = angular.element(it);

// If the expression evaluates to FALSE, then it is not focusable target
var focusExpression = it[0].getAttribute(attribute);
var isFocusable = focusExpression ? (it.scope().$eval(focusExpression) !== false ) : true;

if (isFocusable) elFound = it;
});
}
return elFound;
}
},

// Disables scroll around the passed element.
Expand Down Expand Up @@ -168,11 +195,10 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
// (arrow keys, spacebar, tab, etc).
function disableKeyNav(e) {
//-- temporarily removed this logic, will possibly re-add at a later date
return;
if (!element[0].contains(e.target)) {
e.preventDefault();
e.stopImmediatePropagation();
}
//if (!element[0].contains(e.target)) {
// e.preventDefault();
// e.stopImmediatePropagation();
//}
}

function preventDefault(e) {
Expand Down Expand Up @@ -455,16 +481,58 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
/**
* Functional equivalent for $element.filter(‘md-bottom-sheet’)
* useful with interimElements where the element and its container are important...
*
* @param {[]} elements to scan
* @param {string} name of node to find (e.g. 'md-dialog')
* @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
*/
extractElementByName: function(element, nodeName) {
for (var i = 0, len = element.length; i < len; i++) {
if (element[i].nodeName.toLowerCase() === nodeName) {
return angular.element(element[i]);
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
var found = scanTree(element);
if (!found && !!warnNotFound) {
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element.",[nodeName]) );
}

return angular.element(found || element);

/**
* Breadth-First tree scan for element with matching `nodeName`
*/
function scanTree(element) {
return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
}

/**
* Case-insensitive scan of current elements only (do not descend).
*/
function scanLevel(element) {
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
if (element[i].nodeName.toLowerCase() === nodeName) {
return element[i];
}
}
}
return null;
}

/**
* Scan children of specified node
*/
function scanChildren(element) {
var found;
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
var target = element[i];
if ( !found ) {
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
found = found || scanTree([target.childNodes[j]]);
}
}
}
}
return found;
}

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

/**
Expand Down
88 changes: 88 additions & 0 deletions src/core/util/util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,94 @@ describe('util', function() {

});


describe('findFocusTarget', function() {

it('should not find valid focus target', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div class="autoFocus"><button><img></button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget);

expect(target).toBeFalsy();
}));

it('should find valid a valid focusTarget with "md-autofocus"', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div class="autoFocus"><button md-autofocus><img></button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget);

expect(target[0].nodeName).toBe("BUTTON");
}));

it('should find valid a valid focusTarget with "md-auto-focus"', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div class="autoFocus"><button md-auto-focus><img></button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget);

expect(target[0].nodeName).toBe("BUTTON");
}));

it('should find valid a valid focusTarget with "md-auto-focus" argument', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div class="autoFocus"><button md-autofocus><img></button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget,'[md-auto-focus]');

expect(target[0].nodeName).toBe("BUTTON");
}));

it('should find valid a valid focusTarget with a deep "md-autofocus" argument', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div class="autoFocus"><md-sidenav><button md-autofocus><img></button></md-sidenav></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget);

expect(target[0].nodeName).toBe("BUTTON");
}));

it('should find valid a valid focusTarget with a deep "md-sidenav-focus" argument', inject(function($rootScope, $compile, $mdUtil) {
var template = '' +
'<div class="autoFocus">' +
' <md-sidenav>' +
' <button md-sidenav-focus>' +
' <img>' +
' </button>' +
' </md-sidenav>' +
'</div>';
var widget = $compile(template)($rootScope);
$rootScope.$apply();
var target = $mdUtil.findFocusTarget(widget,'[md-sidenav-focus]');

expect(target[0].nodeName).toBe("BUTTON");
}));
});

describe('extractElementByname', function() {

it('should not find valid element', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div><md-button1><img></md-button1></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.extractElementByName(widget, 'md-button');

// Returns same element
expect( target === widget ).toBe(true);
}));

it('should not find valid element for shallow scan', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div><md-button><img></md-button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.extractElementByName(widget, 'md-button');

expect( target !== widget ).toBe(false);
}));

it('should find valid element for deep scan', inject(function($rootScope, $compile, $mdUtil) {
var widget = $compile('<div><md-button><img></md-button></div>')($rootScope);
$rootScope.$apply();
var target = $mdUtil.extractElementByName(widget, 'md-button', true);

expect( target !== widget ).toBe(true);
}));
});

describe('throttle', function() {
var delay = 500;
var nowMockValue;
Expand Down

0 comments on commit 346737e

Please sign in to comment.