HTML:
<div ng-focus="vm.onFocus(month)", aria-focus="{{vm.focus == month}}", ng-keydown="vm.onKeydown($event, month)">
Component Controller:
onKeydown(e, num) { switch(e.which) { case 13: // enter case 32: { // Space e.preventDefault(); this.select(+num); break; } case 38: { // up e.preventDefault(); this.onUp(this.focus); break; } case 39: { // right e.preventDefault(); this.onRight(this.focus); break; } case 37: { // left e.preventDefault(); this.onLeft(this.focus); break; } case 40: { //down e.preventDefault(); this.onDown(this.focus); break; } default: { angular.noop(); } } } onFocus(num) { this.focus = +num; } onUp(crtFocus) { if (crtFocus === null) { return; } const newFocus = crtFocus - 4 <= this._MIN_MONTH ? this._MIN_MONTH : crtFocus - 4; this.onFocus(newFocus); } onDown(crtFocus) { if (crtFocus === null) { return; } const newFocus = crtFocus + 4 >= this._MAX_MONTH ? this._MAX_MONTH : crtFocus + 4; this.onFocus(newFocus); } onRight(crtFocus) { if (crtFocus === null) { return; } const newFocus = crtFocus + 1 >= this._MAX_MONTH ? this._MAX_MONTH : crtFocus + 1; this.onFocus(newFocus); } onLeft(crtFocus) { if (crtFocus === null) { return; } const newFocus = crtFocus - 1 <= this._MIN_MONTH ? this._MIN_MONTH : crtFocus - 1; this.onFocus(newFocus); } onBlur() { this.focus = null; }
Testing:
describe('keyboard control', () => { const UNKNOWN = 10, ENTER = 13, SPACE = 32, UP = 38, RIGHT = 39, LEFT = 37, DOWN = 40; it('onKeydown should be called when recevice on keydown event', () => { spyOn(ctrl, 'onKeydown'); angular.element(firstTile).triggerHandler({type: 'keydown', which: ENTER}); $scope.$digest(); expect(ctrl.onKeydown).toHaveBeenCalled(); }); it('select function should be called when receive "Enter" keydown events', () => { spyOn(ctrl, 'select').and.callThrough(); angular.element(firstTile).triggerHandler({type: 'keydown', which: ENTER}); $scope.$digest(); expect(ctrl.select).toHaveBeenCalledWith(1); }); it('select function should be called when receive "Space" keydown events', () => { spyOn(ctrl, 'select').and.callThrough(); angular.element(firstTile).triggerHandler({type: 'keydown', which: SPACE}); $scope.$digest(); expect(ctrl.select).toHaveBeenCalledWith(1); }); it('onUp function should be called when UP keydown triggered', () => { spyOn(ctrl, 'onUp').and.callThrough(); ctrl.focus = 12; const expected = ctrl.focus - 4; angular.element(lastTile).triggerHandler({type: 'keydown', which: UP}); $scope.$digest(); expect(ctrl.onUp).toHaveBeenCalledWith(12); expect(ctrl.focus).toEqual(expected); }); it('if current focus is null, onFocus should not be called', () => { spyOn(ctrl, 'onFocus'); expect(ctrl.focus).toBe(null); angular.element(lastTile).triggerHandler({type: 'keydown', which: UP}); $scope.$digest(); expect(ctrl.onFocus).not.toHaveBeenCalled(); }); it('onLeft function should be called when UP keydown triggered', () => { spyOn(ctrl, 'onLeft').and.callThrough(); ctrl.focus = 12; const expected = ctrl.focus - 1; angular.element(lastTile).triggerHandler({type: 'keydown', which: LEFT}); $scope.$digest(); expect(ctrl.onLeft).toHaveBeenCalledWith(12); expect(ctrl.focus).toEqual(expected); }); it('onRight function should be called when UP keydown triggered', () => { spyOn(ctrl, 'onRight').and.callThrough(); ctrl.focus = 1; const expected = ctrl.focus + 1; angular.element(firstTile).triggerHandler({type: 'keydown', which: RIGHT}); $scope.$digest(); expect(ctrl.onRight).toHaveBeenCalledWith(1); expect(ctrl.focus).toEqual(expected); }); it('onDown function should be called when UP keydown triggered', () => { spyOn(ctrl, 'onDown').and.callThrough(); ctrl.focus = 1; const expected = ctrl.focus + 4; angular.element(firstTile).triggerHandler({type: 'keydown', which: DOWN}); $scope.$digest(); expect(ctrl.onDown).toHaveBeenCalledWith(1); expect(ctrl.focus).toEqual(expected); }); it('should only trigger angular.noop() function when other keycode keydown event trigger', () => { spyOn(angular, 'noop'); angular.element(firstTile).triggerHandler({type: 'keydown', which: UNKNOWN}); $scope.$digest(); expect(angular.noop).toHaveBeenCalled(); }); });