diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js
index dc9c52d32d..ef2f803eaa 100644
--- a/src/features/cellnav/js/cellnav.js
+++ b/src/features/cellnav/js/cellnav.js
@@ -15,11 +15,6 @@
*/
var module = angular.module('ui.grid.cellNav', ['ui.grid']);
- function RowCol(row, col) {
- this.row = row;
- this.col = col;
- }
-
/**
* @ngdoc object
* @name ui.grid.cellNav.constant:uiGridCellNavConstants
@@ -37,9 +32,86 @@
}
});
+ /**
+ * @ngdoc object
+ * @name ui.grid.cellNav.object:RowCol
+ * @param {GridRow} row The row for this pair
+ * @param {GridColumn} column The column for this pair
+ * @description A row and column pair that represents the intersection of these two entities.
+ */
+ module.factory('RowColFactory', ['$parse', '$filter',
+ function($parse, $filter){
+ var RowCol = function RowCol(row, col) {
+ /**
+ * @ngdoc object
+ * @name row
+ * @propertyOf ui.grid.cellNav.object:RowCol
+ * @description {@link ui.grid.class:GridRow }
+ */
+ this.row = row;
+ /**
+ * @ngdoc object
+ * @name col
+ * @propertyOf ui.grid.cellNav.object:RowCol
+ * @description {@link ui.grid.class:GridColumn }
+ */
+ this.col = col;
+ };
+
+ /**
+ * @ngdoc function
+ * @name getIntersectionValueRaw
+ * @methodOf ui.grid.cellNav.object:RowCol
+ * @description Gets the intersection of where the row and column meet.
+ * @returns {String|Number|Object} The value from the grid data that this RowCol points too.
+ * If the column has a cellFilter this will NOT return the filtered value.
+ */
+ RowCol.prototype.getIntersectionValueRaw = function(){
+ var getter = $parse(this.col.field);
+ var context = this.row.entity;
+ return getter(context);
+ };
+ /**
+ * @ngdoc function
+ * @name getIntersectionValueFiltered
+ * @methodOf ui.grid.cellNav.object:RowCol
+ * @description Gets the intersection of where the row and column meet.
+ * @returns {String|Number|Object} The value from the grid data that this RowCol points too.
+ * If the column has a cellFilter this will also apply the filter to it and return the value that the filter displays.
+ */
+ RowCol.prototype.getIntersectionValueFiltered = function(){
+ var value = this.getIntersectionValueRaw();
+ if (this.col.cellFilter && this.col.cellFilter !== ''){
+ var getFilterIfExists = function(filterName){
+ try {
+ return $filter(filterName);
+ } catch (e){
+ return null;
+ }
+ };
+ var filter = getFilterIfExists(this.col.cellFilter);
+ if (filter) { // Check if this is filter name or a filter string
+ value = filter(value);
+ } else { // We have the template version of a filter so we need to parse it apart
+ // Get the filter params out using a regex
+ // Test out this regex here https://regex101.com/r/rC5eR5/2
+ var re = /([^:]*):([^:]*):?([\s\S]+)?/;
+ var matches;
+ if ((matches = re.exec(this.col.cellFilter)) !== null) {
+ // View your result using the matches-variable.
+ // eg matches[0] etc.
+ value = $filter(matches[1])(value, matches[2], matches[3]);
+ }
+ }
+ }
+ return value;
+ };
+ return RowCol;
+ }]);
+
- module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q',
- function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q) {
+ module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', 'RowColFactory', '$q',
+ function (gridUtil, uiGridConstants, uiGridCellNavConstants, RowCol, $q) {
/**
* @ngdoc object
* @name ui.grid.cellNav.object:CellNav
@@ -270,8 +342,8 @@
* @description Services for cell navigation features. If you don't like the key maps we use,
* or the direction cells navigation, override with a service decorator (see angular docs)
*/
- module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'ScrollEvent',
- function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, ScrollEvent) {
+ module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'RowColFactory', 'ScrollEvent',
+ function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, RowCol, ScrollEvent) {
var service = {
@@ -624,8 +696,8 @@
*/
- module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', '$timeout',
- function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, $timeout) {
+ module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', 'RowColFactory', '$timeout', '$compile',
+ function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, RowCol, $timeout, $compile) {
return {
replace: true,
priority: -150,
@@ -642,6 +714,14 @@
uiGridCtrl.cellNav = {};
+ //Ensure that the object has all of the methods we expect it to
+ uiGridCtrl.cellNav.makeRowCol = function (obj) {
+ if (!(obj instanceof RowCol)) {
+ obj = new RowCol(obj.row, obj.col);
+ }
+ return obj;
+ };
+
uiGridCtrl.cellNav.getActiveCell = function () {
var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus');
if (elms.length > 0){
@@ -651,19 +731,25 @@
return undefined;
};
- uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown) {
+ uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown, originEvt) {
modifierDown = !(modifierDown === undefined || !modifierDown);
- uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown);
- _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown);
+
+ newRowCol = uiGridCtrl.cellNav.makeRowCol(newRowCol);
+
+ uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown, originEvt);
+ _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown, originEvt);
};
uiGridCtrl.cellNav.clearFocus = grid.cellNav.clearFocus = function () {
- _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, { eventType: uiGridCellNavConstants.EVENT_TYPE.CLEAR });
+ grid.cellNav.focusedCells = [];
+ _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT);
};
- uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown) {
+ uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown, originEvt) {
modifierDown = !(modifierDown === undefined || !modifierDown);
+ rowCol = uiGridCtrl.cellNav.makeRowCol(rowCol);
+
var row = rowCol.row,
col = rowCol.col;
@@ -748,6 +834,70 @@
};
},
post: function ($scope, $elm, $attrs, uiGridCtrl) {
+ var _scope = $scope;
+ var grid = uiGridCtrl.grid;
+
+ function addAriaLiveRegion(){
+ // Thanks to google docs for the inspiration behind how to do this
+ // XXX: Why is this entire mess nessasary?
+ // Because browsers take a lot of coercing to get them to read out live regions
+ //http://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
+ var ariaNotifierDomElt = '
' +
+ ' ' +
+ '
';
+
+ var ariaNotifier = $compile(ariaNotifierDomElt)($scope);
+ $elm.prepend(ariaNotifier);
+ $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown, originEvt) {
+ /*
+ * If the cell nav event was because of a focus event then we don't want to
+ * change the notifier text.
+ * Reasoning: Voice Over fires a focus events when moving arround the grid.
+ * If the screen reader is handing the grid nav properly then we don't need to
+ * use the alert to notify the user of the movement.
+ * In all other cases we do want a notification event.
+ */
+ if (originEvt && originEvt.type === 'focus'){return;}
+
+ function setNotifyText(text){
+ if (text === ariaNotifier.text()){return;}
+ ariaNotifier[0].style.clip = 'rect(0px,0px,0px,0px)';
+ /*
+ * This is how google docs handles clearing the div. Seems to work better than setting the text of the div to ''
+ */
+ ariaNotifier[0].innerHTML = "";
+ ariaNotifier[0].style.visibility = 'hidden';
+ ariaNotifier[0].style.visibility = 'visible';
+ if (text !== ''){
+ ariaNotifier[0].style.clip = 'auto';
+ /*
+ * The space after the text is something that google docs does.
+ */
+ ariaNotifier[0].appendChild(document.createTextNode(text + " "));
+ ariaNotifier[0].style.visibility = 'hidden';
+ ariaNotifier[0].style.visibility = 'visible';
+ }
+ }
+
+ var values = [];
+ var currentSelection = grid.api.cellNav.getCurrentSelection();
+ for (var i = 0; i < currentSelection.length; i++) {
+ values.push(currentSelection[i].getIntersectionValueFiltered());
+ }
+ var cellText = values.toString();
+ setNotifyText(cellText);
+
+ });
+ }
+ addAriaLiveRegion();
}
};
}
@@ -765,7 +915,8 @@
return {
post: function ($scope, $elm, $attrs, controllers) {
var uiGridCtrl = controllers[0],
- renderContainerCtrl = controllers[1];
+ renderContainerCtrl = controllers[1],
+ uiGridCellnavCtrl = controllers[2];
// Skip attaching cell-nav specific logic if the directive is not attached above us
if (!uiGridCtrl.grid.api.cellNav) { return; }
@@ -782,23 +933,41 @@
// Needs to run last after all renderContainers are built
uiGridCellNavService.decorateRenderContainers(grid);
+ if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells){
+ $elm.attr('aria-multiselectable', true);
+ } else {
+ $elm.attr('aria-multiselectable', false);
+ }
+
//add an element with no dimensions that can be used to set focus and capture keystrokes
- var focuser = $compile('')($scope);
+ var focuser = $compile('')($scope);
$elm.append(focuser);
+ focuser.on('focus', function (evt) {
+ evt.uiGridTargetRenderContainerId = containerId;
+ var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
+ if (rowCol === null) {
+ rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
+ if (rowCol.row && rowCol.col) {
+ uiGridCtrl.cellNav.broadcastCellNav(rowCol);
+ }
+ }
+ });
+
+ uiGridCellnavCtrl.setAriaActivedescendant = function(id){
+ $elm.attr('aria-activedescendant', id);
+ };
+
+ uiGridCellnavCtrl.removeAriaActivedescendant = function(id){
+ if ($elm.attr('aria-activedescendant') === id){
+ $elm.attr('aria-activedescendant', '');
+ }
+ };
+
+
uiGridCtrl.focus = function () {
- focuser[0].focus();
+ gridUtil.focus.byElement(focuser[0]);
//allow for first time grid focus
- focuser.on('focus', function (evt) {
- evt.uiGridTargetRenderContainerId = containerId;
- var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
- if (rowCol === null) {
- rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
- if (rowCol.row && rowCol.col) {
- uiGridCtrl.cellNav.broadcastCellNav(rowCol);
- }
- }
- });
};
var viewPortKeyDownWasRaisedForRowCol = null;
@@ -827,6 +996,11 @@
}
});
+ $scope.$on('$destroy', function(){
+ //Remove all event handlers associated with this focuser.
+ focuser.off();
+ });
+
}
};
}
@@ -859,17 +1033,11 @@
var grid = uiGridCtrl.grid;
-
-
- uiGridCtrl.focus();
-
-
-
grid.api.core.on.scrollBegin($scope, function (args) {
// Skip if there's no currently-focused cell
var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
- if (lastRowCol == null) {
+ if (lastRowCol === null) {
return;
}
@@ -879,17 +1047,14 @@
return;
}
- //clear dom of focused cell
-
- var elements = $elm[0].getElementsByClassName('ui-grid-cell-focus');
- Array.prototype.forEach.call(elements,function(e){angular.element(e).removeClass('ui-grid-cell-focus');});
+ uiGridCtrl.cellNav.clearFocus();
});
grid.api.core.on.scrollEnd($scope, function (args) {
// Skip if there's no currently-focused cell
var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
- if (lastRowCol == null) {
+ if (lastRowCol === null) {
return;
}
@@ -921,14 +1086,16 @@
* @restrict A
* @description Stacks on top of ui.grid.uiGridCell to provide cell navigation
*/
- module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants',
- function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants) {
+ module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants', 'RowColFactory',
+ function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants, RowCol) {
return {
priority: -150, // run after default uiGridCell directive and ui.grid.edit uiGridCell
restrict: 'A',
- require: '^uiGrid',
+ require: ['^uiGrid', '?^uiGridCellnav'],
scope: false,
- link: function ($scope, $elm, $attrs, uiGridCtrl) {
+ link: function ($scope, $elm, $attrs, controllers) {
+ var uiGridCtrl = controllers[0],
+ uiGridCellnavCtrl = controllers[1];
// Skip attaching cell-nav specific logic if the directive is not attached above us
if (!uiGridCtrl.grid.api.cellNav) { return; }
@@ -936,56 +1103,77 @@
return;
}
+ //Convinience local variables
+ var grid = uiGridCtrl.grid;
+ $scope.focused = false;
+
+ // Make this cell focusable but only with javascript/a mouse click
+ $elm.attr('tabindex', -1);
+
// When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused
$elm.find('div').on('click', function (evt) {
- uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey);
+ uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey, evt);
evt.stopPropagation();
$scope.$apply();
});
- $elm.find('div').on('focus', function (evt) {
- uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey);
+
+ /*
+ * XXX Hack for screen readers.
+ * This allows the grid to focus using only the screen reader cursor.
+ * Since the focus event doesn't include key press information we can't use it
+ * as our primary source of the event.
+ */
+ $elm.on('mousedown', function(evt) {
+ //Prevents the foucus event from firing if the click event is already going to fire.
+ //If both events fire it will cause bouncing behavior.
+ evt.preventDefault();
+ });
+
+ //You can only focus on elements with a tabindex value
+ $elm.on('focus', function (evt) {
+ uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), false, evt);
+ evt.stopPropagation();
+ $scope.$apply();
});
// This event is fired for all cells. If the cell matches, then focus is set
$scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown) {
- if (evt.eventType === uiGridCellNavConstants.EVENT_TYPE.CLEAR) {
- clearFocus();
- return;
- }
-
- if (rowCol.row === $scope.row &&
- rowCol.col === $scope.col) {
- if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown &&
- uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol) === -1) {
- clearFocus();
- } else {
- setFocused();
- }
-
- // // This cellNav event came from a keydown event so we can safely refocus
- // if (rowCol.hasOwnProperty('eventType') && rowCol.eventType === uiGridCellNavConstants.EVENT_TYPE.KEYDOWN) {
- //// $elm.find('div')[0].focus();
- // }
- }
- else if (!(uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown)) {
+ var isFocused = grid.cellNav.focusedCells.some(function(focusedRowCol, index){
+ return (focusedRowCol.row === $scope.row && focusedRowCol.col === $scope.col);
+ });
+ if (isFocused){
+ setFocused();
+ } else {
clearFocus();
}
});
function setFocused() {
- var div = $elm.find('div');
- div.addClass('ui-grid-cell-focus');
+ if (!$scope.focused){
+ var div = $elm.find('div');
+ div.addClass('ui-grid-cell-focus');
+ $elm.attr('aria-selected', true);
+ uiGridCellnavCtrl.setAriaActivedescendant($elm.attr('id'));
+ $scope.focused = true;
+ }
}
function clearFocus() {
- var div = $elm.find('div');
- div.removeClass('ui-grid-cell-focus');
+ if ($scope.focused){
+ var div = $elm.find('div');
+ div.removeClass('ui-grid-cell-focus');
+ $elm.attr('aria-selected', false);
+ uiGridCellnavCtrl.removeAriaActivedescendant($elm.attr('id'));
+ $scope.focused = false;
+ }
}
$scope.$on('$destroy', function () {
- $elm.find('div').off('click');
+ //.off withouth paramaters removes all handlers
+ $elm.find('div').off();
+ $elm.off();
});
}
};
diff --git a/src/features/cellnav/less/cellNav.less b/src/features/cellnav/less/cellNav.less
index ce4da36786..b24eae2284 100644
--- a/src/features/cellnav/less/cellNav.less
+++ b/src/features/cellnav/less/cellNav.less
@@ -1,4 +1,5 @@
@import '../../../less/variables';
+@import (reference) '../../../less/bootstrap/bootstrap';
// .ui-grid-cell-contents:focus {
// outline: 0;
@@ -11,6 +12,19 @@
}
.ui-grid-focuser {
- width:0px;
- height:0px;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ z-index: -1;
+ width:100%;
+ height:100%;
+ #ui-grid-twbs > .form-control-focus();
+}
+
+.ui-grid-offscreen{
+ display: block;
+ position: absolute;
+ left: -10000px;
+ top: -10000px;
+ clip:rect(0px,0px,0px,0px);
}