Skip to content

Commit

Permalink
feat(core): Accessibility and keyboard support to the grid header.
Browse files Browse the repository at this point in the history
All applicable roles have been applied to the header. OSX Screen reader
correctly reads out all of the header information about each column.
  • Loading branch information
JLLeitschuh committed Jul 27, 2015
1 parent 11a1ae5 commit 1f1de5a
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 59 deletions.
125 changes: 71 additions & 54 deletions src/js/core/directives/ui-grid-header-cell.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(function(){
'use strict';

angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent',
function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent) {
angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
// Do stuff after mouse has been down this many ms on the header cell
var mousedownTimeout = 500;
var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
Expand All @@ -14,75 +14,92 @@
row: '=',
renderIndex: '='
},
require: ['?^uiGrid', '^uiGridRenderContainer'],
require: ['^uiGrid', '^uiGridRenderContainer'],
replace: true,
compile: function() {
return {
pre: function ($scope, $elm, $attrs) {
var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
$elm.append(cellHeader);
},

post: function ($scope, $elm, $attrs, controllers) {
var uiGridCtrl = controllers[0];
var renderContainerCtrl = controllers[1];

$scope.i18n = {
headerCell: i18nService.getSafeText('headerCell'),
sort: i18nService.getSafeText('sort')
};
$scope.getSortDirectionAriaLabel = function(){
var col = $scope.col;
//Trying to recreate this sort of thing but it was getting messy having it in the template.
//Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
var label = sortDirectionText;
//Append the priority if it exists
if (col.sort.priority) {
label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
}
return label;
};

$scope.grid = uiGridCtrl.grid;

$scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];

var initColClass = $scope.col.getColClass(false);
$elm.addClass(initColClass);

// Hide the menu by default
$scope.menuShown = false;

// Put asc and desc sort directions in scope
$scope.asc = uiGridConstants.ASC;
$scope.desc = uiGridConstants.DESC;

// Store a reference to menu element
var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );

var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );


// apply any headerCellClass
var classAdded;
var previousMouseX;

// filter watchers
var filterDeregisters = [];
/*


/*
* Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
* Once we have a down event, we need to work out whether we have a click, a drag, or a
* hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
* Once we have a down event, we need to work out whether we have a click, a drag, or a
* hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
* we ignore it. A hold would open the menu.
*
*
* So, on down event, we put in place handlers for move and up events, and a timer. If the
* timer expires before we see a move or up, then we have a long press and hence a column menu open.
* If the up happens before the timer, then we have a click, and we sort if the column is sortable.
* timer expires before we see a move or up, then we have a long press and hence a column menu open.
* If the up happens before the timer, then we have a click, and we sort if the column is sortable.
* If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
* will handle it.
*
*
* To deal with touch enabled devices that also have mice, we only create our handlers when
* we get the down event, and we create the corresponding handlers - if we're touchstart then
* we get the down event, and we create the corresponding handlers - if we're touchstart then
* we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
*
*
* We also suppress the click action whilst this is happening - otherwise after the mouseup there
* will be a click event and that can cause the column menu to close
*
*/

$scope.downFn = function( event ){
event.stopPropagation();

if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
event = event.originalEvent;
}

// Don't show the menu if it's not the left button
if (event.button && event.button !== 0) {
return;
Expand All @@ -91,15 +108,15 @@

$scope.mousedownStartTime = (new Date()).getTime();
$scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);

$scope.mousedownTimeout.then(function () {
if ( $scope.colMenu ) {
uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
}
});

uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});

$scope.offAllEvents();
if ( event.type === 'touchstart'){
$document.on('touchend', $scope.upFn);
Expand All @@ -109,7 +126,7 @@
$document.on('mousemove', $scope.moveFn);
}
};

$scope.upFn = function( event ){
event.stopPropagation();
$timeout.cancel($scope.mousedownTimeout);
Expand All @@ -118,7 +135,7 @@

var mousedownEndTime = (new Date()).getTime();
var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;

if (mousedownTime > mousedownTimeout) {
// long click, handled above with mousedown
}
Expand All @@ -129,7 +146,7 @@
}
}
};

$scope.moveFn = function( event ){
// Chrome is known to fire some bogus move events.
var changeValue = event.pageX - previousMouseX;
Expand All @@ -140,12 +157,12 @@
$scope.offAllEvents();
$scope.onDownEvents(event.type);
};

$scope.clickFn = function ( event ){
event.stopPropagation();
$contentsElm.off('click', $scope.clickFn);
};


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

$document.off('touchmove', $scope.moveFn);
$document.off('mousemove', $scope.moveFn);

$contentsElm.off('click', $scope.clickFn);
};

$scope.onDownEvents = function( type ){
// If there is a previous event, then wait a while before
// activating the other mode - i.e. if the last event was a touch event then
Expand All @@ -172,40 +189,40 @@
$contentsElm.on('click', $scope.clickFn);
$contentsElm.on('touchstart', $scope.downFn);
$timeout(function(){
$contentsElm.on('mousedown', $scope.downFn);
$contentsElm.on('mousedown', $scope.downFn);
}, changeModeTimeout);
break;
case 'mousemove':
case 'mouseup':
$contentsElm.on('click', $scope.clickFn);
$contentsElm.on('mousedown', $scope.downFn);
$timeout(function(){
$contentsElm.on('touchstart', $scope.downFn);
$contentsElm.on('touchstart', $scope.downFn);
}, changeModeTimeout);
break;
default:
$contentsElm.on('click', $scope.clickFn);
$contentsElm.on('touchstart', $scope.downFn);
$contentsElm.on('mousedown', $scope.downFn);
}
}
};


var updateHeaderOptions = function( grid ){
var contents = $elm;
if ( classAdded ){
contents.removeClass( classAdded );
classAdded = null;
}

if (angular.isFunction($scope.col.headerCellClass)) {
classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
}
else {
classAdded = $scope.col.headerCellClass;
}
contents.addClass(classAdded);

var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
$scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );

Expand All @@ -216,7 +233,7 @@
else {
$scope.sortable = false;
}

// Figure out whether this column is filterable or not
var oldFilterable = $scope.filterable;
if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
Expand All @@ -240,7 +257,7 @@
uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
uiGridCtrl.grid.queueGridRefresh();
}
}));
}));
});
$scope.$on('$destroy', function() {
filterDeregisters.forEach( function(filterDeregister) {
Expand All @@ -251,18 +268,18 @@
filterDeregisters.forEach( function(filterDeregister) {
filterDeregister();
});
}
}

}

// figure out whether we support column menus
if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
$scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
$scope.colMenu = true;
} else {
$scope.colMenu = false;
}

/**
* @ngdoc property
* @name enableColumnMenu
Expand All @@ -281,16 +298,16 @@
* column menus. Defaults to true.
*
*/

$scope.offAllEvents();

if ($scope.sortable || $scope.colMenu) {
$scope.onDownEvents();

$scope.$on('$destroy', function () {
$scope.offAllEvents();
});
}
}
};

/*
Expand All @@ -307,31 +324,31 @@
});
*/
updateHeaderOptions();

// Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);

$scope.$on( '$destroy', dataChangeDereg );
$scope.$on( '$destroy', dataChangeDereg );

$scope.handleClick = function(event) {
// If the shift key is being held down, add this column to the sort
var add = false;
if (event.shiftKey) {
add = true;
}

// Sort this column then rebuild the grid's rows
uiGridCtrl.grid.sortColumn($scope.col, add)
.then(function () {
if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
uiGridCtrl.grid.refresh();
});
};


$scope.toggleMenu = function(event) {
event.stopPropagation();

// If the menu is already showing...
if (uiGridCtrl.columnMenuScope.menuShown) {
// ... and we're the column the menu is on...
Expand Down
38 changes: 33 additions & 5 deletions src/templates/ui-grid/uiGridHeaderCell.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
<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'))}}">
<div
role="columnheader"
ng-class="{ 'sortable': sortable }"
ui-grid-one-bind-aria-labelledby-grid="col.uid + '-header-text ' + col.uid + '-sortdir-text'"
aria-sort="{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}">
<!-- <div class="ui-grid-vertical-bar">&nbsp;</div> -->
<div role="button" tabindex=0 class="ui-grid-cell-contents" col-index="renderIndex" title="TOOLTIP">
<span id="{{grid.id}}-{{col.name}}-header-text">{{ col.displayName CUSTOM_FILTERS }}</span>
<div
role="button"
tabindex="0"
class="ui-grid-cell-contents"
col-index="renderIndex"
title="TOOLTIP">
<span ui-grid-one-bind-id-grid="col.uid + '-header-text'">{{ col.displayName CUSTOM_FILTERS }}</span>

<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>
<span
ui-grid-one-bind-id-grid="col.uid + '-sortdir-text'"
ui-grid-visible="col.sort.direction"
aria-label="{{getSortDirectionAriaLabel()}}">
<i
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 }"
title="{{col.sort.priority ? i18n.headerCell.priority + ' ' + col.sort.priority : null}}"
aria-hidden="true">
&nbsp;
</i>
</span>
</div>

<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">
<div
role="button"
tabindex="0"
ui-grid-one-bind-id-grid="col.uid + '-menu-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}"
ui-grid-one-bind-aria-label="i18n.headerCell.aria.columnMenuButtonLabel"
aria-haspopup="true">
<i class="ui-grid-icon-angle-down" aria-hidden="true">&nbsp;</i>
</div>

Expand Down

0 comments on commit 1f1de5a

Please sign in to comment.