Skip to content

Commit

Permalink
feat(gridUtil): Focus helper functions
Browse files Browse the repository at this point in the history
Adds a few helper functions to assist with focus management.
Makes the focus return promises and queue
Focus methods return promises that resolve themselves when the focus
either suceseeds or fails.
Additionally, the promises queue and cancel eachother when mutiple
focus events are requested before the timeout is purged.
  • Loading branch information
JLLeitschuh committed Jul 27, 2015
1 parent e5c8299 commit 94e50a5
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
115 changes: 115 additions & 0 deletions src/js/core/services/ui-grid-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,121 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC

};

/**
* @ngdoc object
* @name focus
* @propertyOf ui.grid.service:GridUtil
* @description Provies a set of methods to set the document focus inside the grid.
* See {@link ui.grid.service:GridUtil.focus} for more information.
*/

/**
* @ngdoc object
* @name ui.grid.service:GridUtil.focus
* @description Provies a set of methods to set the document focus inside the grid.
* Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
* e.g. click events that need to run before the focus or
* inputs elements that are in a disabled state but are enabled when those events
* are triggered.
*/
s.focus = {
queue: [],
//http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
/**
* @ngdoc method
* @methodOf ui.grid.service:GridUtil.focus
* @name byId
* @description Sets the focus of the document to the given id value.
* If provided with the grid object it will automatically append the grid id.
* This is done to encourage unique dom id's as it allows for multiple grids on a
* page.
* @param {String} id the id of the dom element to set the focus on
* @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
* @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
* then the promise will fail with the `'canceled'` reason.
*/
byId: function (id, Grid) {
this._purgeQueue();
var promise = $timeout(function() {
var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
var element = $window.document.getElementById(elementID);
if (element) {
element.focus();
} else {
s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
}
});
this.queue.push(promise);
return promise;
},

/**
* @ngdoc method
* @methodOf ui.grid.service:GridUtil.focus
* @name byElement
* @description Sets the focus of the document to the given dom element.
* @param {(element|angular.element)} element the DOM element to set the focus on
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
* then the promise will fail with the `'canceled'` reason.
*/
byElement: function(element){
if (!angular.isElement(element)){
s.logWarn("Trying to focus on an element that isn\'t an element.");
return $q.reject('not-element');
}
element = angular.element(element);
this._purgeQueue();
var promise = $timeout(function(){
if (element){
element[0].focus();
}
});
this.queue.push(promise);
return promise;
},
/**
* @ngdoc method
* @methodOf ui.grid.service:GridUtil.focus
* @name bySelector
* @description Sets the focus of the document to the given dom element.
* @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
* @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
* @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
* then the focus will be called.
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
* then the promise will fail with the `'canceled'` reason.
*/
bySelector: function(parentElement, querySelector, aSync){
var self = this;
if (!angular.isElement(parentElement)){
throw new Error("The parent element is not an element.");
}
// Ensure that this is an angular element.
// It is fine if this is already an angular element.
parentElement = angular.element(parentElement);
var focusBySelector = function(){
var element = parentElement[0].querySelector(querySelector);
return self.byElement(element);
};
this._purgeQueue();
if (aSync){ //Do this asynchronysly
var promise = $timeout(focusBySelector);
this.queue.push($timeout(focusBySelector));
return promise;
} else {
return focusBySelector();
}
},
_purgeQueue: function(){
this.queue.forEach(function(element){
$timeout.cancel(element);
});
this.queue = [];
}
};


['width', 'height'].forEach(function (name) {
var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
s['element' + capsName] = function (elem, extra) {
Expand Down
90 changes: 90 additions & 0 deletions test/unit/core/services/ui-grid-util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,96 @@ describe('ui.grid.utilService', function() {
});
});

describe('focus', function(){
var $timeout;
var elm;
var button1, aButton1, button1classUnset = 'ui-grid-button1';
var button2, aButton2, button2class = 'ui-grid-button2';
beforeEach(inject(function(_$timeout_){
$timeout = _$timeout_;
elm = document.createElement('div');

/* Create Button1 */
button1 = document.createElement('button');
aButton1 = angular.element(button1);
aButton1.attr('type', 'button');
// The class is not set here because it is set inside of tests if needed

/* Create Button2 */
button2 = document.createElement('button');
aButton2 = angular.element(button1);
aButton2.attr('type', 'button');
aButton2.addClass(button2class);

elm.appendChild(button1);
elm.appendChild(button2);
document.body.appendChild(elm);
}));

afterEach(function(){
if (document.activeElement !== document.body) {
document.activeElement.blur();
}
angular.element(elm).remove();
});

function expectFocused(element){
expect(element.innerHTML).toEqual(document.activeElement.innerHTML);
}

describe('byElement', function(){
it('should focus on the element passed', function(){
gridUtil.focus.byElement(button1);
$timeout.flush();
expectFocused(button1);
});
});
describe('bySelector', function(){
it('should focus on an elment using a selector', function(){
gridUtil.focus.bySelector(elm, '.' + button2class);
$timeout.flush();
expectFocused(button2);
});

it('should focus on an elment using a selector asynchronysly', function(){
gridUtil.focus.bySelector(elm, '.' + button1classUnset, true);
aButton1.addClass(button1classUnset);

$timeout.flush();
expectFocused(button1);
});
});
it('should return a rejected promise if canceled by another focus call', function(){
// Given
var focus1 = {
callbackSuccess: function(){},
callbackFailed: function(reason){}
};
spyOn(focus1, 'callbackSuccess');
spyOn(focus1, 'callbackFailed');

var focus2 = {
callbackSuccess: function(){},
callbackFailed: function(reason){}
};
spyOn(focus2, 'callbackSuccess');
spyOn(focus2, 'callbackFailed');

// When
// Two focus events are queued
gridUtil.focus.byElement(button1).then(focus1.callbackSuccess, focus1.callbackFailed);
gridUtil.focus.byElement(button2).then(focus2.callbackSuccess, focus2.callbackFailed);
$timeout.flush();

// Then
// The first callback will fail
expect(focus1.callbackSuccess).not.toHaveBeenCalled();
expect(focus1.callbackFailed).toHaveBeenCalledWith('canceled');
expect(focus2.callbackSuccess).toHaveBeenCalled();
expect(focus2.callbackFailed).not.toHaveBeenCalled();
});
});

describe('rtlScrollType', function () {
it('should not throw an exception', function () {
// This was throwing an exception in IE because IE doesn't have a native <element>.remove() method.
Expand Down

0 comments on commit 94e50a5

Please sign in to comment.