Skip to content

Commit 94e50a5

Browse files
committed
feat(gridUtil): Focus helper functions
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.
1 parent e5c8299 commit 94e50a5

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

src/js/core/services/ui-grid-util.js

+115
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,121 @@ module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateC
777777

778778
};
779779

780+
/**
781+
* @ngdoc object
782+
* @name focus
783+
* @propertyOf ui.grid.service:GridUtil
784+
* @description Provies a set of methods to set the document focus inside the grid.
785+
* See {@link ui.grid.service:GridUtil.focus} for more information.
786+
*/
787+
788+
/**
789+
* @ngdoc object
790+
* @name ui.grid.service:GridUtil.focus
791+
* @description Provies a set of methods to set the document focus inside the grid.
792+
* Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
793+
* e.g. click events that need to run before the focus or
794+
* inputs elements that are in a disabled state but are enabled when those events
795+
* are triggered.
796+
*/
797+
s.focus = {
798+
queue: [],
799+
//http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
800+
/**
801+
* @ngdoc method
802+
* @methodOf ui.grid.service:GridUtil.focus
803+
* @name byId
804+
* @description Sets the focus of the document to the given id value.
805+
* If provided with the grid object it will automatically append the grid id.
806+
* This is done to encourage unique dom id's as it allows for multiple grids on a
807+
* page.
808+
* @param {String} id the id of the dom element to set the focus on
809+
* @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
810+
* @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
811+
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
812+
* then the promise will fail with the `'canceled'` reason.
813+
*/
814+
byId: function (id, Grid) {
815+
this._purgeQueue();
816+
var promise = $timeout(function() {
817+
var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
818+
var element = $window.document.getElementById(elementID);
819+
if (element) {
820+
element.focus();
821+
} else {
822+
s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
823+
}
824+
});
825+
this.queue.push(promise);
826+
return promise;
827+
},
828+
829+
/**
830+
* @ngdoc method
831+
* @methodOf ui.grid.service:GridUtil.focus
832+
* @name byElement
833+
* @description Sets the focus of the document to the given dom element.
834+
* @param {(element|angular.element)} element the DOM element to set the focus on
835+
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
836+
* then the promise will fail with the `'canceled'` reason.
837+
*/
838+
byElement: function(element){
839+
if (!angular.isElement(element)){
840+
s.logWarn("Trying to focus on an element that isn\'t an element.");
841+
return $q.reject('not-element');
842+
}
843+
element = angular.element(element);
844+
this._purgeQueue();
845+
var promise = $timeout(function(){
846+
if (element){
847+
element[0].focus();
848+
}
849+
});
850+
this.queue.push(promise);
851+
return promise;
852+
},
853+
/**
854+
* @ngdoc method
855+
* @methodOf ui.grid.service:GridUtil.focus
856+
* @name bySelector
857+
* @description Sets the focus of the document to the given dom element.
858+
* @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
859+
* @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
860+
* @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
861+
* then the focus will be called.
862+
* @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
863+
* then the promise will fail with the `'canceled'` reason.
864+
*/
865+
bySelector: function(parentElement, querySelector, aSync){
866+
var self = this;
867+
if (!angular.isElement(parentElement)){
868+
throw new Error("The parent element is not an element.");
869+
}
870+
// Ensure that this is an angular element.
871+
// It is fine if this is already an angular element.
872+
parentElement = angular.element(parentElement);
873+
var focusBySelector = function(){
874+
var element = parentElement[0].querySelector(querySelector);
875+
return self.byElement(element);
876+
};
877+
this._purgeQueue();
878+
if (aSync){ //Do this asynchronysly
879+
var promise = $timeout(focusBySelector);
880+
this.queue.push($timeout(focusBySelector));
881+
return promise;
882+
} else {
883+
return focusBySelector();
884+
}
885+
},
886+
_purgeQueue: function(){
887+
this.queue.forEach(function(element){
888+
$timeout.cancel(element);
889+
});
890+
this.queue = [];
891+
}
892+
};
893+
894+
780895
['width', 'height'].forEach(function (name) {
781896
var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
782897
s['element' + capsName] = function (elem, extra) {

test/unit/core/services/ui-grid-util.spec.js

+90
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,96 @@ describe('ui.grid.utilService', function() {
514514
});
515515
});
516516

517+
describe('focus', function(){
518+
var $timeout;
519+
var elm;
520+
var button1, aButton1, button1classUnset = 'ui-grid-button1';
521+
var button2, aButton2, button2class = 'ui-grid-button2';
522+
beforeEach(inject(function(_$timeout_){
523+
$timeout = _$timeout_;
524+
elm = document.createElement('div');
525+
526+
/* Create Button1 */
527+
button1 = document.createElement('button');
528+
aButton1 = angular.element(button1);
529+
aButton1.attr('type', 'button');
530+
// The class is not set here because it is set inside of tests if needed
531+
532+
/* Create Button2 */
533+
button2 = document.createElement('button');
534+
aButton2 = angular.element(button1);
535+
aButton2.attr('type', 'button');
536+
aButton2.addClass(button2class);
537+
538+
elm.appendChild(button1);
539+
elm.appendChild(button2);
540+
document.body.appendChild(elm);
541+
}));
542+
543+
afterEach(function(){
544+
if (document.activeElement !== document.body) {
545+
document.activeElement.blur();
546+
}
547+
angular.element(elm).remove();
548+
});
549+
550+
function expectFocused(element){
551+
expect(element.innerHTML).toEqual(document.activeElement.innerHTML);
552+
}
553+
554+
describe('byElement', function(){
555+
it('should focus on the element passed', function(){
556+
gridUtil.focus.byElement(button1);
557+
$timeout.flush();
558+
expectFocused(button1);
559+
});
560+
});
561+
describe('bySelector', function(){
562+
it('should focus on an elment using a selector', function(){
563+
gridUtil.focus.bySelector(elm, '.' + button2class);
564+
$timeout.flush();
565+
expectFocused(button2);
566+
});
567+
568+
it('should focus on an elment using a selector asynchronysly', function(){
569+
gridUtil.focus.bySelector(elm, '.' + button1classUnset, true);
570+
aButton1.addClass(button1classUnset);
571+
572+
$timeout.flush();
573+
expectFocused(button1);
574+
});
575+
});
576+
it('should return a rejected promise if canceled by another focus call', function(){
577+
// Given
578+
var focus1 = {
579+
callbackSuccess: function(){},
580+
callbackFailed: function(reason){}
581+
};
582+
spyOn(focus1, 'callbackSuccess');
583+
spyOn(focus1, 'callbackFailed');
584+
585+
var focus2 = {
586+
callbackSuccess: function(){},
587+
callbackFailed: function(reason){}
588+
};
589+
spyOn(focus2, 'callbackSuccess');
590+
spyOn(focus2, 'callbackFailed');
591+
592+
// When
593+
// Two focus events are queued
594+
gridUtil.focus.byElement(button1).then(focus1.callbackSuccess, focus1.callbackFailed);
595+
gridUtil.focus.byElement(button2).then(focus2.callbackSuccess, focus2.callbackFailed);
596+
$timeout.flush();
597+
598+
// Then
599+
// The first callback will fail
600+
expect(focus1.callbackSuccess).not.toHaveBeenCalled();
601+
expect(focus1.callbackFailed).toHaveBeenCalledWith('canceled');
602+
expect(focus2.callbackSuccess).toHaveBeenCalled();
603+
expect(focus2.callbackFailed).not.toHaveBeenCalled();
604+
});
605+
});
606+
517607
describe('rtlScrollType', function () {
518608
it('should not throw an exception', function () {
519609
// This was throwing an exception in IE because IE doesn't have a native <element>.remove() method.

0 commit comments

Comments
 (0)