From 11a1ae55d4e4b6dab58f192eec9e6ddd34730446 Mon Sep 17 00:00:00 2001 From: Leitschuh Date: Mon, 22 Jun 2015 14:15:21 -0400 Subject: [PATCH] feat(core): Accessibility and keyboard support to the filter controls. The filter controls can now be accessed using the tab keys and the remove filter button automatically moves the focus to the input when it is disabled. They also support OSX Voice over reading. Additionally all of the google accessibility audit rules pass. The focus input now provides a default aria label on the input that can be overridden in the filter settings. --- src/js/core/directives/ui-grid-filter.js | 14 +- src/js/core/directives/ui-grid-menu.js | 30 ++--- src/js/core/factories/GridColumn.js | 150 ++++++++++++---------- src/templates/ui-grid/ui-grid-filter.html | 49 +++++-- src/templates/ui-grid/uiGridMenuItem.html | 2 +- 5 files changed, 147 insertions(+), 98 deletions(-) diff --git a/src/js/core/directives/ui-grid-filter.js b/src/js/core/directives/ui-grid-filter.js index 4389f56e6c..338575c6c6 100644 --- a/src/js/core/directives/ui-grid-filter.js +++ b/src/js/core/directives/ui-grid-filter.js @@ -1,7 +1,7 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', function ($compile, $templateCache) { + angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) { return { compile: function() { @@ -11,14 +11,22 @@ $elm.children().remove(); if ( filterable ){ var template = $scope.col.filterHeaderTemplate; - + $elm.append($compile(template)($scope)); } }; - + $scope.$on( '$destroy', function() { delete $scope.col.updateFilters; }); + }, + post: function ($scope, $elm, $attrs, controllers){ + $scope.aria = i18nService.getSafeText('headerCell.aria'); + $scope.removeFilter = function(colFilter, index){ + colFilter.term = null; + //Set the focus to the filter input after the action disables the button + gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index); + }; } }; } diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index 5338f5889a..5f088a4813 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -16,7 +16,7 @@ var app = angular.module('app', ['ui.grid']); app.controller('MainCtrl', ['$scope', function ($scope) { - + }]); @@ -30,7 +30,7 @@ */ angular.module('ui.grid') -.directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', +.directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { var uiGridMenu = { priority: 0, @@ -46,7 +46,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { var self = this; var menuMid; var $animate; - + // *** Show/Hide functions ****** self.showMenu = $scope.showMenu = function(event, args) { if ( !$scope.shown ){ @@ -57,11 +57,11 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { * animate removal of the ng-if, as the menu items aren't there yet. And we don't want * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated * on scroll events. - * + * * Note when testing animation that animations don't run on the tutorials. When debugging it looks * like they do, but angular has a default $animate provider that is just a stub, and that's what's - * being called. ALso don't be fooled by the fact that your browser has actually loaded the - * angular-translate.js, it's not using it. You need to test animations in an external application. + * being called. ALso don't be fooled by the fact that your browser has actually loaded the + * angular-translate.js, it's not using it. You need to test animations in an external application. */ $scope.shown = true; @@ -96,7 +96,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason. - * + * * The user may have clicked on the menu again whilst * we're waiting, so we check that the mid isn't shown before applying the ng-if. */ @@ -120,7 +120,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { $scope.showMenu(event, args); }); - + // *** Auto hide when click elsewhere ****** var applyHideMenu = function(){ if ($scope.shown) { @@ -129,7 +129,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { }); } }; - + if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { $scope.autoHide = true; } @@ -141,7 +141,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { $scope.$on('$destroy', function () { angular.element(document).off('click touchstart', applyHideMenu); }); - + $scope.$on('$destroy', function() { angular.element($window).off('resize', applyHideMenu); @@ -153,8 +153,8 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); }, - - + + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { var self = this; }] @@ -184,12 +184,12 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { pre: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0], uiGridMenuCtrl = controllers[1]; - + if ($scope.templateUrl) { gridUtil.getTemplate($scope.templateUrl) .then(function (contents) { var template = angular.element(contents); - + var newElm = $compile(template)($scope); $elm.replaceWith(newElm); }); @@ -253,4 +253,4 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { return uiGridMenuItem; }]); -})(); \ No newline at end of file +})(); diff --git a/src/js/core/factories/GridColumn.js b/src/js/core/factories/GridColumn.js index e62dc16174..5a9184cadd 100644 --- a/src/js/core/factories/GridColumn.js +++ b/src/js/core/factories/GridColumn.js @@ -5,16 +5,16 @@ angular.module('ui.grid') /** * ****************************************************************************************** - * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn, + * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn, * and need to be noted as such for those extending and building ui-grid itself. * However, from an end-developer perspective, they interact with all these through columnDefs, * and they really need to be documented there. I feel like they're relatively static, and * I can't find an elegant way for ngDoc to reference to both....so I've duplicated each * comment block. Ugh. - * + * */ - /** + /** * @ngdoc property * @name name * @propertyOf ui.grid.class:GridColumn @@ -23,7 +23,7 @@ angular.module('ui.grid') * */ - /** + /** * @ngdoc property * @name name * @propertyOf ui.grid.class:GridOptions.columnDef @@ -31,8 +31,8 @@ angular.module('ui.grid') * compatibility with 2.x name can be omitted if field is present * */ - - /** + + /** * @ngdoc property * @name displayName * @propertyOf ui.grid.class:GridColumn @@ -41,7 +41,7 @@ angular.module('ui.grid') * */ - /** + /** * @ngdoc property * @name displayName * @propertyOf ui.grid.class:GridOptions.columnDef @@ -49,37 +49,37 @@ angular.module('ui.grid') * provided then one is generated using the name. * */ - - /** + + /** * @ngdoc property * @name field * @propertyOf ui.grid.class:GridColumn - * @description field must be provided if you wish to bind to a - * property in the data source. Should be an angular expression that evaluates against grid.options.data + * @description field must be provided if you wish to bind to a + * property in the data source. Should be an angular expression that evaluates against grid.options.data * array element. Can be a complex expression: employee.address.city, or can be a function: employee.getFullAddress(). * See the angular docs on binding expressions. * */ - - /** + + /** * @ngdoc property * @name field * @propertyOf ui.grid.class:GridOptions.columnDef - * @description field must be provided if you wish to bind to a - * property in the data source. Should be an angular expression that evaluates against grid.options.data + * @description field must be provided if you wish to bind to a + * property in the data source. Should be an angular expression that evaluates against grid.options.data * array element. Can be a complex expression: employee.address.city, or can be a function: employee.getFullAddress(). * See the angular docs on binding expressions. * */ - - /** + + /** * @ngdoc property * @name filter * @propertyOf ui.grid.class:GridColumn - * @description Filter on this column. + * @description Filter on this column. * @example - *
{ term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', flags: { caseSensitive: false }, type: uiGridConstants.filter.SELECT, [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] }
+ *
{ term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', ariaLabel: 'Filter for text, flags: { caseSensitive: false }, type: uiGridConstants.filter.SELECT, [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] }
* */ - + /** * @ngdoc object * @name ui.grid.class:GridColumn @@ -88,7 +88,7 @@ angular.module('ui.grid') * @param {ColumnDef} colDef the column def to associate with this column * @param {number} uid the unique and immutable uid we'd like to allocate to this column * @param {Grid} grid the grid we'd like to create this column in - */ + */ function GridColumn(colDef, uid, grid) { var self = this; @@ -115,17 +115,17 @@ angular.module('ui.grid') // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name); - /** + /** * @ngdoc property * @name aggregationType * @propertyOf ui.grid.class:GridOptions.columnDef * @description The aggregation that you'd like to show in the columnFooter for this - * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`, - * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`, + * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`, + * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`, * `uiGridConstants.aggregationTypes.max`. - * + * * You can also provide a function as the aggregation type, in this case your function needs to accept the full - * set of visible rows, and return a value that should be shown + * set of visible rows, and return a value that should be shown */ if (!self.aggregationType) { self.aggregationValue = undefined; @@ -205,7 +205,7 @@ angular.module('ui.grid') * @param {ColumnDef} colDef the column def to look in for the property value * @param {string} propName the property name we'd like to set * @param {object} defaultValue the value to use if the colDef doesn't provide the setting - */ + */ GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) { var self = this; @@ -223,13 +223,13 @@ angular.module('ui.grid') } }; - - - /** + + + /** * @ngdoc property * @name width * @propertyOf ui.grid.class:GridOptions.columnDef - * @description sets the column width. Can be either + * @description sets the column width. Can be either * a number or a percentage, or an * for auto. * @example *
  $scope.gridOptions.columnDefs = [ { field: 'field1', width: 100},
@@ -238,7 +238,7 @@ angular.module('ui.grid')
    *
    */
 
-  /** 
+  /**
    * @ngdoc property
    * @name minWidth
    * @propertyOf ui.grid.class:GridOptions.columnDef
@@ -248,7 +248,7 @@ angular.module('ui.grid')
    *
    */
 
-  /** 
+  /**
    * @ngdoc property
    * @name maxWidth
    * @propertyOf ui.grid.class:GridOptions.columnDef
@@ -258,64 +258,65 @@ angular.module('ui.grid')
    *
    */
 
-  /** 
+  /**
    * @ngdoc property
    * @name visible
    * @propertyOf ui.grid.class:GridOptions.columnDef
    * @description sets whether or not the column is visible
    * 
Default is true * @example - *
  $scope.gridOptions.columnDefs = [ 
+   * 
  $scope.gridOptions.columnDefs = [
    *     { field: 'field1', visible: true},
    *     { field: 'field2', visible: false }
    *   ]; 
* */ - + /** * @ngdoc property * @name sort * @propertyOf ui.grid.class:GridOptions.columnDef * @description An object of sort information, attributes are: - * + * * - direction: values are uiGridConstants.ASC or uiGridConstants.DESC * - ignoreSort: if set to true this sort is ignored (used by tree to manipulate the sort functionality) * @example *
  $scope.gridOptions.columnDefs = [ { field: 'field1', sort: { direction: uiGridConstants.ASC, ignoreSort: true }}] 
*/ - - /** + + /** * @ngdoc property * @name sortingAlgorithm * @propertyOf ui.grid.class:GridColumn - * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters + * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters * like any normal sorting function. * */ - /** + /** * @ngdoc property * @name sortingAlgorithm * @propertyOf ui.grid.class:GridOptions.columnDef - * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters + * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters * like any normal sorting function. * */ - - /** + + /** * @ngdoc array * @name filters * @propertyOf ui.grid.class:GridOptions.columnDef * @description Specify multiple filter fields. * @example - *
$scope.gridOptions.columnDefs = [ 
+   * 
$scope.gridOptions.columnDefs = [
    *   {
    *     field: 'field1', filters: [
    *       {
    *         term: 'aa',
    *         condition: uiGridConstants.filter.STARTS_WITH,
    *         placeholder: 'starts with...',
+   *         ariaLabel: 'Filter for field1',
    *         flags: { caseSensitive: false },
    *         type: uiGridConstants.filter.SELECT,
    *         selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
@@ -328,10 +329,10 @@ angular.module('ui.grid')
    *   }
    * ]; 
* - * - */ - - /** + * + */ + + /** * @ngdoc array * @name filters * @propertyOf ui.grid.class:GridColumn @@ -342,6 +343,7 @@ angular.module('ui.grid') * term: 'foo', // ngModel for * condition: uiGridConstants.filter.STARTS_WITH, * placeholder: 'starts with...', + * ariaLabel: 'Filter for foo', * flags: { caseSensitive: false }, * type: uiGridConstants.filter.SELECT, * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] @@ -353,16 +355,16 @@ angular.module('ui.grid') * } * ]
* - * - */ + * + */ - /** + /** * @ngdoc array * @name menuItems * @propertyOf ui.grid.class:GridOptions.columnDef - * @description used to add menu items to a column. Refer to the tutorial on this + * @description used to add menu items to a column. Refer to the tutorial on this * functionality. A number of settings are supported: - * + * * - title: controls the title that is displayed in the menu * - icon: the icon shown alongside that title * - action: the method to call when the menu is clicked @@ -371,7 +373,7 @@ angular.module('ui.grid') * - context: context to pass to the action function, available in this.context in your handler * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false * @example - *
  $scope.gridOptions.columnDefs = [ 
+   * 
  $scope.gridOptions.columnDefs = [
    *   { field: 'field1', menuItems: [
    *     {
    *       title: 'Outer Scope Alert',
@@ -391,7 +393,7 @@ angular.module('ui.grid')
    *     }
    *   ] }]; 
* - */ + */ /** * @ngdoc method @@ -403,7 +405,7 @@ angular.module('ui.grid') * @param {boolean} isNew whether the column is being newly created, if not * we're updating an existing column, and some items such as the sort shouldn't * be copied down - */ + */ GridColumn.prototype.updateColumnDef = function(colDef, isNew) { var self = this; @@ -414,7 +416,7 @@ angular.module('ui.grid') } self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName; - + var colDefWidth = colDef.width; var parseErrorMsg = "Cannot parse column width '" + colDefWidth + "' for column named '" + colDef.name + "'"; @@ -454,11 +456,11 @@ angular.module('ui.grid') //use field if it is defined; name if it is not self.field = (colDef.field === undefined) ? colDef.name : colDef.field; - + if ( typeof( self.field ) !== 'string' ){ gridUtil.logError( 'Field is not a string, this is likely to break the code, Field is: ' + self.field ); } - + self.name = colDef.name; // Use colDef.displayName as long as it's not undefined, otherwise default to the field name @@ -478,7 +480,7 @@ angular.module('ui.grid') * if you have long values in your cells), if a function then that function is called * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip, * if it is a static string then displays that static string. - * + * * Defaults to false * */ @@ -505,7 +507,7 @@ angular.module('ui.grid') * if you have long values in your headers), if a function then that function is called * passing in the row and the col `headerTooltip( col )`, and the return value is shown in the tooltip, * if a static string then shows that static string. - * + * * Defaults to false * */ @@ -631,7 +633,7 @@ angular.module('ui.grid') if ( typeof(self.suppressRemoveSort) === 'undefined'){ self.suppressRemoveSort = typeof(colDef.suppressRemoveSort) !== 'undefined' ? colDef.suppressRemoveSort : false; } - + /** * @ngdoc property * @name enableFiltering @@ -656,13 +658,13 @@ angular.module('ui.grid') // Set up default filters array for when one is not provided. // In other words, this (in column def): - // + // // filter: { term: 'something', flags: {}, condition: [CONDITION] } - // + // // is just shorthand for this: - // + // // filters: [{ term: 'something', flags: {}, condition: [CONDITION] }] - // + // var defaultFilters = []; if (colDef.filter) { defaultFilters.push(colDef.filter); @@ -690,6 +692,7 @@ angular.module('ui.grid') * - term: If set, the filter field will be pre-populated * with this value. * - placeholder: String that will be set to the `.placeholder` attribute. + * - ariaLabel: String that will be set to the `.ariaLabel` attribute. This is what is read as a label to screen reader users. * - noTerm: set this to true if you have defined a custom function in condition, and * your custom function doesn't require a term (so it can run even when the term is null) * - flags: only flag currently available is `caseSensitive`, set to false if you don't want @@ -708,6 +711,7 @@ angular.module('ui.grid') * term: 'xx', * condition: uiGridConstants.filter.STARTS_WITH, * placeholder: 'starts with...', + * ariaLabel: 'Starts with filter for field1', * flags: { caseSensitive: false }, * type: uiGridConstants.filter.SELECT, * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ], @@ -720,7 +724,7 @@ angular.module('ui.grid') /* - + /* self.filters = [ @@ -728,6 +732,7 @@ angular.module('ui.grid') term: 'search term' condition: uiGridConstants.filter.CONTAINS, placeholder: 'my placeholder', + ariaLabel: 'Starts with filter for field1', flags: { caseSensitive: true } @@ -748,6 +753,9 @@ angular.module('ui.grid') if (typeof(defaultFilters[index].placeholder) !== 'undefined') { filter.placeholder = defaultFilters[index].placeholder; } + if (typeof(defaultFilters[index].ariaLabel) !== 'undefined') { + filter.ariaLabel = defaultFilters[index].ariaLabel; + } if (typeof(defaultFilters[index].flags) !== 'undefined') { filter.flags = defaultFilters[index].flags; } @@ -767,7 +775,7 @@ angular.module('ui.grid') this.sort = {}; self.grid.api.core.raise.sortChanged( self.grid, self.grid.getColumnSorting() ); }; - + }; @@ -846,7 +854,7 @@ angular.module('ui.grid') }; - /** + /** * @ngdoc property * @name aggregationHideLabel * @propertyOf ui.grid.class:GridOptions.columnDef @@ -863,7 +871,7 @@ angular.module('ui.grid') * based on colDef.aggregationHideLabel. * * @param {string} label the i18n lookup value to use for the column label - * + * */ GridColumn.prototype.getAggregationText = function () { var self = this; diff --git a/src/templates/ui-grid/ui-grid-filter.html b/src/templates/ui-grid/ui-grid-filter.html index 4c7279e01d..05f5ecbb22 100644 --- a/src/templates/ui-grid/ui-grid-filter.html +++ b/src/templates/ui-grid/ui-grid-filter.html @@ -1,19 +1,52 @@ -
+
- + -
-   +
+ +   +
- +
- -
-   +
+ +   +
diff --git a/src/templates/ui-grid/uiGridMenuItem.html b/src/templates/ui-grid/uiGridMenuItem.html index 255cdb42ef..a7512e9c60 100644 --- a/src/templates/ui-grid/uiGridMenuItem.html +++ b/src/templates/ui-grid/uiGridMenuItem.html @@ -1 +1 @@ -
  • {{ name }}
  • \ No newline at end of file +
  • {{ name }}