Skip to content

Commit 1f1de5a

Browse files
committed
feat(core): Accessibility and keyboard support to the grid header.
All applicable roles have been applied to the header. OSX Screen reader correctly reads out all of the header information about each column.
1 parent 11a1ae5 commit 1f1de5a

File tree

2 files changed

+104
-59
lines changed

2 files changed

+104
-59
lines changed

Diff for: src/js/core/directives/ui-grid-header-cell.js

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

4-
angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent',
5-
function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent) {
4+
angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
5+
function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
66
// Do stuff after mouse has been down this many ms on the header cell
77
var mousedownTimeout = 500;
88
var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
@@ -14,75 +14,92 @@
1414
row: '=',
1515
renderIndex: '='
1616
},
17-
require: ['?^uiGrid', '^uiGridRenderContainer'],
17+
require: ['^uiGrid', '^uiGridRenderContainer'],
1818
replace: true,
1919
compile: function() {
2020
return {
2121
pre: function ($scope, $elm, $attrs) {
2222
var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
2323
$elm.append(cellHeader);
2424
},
25-
25+
2626
post: function ($scope, $elm, $attrs, controllers) {
2727
var uiGridCtrl = controllers[0];
2828
var renderContainerCtrl = controllers[1];
2929

30+
$scope.i18n = {
31+
headerCell: i18nService.getSafeText('headerCell'),
32+
sort: i18nService.getSafeText('sort')
33+
};
34+
$scope.getSortDirectionAriaLabel = function(){
35+
var col = $scope.col;
36+
//Trying to recreate this sort of thing but it was getting messy having it in the template.
37+
//Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
38+
var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
39+
var label = sortDirectionText;
40+
//Append the priority if it exists
41+
if (col.sort.priority) {
42+
label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
43+
}
44+
return label;
45+
};
46+
3047
$scope.grid = uiGridCtrl.grid;
3148

3249
$scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];
33-
50+
3451
var initColClass = $scope.col.getColClass(false);
3552
$elm.addClass(initColClass);
36-
53+
3754
// Hide the menu by default
3855
$scope.menuShown = false;
39-
56+
4057
// Put asc and desc sort directions in scope
4158
$scope.asc = uiGridConstants.ASC;
4259
$scope.desc = uiGridConstants.DESC;
43-
60+
4461
// Store a reference to menu element
4562
var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );
46-
63+
4764
var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
48-
65+
4966

5067
// apply any headerCellClass
5168
var classAdded;
5269
var previousMouseX;
5370

5471
// filter watchers
5572
var filterDeregisters = [];
56-
57-
58-
/*
73+
74+
75+
/*
5976
* Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
60-
* Once we have a down event, we need to work out whether we have a click, a drag, or a
61-
* hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
77+
* Once we have a down event, we need to work out whether we have a click, a drag, or a
78+
* hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
6279
* we ignore it. A hold would open the menu.
63-
*
80+
*
6481
* So, on down event, we put in place handlers for move and up events, and a timer. If the
65-
* timer expires before we see a move or up, then we have a long press and hence a column menu open.
66-
* If the up happens before the timer, then we have a click, and we sort if the column is sortable.
82+
* timer expires before we see a move or up, then we have a long press and hence a column menu open.
83+
* If the up happens before the timer, then we have a click, and we sort if the column is sortable.
6784
* If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
6885
* will handle it.
69-
*
86+
*
7087
* To deal with touch enabled devices that also have mice, we only create our handlers when
71-
* we get the down event, and we create the corresponding handlers - if we're touchstart then
88+
* we get the down event, and we create the corresponding handlers - if we're touchstart then
7289
* we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
73-
*
90+
*
7491
* We also suppress the click action whilst this is happening - otherwise after the mouseup there
7592
* will be a click event and that can cause the column menu to close
7693
*
7794
*/
78-
95+
7996
$scope.downFn = function( event ){
8097
event.stopPropagation();
8198

8299
if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
83100
event = event.originalEvent;
84101
}
85-
102+
86103
// Don't show the menu if it's not the left button
87104
if (event.button && event.button !== 0) {
88105
return;
@@ -91,15 +108,15 @@
91108

92109
$scope.mousedownStartTime = (new Date()).getTime();
93110
$scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);
94-
111+
95112
$scope.mousedownTimeout.then(function () {
96113
if ( $scope.colMenu ) {
97114
uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
98115
}
99116
});
100117

101118
uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});
102-
119+
103120
$scope.offAllEvents();
104121
if ( event.type === 'touchstart'){
105122
$document.on('touchend', $scope.upFn);
@@ -109,7 +126,7 @@
109126
$document.on('mousemove', $scope.moveFn);
110127
}
111128
};
112-
129+
113130
$scope.upFn = function( event ){
114131
event.stopPropagation();
115132
$timeout.cancel($scope.mousedownTimeout);
@@ -118,7 +135,7 @@
118135

119136
var mousedownEndTime = (new Date()).getTime();
120137
var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;
121-
138+
122139
if (mousedownTime > mousedownTimeout) {
123140
// long click, handled above with mousedown
124141
}
@@ -129,7 +146,7 @@
129146
}
130147
}
131148
};
132-
149+
133150
$scope.moveFn = function( event ){
134151
// Chrome is known to fire some bogus move events.
135152
var changeValue = event.pageX - previousMouseX;
@@ -140,12 +157,12 @@
140157
$scope.offAllEvents();
141158
$scope.onDownEvents(event.type);
142159
};
143-
160+
144161
$scope.clickFn = function ( event ){
145162
event.stopPropagation();
146163
$contentsElm.off('click', $scope.clickFn);
147164
};
148-
165+
149166

150167
$scope.offAllEvents = function(){
151168
$contentsElm.off('touchstart', $scope.downFn);
@@ -156,10 +173,10 @@
156173

157174
$document.off('touchmove', $scope.moveFn);
158175
$document.off('mousemove', $scope.moveFn);
159-
176+
160177
$contentsElm.off('click', $scope.clickFn);
161178
};
162-
179+
163180
$scope.onDownEvents = function( type ){
164181
// If there is a previous event, then wait a while before
165182
// activating the other mode - i.e. if the last event was a touch event then
@@ -172,40 +189,40 @@
172189
$contentsElm.on('click', $scope.clickFn);
173190
$contentsElm.on('touchstart', $scope.downFn);
174191
$timeout(function(){
175-
$contentsElm.on('mousedown', $scope.downFn);
192+
$contentsElm.on('mousedown', $scope.downFn);
176193
}, changeModeTimeout);
177194
break;
178195
case 'mousemove':
179196
case 'mouseup':
180197
$contentsElm.on('click', $scope.clickFn);
181198
$contentsElm.on('mousedown', $scope.downFn);
182199
$timeout(function(){
183-
$contentsElm.on('touchstart', $scope.downFn);
200+
$contentsElm.on('touchstart', $scope.downFn);
184201
}, changeModeTimeout);
185202
break;
186203
default:
187204
$contentsElm.on('click', $scope.clickFn);
188205
$contentsElm.on('touchstart', $scope.downFn);
189206
$contentsElm.on('mousedown', $scope.downFn);
190-
}
207+
}
191208
};
192-
209+
193210

194211
var updateHeaderOptions = function( grid ){
195212
var contents = $elm;
196213
if ( classAdded ){
197214
contents.removeClass( classAdded );
198215
classAdded = null;
199216
}
200-
217+
201218
if (angular.isFunction($scope.col.headerCellClass)) {
202219
classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
203220
}
204221
else {
205222
classAdded = $scope.col.headerCellClass;
206223
}
207224
contents.addClass(classAdded);
208-
225+
209226
var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
210227
$scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );
211228

@@ -216,7 +233,7 @@
216233
else {
217234
$scope.sortable = false;
218235
}
219-
236+
220237
// Figure out whether this column is filterable or not
221238
var oldFilterable = $scope.filterable;
222239
if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
@@ -240,7 +257,7 @@
240257
uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
241258
uiGridCtrl.grid.queueGridRefresh();
242259
}
243-
}));
260+
}));
244261
});
245262
$scope.$on('$destroy', function() {
246263
filterDeregisters.forEach( function(filterDeregister) {
@@ -251,18 +268,18 @@
251268
filterDeregisters.forEach( function(filterDeregister) {
252269
filterDeregister();
253270
});
254-
}
255-
271+
}
272+
256273
}
257-
274+
258275
// figure out whether we support column menus
259-
if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
276+
if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
260277
$scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
261278
$scope.colMenu = true;
262279
} else {
263280
$scope.colMenu = false;
264281
}
265-
282+
266283
/**
267284
* @ngdoc property
268285
* @name enableColumnMenu
@@ -281,16 +298,16 @@
281298
* column menus. Defaults to true.
282299
*
283300
*/
284-
301+
285302
$scope.offAllEvents();
286-
303+
287304
if ($scope.sortable || $scope.colMenu) {
288305
$scope.onDownEvents();
289-
306+
290307
$scope.$on('$destroy', function () {
291308
$scope.offAllEvents();
292309
});
293-
}
310+
}
294311
};
295312

296313
/*
@@ -307,31 +324,31 @@
307324
});
308325
*/
309326
updateHeaderOptions();
310-
327+
311328
// Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
312329
var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);
313330

314-
$scope.$on( '$destroy', dataChangeDereg );
331+
$scope.$on( '$destroy', dataChangeDereg );
315332

316333
$scope.handleClick = function(event) {
317334
// If the shift key is being held down, add this column to the sort
318335
var add = false;
319336
if (event.shiftKey) {
320337
add = true;
321338
}
322-
339+
323340
// Sort this column then rebuild the grid's rows
324341
uiGridCtrl.grid.sortColumn($scope.col, add)
325342
.then(function () {
326343
if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
327344
uiGridCtrl.grid.refresh();
328345
});
329346
};
330-
347+
331348

332349
$scope.toggleMenu = function(event) {
333350
event.stopPropagation();
334-
351+
335352
// If the menu is already showing...
336353
if (uiGridCtrl.columnMenuScope.menuShown) {
337354
// ... and we're the column the menu is on...

Diff for: src/templates/ui-grid/uiGridHeaderCell.html

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1-
<div role="columnheader" ng-class="{ 'sortable': sortable }" aria-labelledby="{{grid.id}}-{{col.name}}-header-text {{grid.id}}-{{col.name}}-sortdir-text" aria-sort="{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}">
1+
<div
2+
role="columnheader"
3+
ng-class="{ 'sortable': sortable }"
4+
ui-grid-one-bind-aria-labelledby-grid="col.uid + '-header-text ' + col.uid + '-sortdir-text'"
5+
aria-sort="{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}">
26
<!-- <div class="ui-grid-vertical-bar">&nbsp;</div> -->
3-
<div role="button" tabindex=0 class="ui-grid-cell-contents" col-index="renderIndex" title="TOOLTIP">
4-
<span id="{{grid.id}}-{{col.name}}-header-text">{{ col.displayName CUSTOM_FILTERS }}</span>
7+
<div
8+
role="button"
9+
tabindex="0"
10+
class="ui-grid-cell-contents"
11+
col-index="renderIndex"
12+
title="TOOLTIP">
13+
<span ui-grid-one-bind-id-grid="col.uid + '-header-text'">{{ col.displayName CUSTOM_FILTERS }}</span>
514

6-
<span id="{{grid.id}}-{{col.name}}-sortdir-text" ui-grid-visible="col.sort.direction" aria-label="Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}" ng-class="{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }">&nbsp;</span>
15+
<span
16+
ui-grid-one-bind-id-grid="col.uid + '-sortdir-text'"
17+
ui-grid-visible="col.sort.direction"
18+
aria-label="{{getSortDirectionAriaLabel()}}">
19+
<i
20+
ng-class="{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }"
21+
title="{{col.sort.priority ? i18n.headerCell.priority + ' ' + col.sort.priority : null}}"
22+
aria-hidden="true">
23+
&nbsp;
24+
</i>
25+
</span>
726
</div>
827

9-
<div role="button" class="ui-grid-column-menu-button" ng-if="grid.options.enableColumnMenus && !col.isRowHeader && col.colDef.enableColumnMenu !== false" ng-click="toggleMenu($event)" ng-class="{'ui-grid-column-menu-button-last-col': isLastCol}" aria-label="{{col.displayName}} menu" aria-haspopup="true">
28+
<div
29+
role="button"
30+
tabindex="0"
31+
ui-grid-one-bind-id-grid="col.uid + '-menu-button'"
32+
class="ui-grid-column-menu-button"
33+
ng-if="grid.options.enableColumnMenus && !col.isRowHeader && col.colDef.enableColumnMenu !== false"
34+
ng-click="toggleMenu($event)"
35+
ng-class="{'ui-grid-column-menu-button-last-col': isLastCol}"
36+
ui-grid-one-bind-aria-label="i18n.headerCell.aria.columnMenuButtonLabel"
37+
aria-haspopup="true">
1038
<i class="ui-grid-icon-angle-down" aria-hidden="true">&nbsp;</i>
1139
</div>
1240

0 commit comments

Comments
 (0)