Skip to content

Commit e5ae7c0

Browse files
marcelo-portugalmportuga
authored andcommitted
fix: 🐛 improve accessibility in the grid menus and selection
add more keyboard handling and better announcements for checkboxes and menus
1 parent bb28b2f commit e5ae7c0

18 files changed

+140
-89
lines changed

packages/cellnav/src/js/cellnav.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -830,15 +830,16 @@
830830
}
831831

832832
function getCellDisplayValue(currentRowColumn) {
833+
var prefix = '';
834+
833835
if (currentRowColumn.col.field === 'selectionRowHeaderCol') {
834836
// This is the case when the 'selection' feature is used in the grid and the user has moved
835837
// to or inside of the left grid container which holds the checkboxes for selecting rows.
836838
// This is necessary for Accessibility. Without this a screen reader cannot determine if the row
837839
// is or is not currently selected.
838-
return currentRowColumn.row.isSelected ? i18nService.getSafeText('search.aria.selected') : i18nService.getSafeText('search.aria.notSelected');
839-
} else {
840-
return grid.getCellDisplayValue(currentRowColumn.row, currentRowColumn.col);
840+
prefix = (currentRowColumn.row.isSelected ? i18nService.getSafeText('search.aria.selected') : i18nService.getSafeText('search.aria.notSelected')) + ', ';
841841
}
842+
return prefix + grid.getCellDisplayValue(currentRowColumn.row, currentRowColumn.col);
842843
}
843844

844845
var values = [];
@@ -847,9 +848,7 @@
847848
var cellDisplayValue = getCellDisplayValue(currentSelection[i]) + getAppendedColumnHeaderText(currentSelection[i].col);
848849
values.push(cellDisplayValue);
849850
}
850-
var cellText = values.toString();
851-
setNotifyText(cellText);
852-
851+
setNotifyText(values.toString());
853852
});
854853
}
855854
// Only add the ngAria stuff it will be used

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

+9
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,11 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
310310
$scope.menuItems = $scope.defaultMenuItems;
311311
uiGridColumnMenuService.setColMenuItemWatch( $scope );
312312

313+
function updateCurrentColStatus(menuShown) {
314+
if ($scope.col) {
315+
$scope.col.menuShown = menuShown;
316+
}
317+
}
313318

314319
/**
315320
* @ngdoc method
@@ -324,8 +329,11 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
324329
* @param {element} $columnElement the column element we want to position below
325330
*/
326331
$scope.showMenu = function(column, $columnElement, event) {
332+
// Update the menu status for the current column
333+
updateCurrentColStatus(false);
327334
// Swap to this column
328335
$scope.col = column;
336+
updateCurrentColStatus(true);
329337

330338
// Get the position information for the column element
331339
var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement );
@@ -359,6 +367,7 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $documen
359367
*/
360368
$scope.hideMenu = function( broadcastTrigger ) {
361369
$scope.menuShown = false;
370+
updateCurrentColStatus(false);
362371
if ( !broadcastTrigger ) {
363372
$scope.$broadcast('hide-menu');
364373
}

packages/core/src/js/directives/ui-grid-header-cell.js

+8-21
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969

7070
// Hide the menu by default
7171
$scope.menuShown = false;
72+
$scope.col.menuShown = false;
7273

7374
// Put asc and desc sort directions in scope
7475
$scope.asc = uiGridConstants.ASC;
@@ -347,44 +348,30 @@
347348

348349
$scope.handleClick = function(event) {
349350
// If the shift key is being held down, add this column to the sort
350-
var add = false;
351-
if (event.shiftKey) {
352-
add = true;
353-
}
354-
355351
// Sort this column then rebuild the grid's rows
356-
uiGridCtrl.grid.sortColumn($scope.col, add)
352+
uiGridCtrl.grid.sortColumn($scope.col, event.shiftKey)
357353
.then(function () {
358354
if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
359355
uiGridCtrl.grid.refresh();
360356
}).catch(angular.noop);
361357
};
362358

363359
$scope.headerCellArrowKeyDown = function(event) {
364-
if (event.keyCode === 32 || event.keyCode === 13) {
360+
if (event.keyCode === uiGridConstants.keymap.SPACE || event.keyCode === uiGridConstants.keymap.ENTER) {
365361
event.preventDefault();
366362
$scope.toggleMenu(event);
367363
}
368364
};
369365

370366
$scope.toggleMenu = function(event) {
371-
372367
event.stopPropagation();
373368

374-
// If the menu is already showing...
375-
if (uiGridCtrl.columnMenuScope.menuShown) {
376-
// ... and we're the column the menu is on...
377-
if (uiGridCtrl.columnMenuScope.col === $scope.col) {
378-
// ... hide it
379-
uiGridCtrl.columnMenuScope.hideMenu();
380-
}
381-
// ... and we're NOT the column the menu is on
382-
else {
383-
// ... move the menu to our column
384-
uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
385-
}
369+
// If the menu is already showing and we're the column the menu is on
370+
if (uiGridCtrl.columnMenuScope.menuShown && uiGridCtrl.columnMenuScope.col === $scope.col) {
371+
// ... hide it
372+
uiGridCtrl.columnMenuScope.hideMenu();
386373
}
387-
// If the menu is NOT showing
374+
// If the menu is NOT showing or is showing in a different column
388375
else {
389376
// ... show it on our column
390377
uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);

packages/core/src/js/directives/ui-grid-menu-button.js

+10
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,16 @@ function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
376376

377377
$scope.shown = false;
378378

379+
$scope.toggleOnKeydown = function(event) {
380+
if (
381+
event.keyCode === uiGridConstants.keymap.ENTER ||
382+
event.keyCode === uiGridConstants.keymap.SPACE ||
383+
(event.keyCode === uiGridConstants.keymap.ESC && $scope.shown)
384+
) {
385+
$scope.toggleMenu();
386+
}
387+
};
388+
379389
$scope.toggleMenu = function () {
380390
if ( $scope.shown ) {
381391
$scope.$broadcast('hide-menu');

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

+6-20
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,10 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18
112112

113113
// Turn off an existing document click handler
114114
angular.element(document).off('click touchstart', applyHideMenu);
115-
$elm.off('keyup', checkKeyUp);
116-
$elm.off('keydown', checkKeyDown);
117115

118116
// Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one
119117
$timeout(function() {
120118
angular.element(document).on(docEventType, applyHideMenu);
121-
$elm.on('keyup', checkKeyUp);
122-
$elm.on('keydown', checkKeyDown);
123119
});
124120
};
125121

@@ -144,8 +140,6 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18
144140
}
145141

146142
angular.element(document).off('click touchstart', applyHideMenu);
147-
$elm.off('keyup', checkKeyUp);
148-
$elm.off('keydown', checkKeyDown);
149143
};
150144

151145
$scope.$on('hide-menu', function (event, args) {
@@ -173,28 +167,22 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18
173167
}
174168
};
175169

176-
// close menu on ESC and keep tab cyclical
177-
var checkKeyUp = function(event) {
178-
if (event.keyCode === 27) {
179-
$scope.hideMenu();
180-
}
181-
};
182-
183-
var checkKeyDown = function(event) {
170+
$scope.checkKeyDown = function(event) {
184171
var setFocus = function(elm) {
185172
elm.focus();
186173
event.preventDefault();
187-
return false;
188174
};
189-
if (event.keyCode === 9) {
175+
if (event.keyCode === uiGridConstants.keymap.ESC) {
176+
$scope.hideMenu();
177+
} else if (event.keyCode === uiGridConstants.keymap.TAB) {
190178
var firstMenuItem, lastMenuItem;
191179
var menuItemButtons = $elm[0].querySelectorAll('button:not(.ng-hide)');
192180
if (menuItemButtons.length > 0) {
193181
firstMenuItem = menuItemButtons[0];
194182
lastMenuItem = menuItemButtons[menuItemButtons.length - 1];
195-
if (event.target === lastMenuItem && !event.shiftKey) {
183+
if (event.target.parentElement.id === lastMenuItem.parentElement.id && !event.shiftKey) {
196184
setFocus(firstMenuItem);
197-
} else if (event.target === firstMenuItem && event.shiftKey) {
185+
} else if (event.target.parentElement.id === firstMenuItem.parentElement.id && event.shiftKey) {
198186
setFocus(lastMenuItem);
199187
}
200188
}
@@ -212,8 +200,6 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18
212200
$scope.$on('$destroy', function unbindEvents() {
213201
angular.element($window).off('resize', applyHideMenu);
214202
angular.element(document).off('click touchstart', applyHideMenu);
215-
$elm.off('keyup', checkKeyUp);
216-
$elm.off('keydown', checkKeyDown);
217203
});
218204

219205
if (uiGridCtrl) {

packages/core/src/js/en.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
last: 'Last Page'
3838
},
3939
selection: {
40-
selectAll: 'Select All'
40+
aria: {
41+
row: 'Row'
42+
},
43+
selectAll: 'Select All',
44+
displayName: 'Row Selection Checkbox'
4145
},
4246
menu: {
4347
text: 'Choose Columns:'

packages/core/src/js/factories/GridRow.js

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ angular.module('ui.grid')
3131
*/
3232
this.entity = entity;
3333

34+
/**
35+
* @ngdoc object
36+
* @name index
37+
* @propertyOf ui.grid.class:GridRow
38+
* @description The current position of the row in the array
39+
*/
40+
this.index = index;
41+
3442
/**
3543
* @ngdoc object
3644
* @name uid

packages/core/src/templates/ui-grid/ui-grid-menu-button.html

+4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<div class="ui-grid-menu-button">
22
<div role='button'
33
ui-grid-one-bind-id-grid="'grid-menu'"
4+
ui-grid-one-bind-aria-label="i18n.aria.buttonLabel"
5+
tabindex="0"
46
class="ui-grid-icon-container"
57
ng-click="toggleMenu()"
8+
ng-keydown="toggleOnKeydown($event)"
9+
aria-expanded="{{shown}}"
610
aria-haspopup="true">
711
<i class="ui-grid-icon-menu"
812
ui-grid-one-bind-aria-label="i18n.aria.buttonLabel">&nbsp;</i>

packages/core/src/templates/ui-grid/uiGridHeaderCell.html

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
ng-click="toggleMenu($event)"
3939
ng-keydown="headerCellArrowKeyDown($event)"
4040
ui-grid-one-bind-aria-label="i18n.headerCell.aria.columnMenuButtonLabel"
41+
aria-expanded="{{col.menuShown}}"
4142
aria-haspopup="true">
4243
<i
4344
class="ui-grid-icon-angle-down"

packages/core/src/templates/ui-grid/uiGridMenu.html

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<div
22
class="ui-grid-menu"
3+
ng-keydown="checkKeyDown($event)"
34
ng-show="shown">
45
<style ui-grid-style>
56
{{dynamicStyles}}

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

+4
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ describe('ui-grid-column-menu uiGridColumnMenuService', function() {
579579
$scope.$destroy();
580580
uiGrid.remove();
581581
});
582+
it('should update the extended state of the relate menu button', function() {
583+
expect($('.ui-grid-column-menu-button').first().attr('aria-expanded')).toEqual('true');
584+
});
585+
582586
it('should raise the sortChanged event when unsort is clicked', function() {
583587
$($('.ui-grid-menu-item')[2]).click();
584588
$timeout.flush();

packages/core/test/core/directives/ui-grid-header-cell.spec.js

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
describe('uiGridHeaderCell', function() {
22
'use strict';
33

4-
var grid, $scope, $compile, $document, $q, $timeout, $window, recompile, $animate, uiGridConstants, gridUtil, $httpBackend,
5-
downEvent, upEvent, clickEvent,
4+
var grid, $scope, $compile, $document, $timeout, recompile, uiGridConstants, gridUtil, $httpBackend,
5+
downEvent, clickEvent,
66
data = [
77
{name: 'Ethel Price', gender: 'female', company: 'Enersol'},
88
{name: 'Claudine Neal', gender: 'female', company: 'Sealoud'},
@@ -26,14 +26,11 @@ describe('uiGridHeaderCell', function() {
2626
beforeEach(function() {
2727
module('ui.grid');
2828

29-
inject(function(_$compile_, $rootScope, _$document_, _$q_, _$timeout_, _$window_, _$animate_, _uiGridConstants_, _gridUtil_, _$httpBackend_) {
29+
inject(function(_$compile_, $rootScope, _$document_, _$timeout_, _uiGridConstants_, _gridUtil_, _$httpBackend_) {
3030
$scope = $rootScope;
3131
$compile = _$compile_;
3232
$document = _$document_;
33-
$q = _$q_;
3433
$timeout = _$timeout_;
35-
$window = _$window_;
36-
$animate = _$animate_;
3734
uiGridConstants = _uiGridConstants_;
3835
gridUtil = _gridUtil_;
3936
$httpBackend = _$httpBackend_;
@@ -42,11 +39,9 @@ describe('uiGridHeaderCell', function() {
4239
// Decide whether to use mouse or touch events based on which capabilities the browser has
4340
if (gridUtil.isTouchEnabled()) {
4441
downEvent = 'touchstart';
45-
upEvent = 'touchend';
4642
clickEvent = 'touchstart';
4743
} else {
4844
downEvent = 'mousedown';
49-
upEvent = 'mouseup';
5045
clickEvent = 'click';
5146
}
5247

packages/core/test/core/directives/ui-grid-menu-button.spec.js

+34
Original file line numberDiff line numberDiff line change
@@ -456,5 +456,39 @@ describe('ui-grid-menu-button uiGridGridMenuService', function() {
456456
$scope.$broadcast('menu-hidden');
457457
expect(gridUtil.focus.bySelector).toHaveBeenCalledWith(jasmine.any(Object), '.ui-grid-icon-container');
458458
});
459+
describe('toggleOnKeydown method', function() {
460+
var event;
461+
462+
beforeEach(function() {
463+
event = $.Event('keydown');
464+
$scope.$broadcast('hide-menu');
465+
spyOn(uiGridGridMenuService, 'getMenuItems').and.callThrough();
466+
});
467+
afterEach(function() {
468+
uiGridGridMenuService.getMenuItems.calls.reset();
469+
});
470+
it('should toggle the menu on ENTER', function() {
471+
event.keyCode = 13;
472+
element.find('.ui-grid-icon-container').trigger(event);
473+
expect(uiGridGridMenuService.getMenuItems).toHaveBeenCalled();
474+
});
475+
it('should toggle the menu on SPACE', function() {
476+
event.keyCode = 32;
477+
element.find('.ui-grid-icon-container').trigger(event);
478+
expect(uiGridGridMenuService.getMenuItems).toHaveBeenCalled();
479+
});
480+
it('should not try to toggle the menu on ESC if it is closed', function() {
481+
event.keyCode = 27;
482+
element.find('.ui-grid-icon-container').trigger(event);
483+
expect(uiGridGridMenuService.getMenuItems).not.toHaveBeenCalled();
484+
});
485+
it('should hide the menu on ESC if the menu is open', function() {
486+
element.find('.ui-grid-icon-container').trigger('click');
487+
uiGridGridMenuService.getMenuItems.calls.reset();
488+
event.keyCode = 27;
489+
element.find('.ui-grid-icon-container').trigger(event);
490+
expect(uiGridGridMenuService.getMenuItems).not.toHaveBeenCalled();
491+
});
492+
});
459493
});
460494
});

0 commit comments

Comments
 (0)