Skip to content

Commit

Permalink
fix(ui-grid-column-menu.js): Added keyboard navigation to column menu (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
DanielDiTommaso authored and mportuga committed Mar 23, 2018
1 parent 85ce401 commit df42920
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 59 deletions.
58 changes: 58 additions & 0 deletions src/js/core/directives/ui-grid-column-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen


$scope.$on('menu-hidden', function() {
var menuItems = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0];

$elm[0].removeAttribute('style');

if ( $scope.hideThenShow ){
Expand All @@ -403,6 +405,13 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
}
}

if (menuItems) {
menuItems.onkeydown = null;
angular.forEach(menuItems.children, function removeHandlers(item) {
item.onkeydown = null;
});
}
});

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

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

function addKeydownHandlersToMenu() {
var menu = angular.element($elm[0].querySelector('.ui-grid-menu-items'))[0],
menuItems,
visibleMenuItems = [];

if (menu) {
menu.onkeydown = function closeMenu(event) {
if (event.keyCode === uiGridConstants.keymap.ESC) {
event.preventDefault();
$scope.hideMenu();
}
};

menuItems = menu.querySelectorAll('.ui-grid-menu-item:not(.ng-hide)');
angular.forEach(menuItems, function filterVisibleItems(item) {
if (item.offsetParent !== null) {
this.push(item);
}
}, visibleMenuItems);

if (visibleMenuItems.length) {
if (visibleMenuItems.length === 1) {
visibleMenuItems[0].onkeydown = function singleItemHandler(event) {
circularFocusHandler(event, true);
};
} else {
visibleMenuItems[0].onkeydown = function firstItemHandler(event) {
circularFocusHandler(event, false, event.shiftKey, visibleMenuItems.length - 1);
};
visibleMenuItems[visibleMenuItems.length - 1].onkeydown = function lastItemHandler(event) {
circularFocusHandler(event, false, !event.shiftKey, 0);
};
}
}
}

function circularFocusHandler(event, isSingleItem, shiftKeyStatus, index) {
if (event.keyCode === uiGridConstants.keymap.TAB) {
if (isSingleItem) {
event.preventDefault();
} else if (shiftKeyStatus) {
event.preventDefault();
visibleMenuItems[index].focus();
}
}
}
}

// Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
var setFocusOnHideColumn = function(){
$timeout(function() {
Expand Down
253 changes: 194 additions & 59 deletions test/unit/core/directives/ui-grid-column-menu.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
describe('ui-grid-column-menu uiGridColumnMenuService', function() {
'use strict';

var uiGridColumnMenuService, gridClassFactory, gridUtil, grid, $rootScope, $scope;

beforeEach(function() {
module('ui.grid');

inject(function(_uiGridColumnMenuService_, _gridClassFactory_, _gridUtil_, _$rootScope_) {
uiGridColumnMenuService = _uiGridColumnMenuService_;
gridClassFactory = _gridClassFactory_;
gridUtil = _gridUtil_;
$rootScope = _$rootScope_;
});
});
var $rootScope,
$scope,
grid,
gridClassFactory,
gridUtil,
uiGridColumnMenuService,
uiGridConstants;

beforeEach(function() {
module('ui.grid');

inject(function( _$rootScope_, _gridClassFactory_, _gridUtil_, _uiGridColumnMenuService_, _uiGridConstants_) {
$rootScope = _$rootScope_;
gridClassFactory = _gridClassFactory_;
gridUtil = _gridUtil_;
uiGridColumnMenuService = _uiGridColumnMenuService_;
uiGridConstants = _uiGridConstants_;
});
});

describe('uiGridColumnMenuService', function() {
beforeEach(function() {
Expand Down Expand Up @@ -520,53 +527,181 @@ describe('ui-grid-column-menu uiGridColumnMenuService', function() {
});
});

describe('uiGridColumnMenu directive', function() {
var $compile, $timeout, element, uiGrid,
columnVisibilityChanged, sortChanged;

beforeEach(function() {
inject(function(_$compile_, _$timeout_) {
$compile = _$compile_;
$timeout = _$timeout_;
});
$scope = $rootScope.$new();

$scope.gridOpts = {
enableSorting: true,
data: [{ name: 'Bob' }, {name: 'Mathias'}, {name: 'Fred'}],
onRegisterApi: function(gridApi) {
columnVisibilityChanged = jasmine.createSpy('columnVisibilityChanged');
gridApi.core.on.columnVisibilityChanged($scope, columnVisibilityChanged);

sortChanged = jasmine.createSpy('sortChanged');
gridApi.core.on.sortChanged($scope, sortChanged);
}
};

element = angular.element('<div ui-grid="gridOpts"></div>');

uiGrid = $compile(element)($scope);
$scope.$apply();

$('body').append(uiGrid);
$('.ui-grid-column-menu-button').click();
});
afterEach(function() {
$scope.$destroy();
uiGrid.remove();
});
it('should raise the sortChanged event when unsort is clicked', function() {
$($('.ui-grid-menu-item')[2]).click();
$timeout.flush();

expect(sortChanged).toHaveBeenCalledWith(jasmine.any(Object), []);
});

it('should raise the columnVisibilityChanged event when hide column is clicked', function() {
$($('.ui-grid-menu-item')[3]).click();

expect(columnVisibilityChanged).toHaveBeenCalled();
describe('uiGridColumnMenu directive', function() {
var $compile, $timeout, element, uiGrid,
columnVisibilityChanged, sortChanged;

beforeEach(function() {
inject(function(_$compile_, _$timeout_) {
$compile = _$compile_;
$timeout = _$timeout_;
});
$scope = $rootScope.$new();

$scope.gridOpts = {
enableSorting: true,
data: [{ name: 'Bob' }, {name: 'Mathias'}, {name: 'Fred'}],
columnDefs: [
{
field: 'multipleMenuItems'
},
{
field: 'singleMenuItem',
enableSorting: false
}
],
onRegisterApi: function(gridApi) {
columnVisibilityChanged = jasmine.createSpy('columnVisibilityChanged');
gridApi.core.on.columnVisibilityChanged($scope, columnVisibilityChanged);

sortChanged = jasmine.createSpy('sortChanged');
gridApi.core.on.sortChanged($scope, sortChanged);
}
};

element = angular.element('<div ui-grid="gridOpts"></div>');

spyOn(uiGridColumnMenuService, 'repositionMenu').and.callFake(angular.noop);
});
afterEach(function() {
uiGridColumnMenuService.repositionMenu.calls.reset();
});

});
describe('when the menu has multiple menu items', function() {
beforeEach(function() {
uiGrid = $compile(element)($scope);
$scope.$apply();

$('body').append(uiGrid);
$('.ui-grid-column-menu-button').first().click();
});
afterEach(function() {
$scope.$destroy();
uiGrid.remove();
});
it('should raise the sortChanged event when unsort is clicked', function() {
$($('.ui-grid-menu-item')[2]).click();
$timeout.flush();

expect(sortChanged).toHaveBeenCalledWith(jasmine.any(Object), []);
});

it('should raise the columnVisibilityChanged event when hide column is clicked', function() {
$($('.ui-grid-menu-item')[3]).click();

expect(columnVisibilityChanged).toHaveBeenCalled();
});

describe('uiGridMenu keydown handlers', function() {
beforeEach(function() {
$timeout.flush();
});

it('should add keydown handler to ui-grid-menu', function() {
var menu = $('.ui-grid-menu-items')[0];

expect(menu.onkeydown).not.toBe(null);
});
it('should add keydown handlers to first and last visible menu-items', function() {
var items = $('.ui-grid-menu-item');

for (var i = 0; i < items.length; i++) {
if (i === 0 || i === items.length - 1) {
expect(items[i].onkeydown).not.toBe(null);
} else {
expect(items[i].onkeydown).toBe(null);
}
}
});
describe('menu keydown handler', function() {
it('should close menu when escape key is pressed', function() {
var menu = $('.ui-grid-menu-items'),
event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.ESC});

expect(menu[0].onkeydown).not.toBe(null);
menu.trigger(event);
$timeout.flush();
expect(menu[0].onkeydown).toBe(null);
});
});
describe('menu-item keydown handler', function() {
it('should focus on last visible item when shift tab is pressed on first visible item', function() {
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: true}),
items = $('.ui-grid-menu-item');

spyOn(items[items.length - 1], 'focus');
items[0].onkeydown(event);
expect(items[items.length - 1].focus).toHaveBeenCalled();
});
it('should focus on first visible item when tab is pressed on last visible item', function() {
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: false}),
items = $('.ui-grid-menu-item');

spyOn(items[0], 'focus');
items[items.length - 1].onkeydown(event);
expect(items[0].focus).toHaveBeenCalled();
});
});
describe('closing ui-grid-column-menu', function() {
it('should remove keydown handlers from menu-items', function() {
var menu = $('.ui-grid-menu-items'),
items = $('.ui-grid-menu-item'),
event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.ESC});

expect(menu[0].onkeydown).not.toBe(null);
expect(items[0].onkeydown).not.toBe(null);
expect(items[items.length - 1].onkeydown).not.toBe(null);

menu.trigger(event);
$timeout.flush();

expect(menu[0].onkeydown).toBe(null);
angular.forEach($('.ui-grid-menu-item'), function(item) {
expect(item.onkeydown).toBe(null);
});
});
});
});
});
describe('when the menu has a single item', function() {
beforeEach(function() {
uiGrid = $compile(element)($scope);
$scope.$apply();

$('body').append(uiGrid);
$($('.ui-grid-column-menu-button')[1]).click();
});
afterEach(function() {
$scope.$destroy();
uiGrid.remove();
});
describe('uiGridMenu keydown handlers', function() {
beforeEach(function () {
$timeout.flush();
});

it('should add keydown handler to ui-grid-menu', function () {
var menu = $('.ui-grid-menu-items')[0];

expect(menu.onkeydown).not.toBe(null);
});
describe('menu-item keydown handler', function() {
it('should focus on last visible item when shift tab is pressed on first visible item', function() {
var event = jQuery.Event('keydown', {keyCode: uiGridConstants.keymap.TAB, shiftKey: true}),
items = $('.ui-grid-menu-item:not(.ng-hide)');

spyOn(event, 'preventDefault').and.callThrough();
spyOn(items[0], 'focus');

items[0].onkeydown(event);
expect(items[0].focus).not.toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();

event.preventDefault.calls.reset();
items[0].focus.calls.reset();
});
});
});
});
});
});

0 comments on commit df42920

Please sign in to comment.