Skip to content

Commit df42920

Browse files
DanielDiTommasomportuga
authored andcommitted
fix(ui-grid-column-menu.js): Added keyboard navigation to column menu (#6629)
Provided keydown handlers for uiGridColumnMenu so you can tab-cycle through the menu items correctly. Escape also now closes an open menu. fix #5075
1 parent 85ce401 commit df42920

File tree

2 files changed

+252
-59
lines changed

2 files changed

+252
-59
lines changed

src/js/core/directives/ui-grid-column-menu.js

+58
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
387387

388388

389389
$scope.$on('menu-hidden', function() {
390+
var menuItems = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0];
391+
390392
$elm[0].removeAttribute('style');
391393

392394
if ( $scope.hideThenShow ){
@@ -403,6 +405,13 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
403405
gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
404406
}
405407
}
408+
409+
if (menuItems) {
410+
menuItems.onkeydown = null;
411+
angular.forEach(menuItems.children, function removeHandlers(item) {
412+
item.onkeydown = null;
413+
});
414+
}
406415
});
407416

408417
$scope.$on('menu-shown', function() {
@@ -413,6 +422,7 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
413422
gridUtil.focus.bySelector($document, '.ui-grid-menu-items .ui-grid-menu-item:not(.ng-hide)', true);
414423
delete $scope.colElementPosition;
415424
delete $scope.columnElement;
425+
addKeydownHandlersToMenu();
416426
});
417427
});
418428

@@ -435,6 +445,54 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
435445
$scope.hideMenu();
436446
};
437447

448+
function addKeydownHandlersToMenu() {
449+
var menu = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0],
450+
menuItems,
451+
visibleMenuItems = [];
452+
453+
if (menu) {
454+
menu.onkeydown = function closeMenu(event) {
455+
if (event.keyCode === uiGridConstants.keymap.ESC) {
456+
event.preventDefault();
457+
$scope.hideMenu();
458+
}
459+
};
460+
461+
menuItems = menu.querySelectorAll('.ui-grid-menu-item:not(.ng-hide)');
462+
angular.forEach(menuItems, function filterVisibleItems(item) {
463+
if (item.offsetParent !== null) {
464+
this.push(item);
465+
}
466+
}, visibleMenuItems);
467+
468+
if (visibleMenuItems.length) {
469+
if (visibleMenuItems.length === 1) {
470+
visibleMenuItems[0].onkeydown = function singleItemHandler(event) {
471+
circularFocusHandler(event, true);
472+
};
473+
} else {
474+
visibleMenuItems[0].onkeydown = function firstItemHandler(event) {
475+
circularFocusHandler(event, false, event.shiftKey, visibleMenuItems.length - 1);
476+
};
477+
visibleMenuItems[visibleMenuItems.length - 1].onkeydown = function lastItemHandler(event) {
478+
circularFocusHandler(event, false, !event.shiftKey, 0);
479+
};
480+
}
481+
}
482+
}
483+
484+
function circularFocusHandler(event, isSingleItem, shiftKeyStatus, index) {
485+
if (event.keyCode === uiGridConstants.keymap.TAB) {
486+
if (isSingleItem) {
487+
event.preventDefault();
488+
} else if (shiftKeyStatus) {
489+
event.preventDefault();
490+
visibleMenuItems[index].focus();
491+
}
492+
}
493+
}
494+
}
495+
438496
// Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
439497
var setFocusOnHideColumn = function(){
440498
$timeout(function() {

test/unit/core/directives/ui-grid-column-menu.spec.js

+194-59
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
describe('ui-grid-column-menu uiGridColumnMenuService', function() {
22
'use strict';
33

4-
var uiGridColumnMenuService, gridClassFactory, gridUtil, grid, $rootScope, $scope;
5-
6-
beforeEach(function() {
7-
module('ui.grid');
8-
9-
inject(function(_uiGridColumnMenuService_, _gridClassFactory_, _gridUtil_, _$rootScope_) {
10-
uiGridColumnMenuService = _uiGridColumnMenuService_;
11-
gridClassFactory = _gridClassFactory_;
12-
gridUtil = _gridUtil_;
13-
$rootScope = _$rootScope_;
14-
});
15-
});
4+
var $rootScope,
5+
$scope,
6+
grid,
7+
gridClassFactory,
8+
gridUtil,
9+
uiGridColumnMenuService,
10+
uiGridConstants;
11+
12+
beforeEach(function() {
13+
module('ui.grid');
14+
15+
inject(function( _$rootScope_, _gridClassFactory_, _gridUtil_, _uiGridColumnMenuService_, _uiGridConstants_) {
16+
$rootScope = _$rootScope_;
17+
gridClassFactory = _gridClassFactory_;
18+
gridUtil = _gridUtil_;
19+
uiGridColumnMenuService = _uiGridColumnMenuService_;
20+
uiGridConstants = _uiGridConstants_;
21+
});
22+
});
1623

1724
describe('uiGridColumnMenuService', function() {
1825
beforeEach(function() {
@@ -520,53 +527,181 @@ describe('ui-grid-column-menu uiGridColumnMenuService', function() {
520527
});
521528
});
522529

523-
describe('uiGridColumnMenu directive', function() {
524-
var $compile, $timeout, element, uiGrid,
525-
columnVisibilityChanged, sortChanged;
526-
527-
beforeEach(function() {
528-
inject(function(_$compile_, _$timeout_) {
529-
$compile = _$compile_;
530-
$timeout = _$timeout_;
531-
});
532-
$scope = $rootScope.$new();
533-
534-
$scope.gridOpts = {
535-
enableSorting: true,
536-
data: [{ name: 'Bob' }, {name: 'Mathias'}, {name: 'Fred'}],
537-
onRegisterApi: function(gridApi) {
538-
columnVisibilityChanged = jasmine.createSpy('columnVisibilityChanged');
539-
gridApi.core.on.columnVisibilityChanged($scope, columnVisibilityChanged);
540-
541-
sortChanged = jasmine.createSpy('sortChanged');
542-
gridApi.core.on.sortChanged($scope, sortChanged);
543-
}
544-
};
545-
546-
element = angular.element('<div ui-grid="gridOpts"></div>');
547-
548-
uiGrid = $compile(element)($scope);
549-
$scope.$apply();
550-
551-
$('body').append(uiGrid);
552-
$('.ui-grid-column-menu-button').click();
553-
});
554-
afterEach(function() {
555-
$scope.$destroy();
556-
uiGrid.remove();
557-
});
558-
it('should raise the sortChanged event when unsort is clicked', function() {
559-
$($('.ui-grid-menu-item')[2]).click();
560-
$timeout.flush();
561-
562-
expect(sortChanged).toHaveBeenCalledWith(jasmine.any(Object), []);
563-
});
564-
565-
it('should raise the columnVisibilityChanged event when hide column is clicked', function() {
566-
$($('.ui-grid-menu-item')[3]).click();
567-
568-
expect(columnVisibilityChanged).toHaveBeenCalled();
530+
describe('uiGridColumnMenu directive', function() {
531+
var $compile, $timeout, element, uiGrid,
532+
columnVisibilityChanged, sortChanged;
533+
534+
beforeEach(function() {
535+
inject(function(_$compile_, _$timeout_) {
536+
$compile = _$compile_;
537+
$timeout = _$timeout_;
538+
});
539+
$scope = $rootScope.$new();
540+
541+
$scope.gridOpts = {
542+
enableSorting: true,
543+
data: [{ name: 'Bob' }, {name: 'Mathias'}, {name: 'Fred'}],
544+
columnDefs: [
545+
{
546+
field: 'multipleMenuItems'
547+
},
548+
{
549+
field: 'singleMenuItem',
550+
enableSorting: false
551+
}
552+
],
553+
onRegisterApi: function(gridApi) {
554+
columnVisibilityChanged = jasmine.createSpy('columnVisibilityChanged');
555+
gridApi.core.on.columnVisibilityChanged($scope, columnVisibilityChanged);
556+
557+
sortChanged = jasmine.createSpy('sortChanged');
558+
gridApi.core.on.sortChanged($scope, sortChanged);
559+
}
560+
};
561+
562+
element = angular.element('<div ui-grid="gridOpts"></div>');
563+
564+
spyOn(uiGridColumnMenuService, 'repositionMenu').and.callFake(angular.noop);
565+
});
566+
afterEach(function() {
567+
uiGridColumnMenuService.repositionMenu.calls.reset();
569568
});
570569

571-
});
570+
describe('when the menu has multiple menu items', function() {
571+
beforeEach(function() {
572+
uiGrid = $compile(element)($scope);
573+
$scope.$apply();
574+
575+
$('body').append(uiGrid);
576+
$('.ui-grid-column-menu-button').first().click();
577+
});
578+
afterEach(function() {
579+
$scope.$destroy();
580+
uiGrid.remove();
581+
});
582+
it('should raise the sortChanged event when unsort is clicked', function() {
583+
$($('.ui-grid-menu-item')[2]).click();
584+
$timeout.flush();
585+
586+
expect(sortChanged).toHaveBeenCalledWith(jasmine.any(Object), []);
587+
});
588+
589+
it('should raise the columnVisibilityChanged event when hide column is clicked', function() {
590+
$($('.ui-grid-menu-item')[3]).click();
591+
592+
expect(columnVisibilityChanged).toHaveBeenCalled();
593+
});
594+
595+
describe('uiGridMenu keydown handlers', function() {
596+
beforeEach(function() {
597+
$timeout.flush();
598+
});
599+
600+
it('should add keydown handler to ui-grid-menu', function() {
601+
var menu = $('.ui-grid-menu-items')[0];
602+
603+
expect(menu.onkeydown).not.toBe(null);
604+
});
605+
it('should add keydown handlers to first and last visible menu-items', function() {
606+
var items = $('.ui-grid-menu-item');
607+
608+
for (var i = 0; i < items.length; i++) {
609+
if (i === 0 || i === items.length - 1) {
610+
expect(items[i].onkeydown).not.toBe(null);
611+
} else {
612+
expect(items[i].onkeydown).toBe(null);
613+
}
614+
}
615+
});
616+
describe('menu keydown handler', function() {
617+
it('should close menu when escape key is pressed', function() {
618+
var menu = $('.ui-grid-menu-items'),
619+
event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.ESC});
620+
621+
expect(menu[0].onkeydown).not.toBe(null);
622+
menu.trigger(event);
623+
$timeout.flush();
624+
expect(menu[0].onkeydown).toBe(null);
625+
});
626+
});
627+
describe('menu-item keydown handler', function() {
628+
it('should focus on last visible item when shift tab is pressed on first visible item', function() {
629+
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: true}),
630+
items = $('.ui-grid-menu-item');
631+
632+
spyOn(items[items.length - 1], 'focus');
633+
items[0].onkeydown(event);
634+
expect(items[items.length - 1].focus).toHaveBeenCalled();
635+
});
636+
it('should focus on first visible item when tab is pressed on last visible item', function() {
637+
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: false}),
638+
items = $('.ui-grid-menu-item');
639+
640+
spyOn(items[0], 'focus');
641+
items[items.length - 1].onkeydown(event);
642+
expect(items[0].focus).toHaveBeenCalled();
643+
});
644+
});
645+
describe('closing ui-grid-column-menu', function() {
646+
it('should remove keydown handlers from menu-items', function() {
647+
var menu = $('.ui-grid-menu-items'),
648+
items = $('.ui-grid-menu-item'),
649+
event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.ESC});
650+
651+
expect(menu[0].onkeydown).not.toBe(null);
652+
expect(items[0].onkeydown).not.toBe(null);
653+
expect(items[items.length - 1].onkeydown).not.toBe(null);
654+
655+
menu.trigger(event);
656+
$timeout.flush();
657+
658+
expect(menu[0].onkeydown).toBe(null);
659+
angular.forEach($('.ui-grid-menu-item'), function(item) {
660+
expect(item.onkeydown).toBe(null);
661+
});
662+
});
663+
});
664+
});
665+
});
666+
describe('when the menu has a single item', function() {
667+
beforeEach(function() {
668+
uiGrid = $compile(element)($scope);
669+
$scope.$apply();
670+
671+
$('body').append(uiGrid);
672+
$($('.ui-grid-column-menu-button')[1]).click();
673+
});
674+
afterEach(function() {
675+
$scope.$destroy();
676+
uiGrid.remove();
677+
});
678+
describe('uiGridMenu keydown handlers', function() {
679+
beforeEach(function () {
680+
$timeout.flush();
681+
});
682+
683+
it('should add keydown handler to ui-grid-menu', function () {
684+
var menu = $('.ui-grid-menu-items')[0];
685+
686+
expect(menu.onkeydown).not.toBe(null);
687+
});
688+
describe('menu-item keydown handler', function() {
689+
it('should focus on last visible item when shift tab is pressed on first visible item', function() {
690+
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: true}),
691+
items = $('.ui-grid-menu-item:not(.ng-hide)');
692+
693+
spyOn(event, 'preventDefault').and.callThrough();
694+
spyOn(items[0], 'focus');
695+
696+
items[0].onkeydown(event);
697+
expect(items[0].focus).not.toHaveBeenCalled();
698+
expect(event.preventDefault).toHaveBeenCalled();
699+
700+
event.preventDefault.calls.reset();
701+
items[0].focus.calls.reset();
702+
});
703+
});
704+
});
705+
});
706+
});
572707
});

0 commit comments

Comments
 (0)