From 1d577b1514c26fc005a47506a600b38d8d271a9a Mon Sep 17 00:00:00 2001 From: Leitschuh Date: Mon, 22 Jun 2015 17:57:47 -0400 Subject: [PATCH] feat(core): grid menus accessible - When the grid opens the focus is automatically set to the first element in the list. When the menu closes the focus is automatically reutnred to the menu button that opened that list except for when the column is hidden. When the column is hidden the focus is set to the master grid menu. - Also adds the ability to have screen reader only buttons in the menus. The example of this is the 'close' menu item that only appears when the focus is over it. - Updates e2e tests to reflect menu item addition. - The menu now has an additional item that is visible to screen readers only and only becomes visible when focused. This adds one item to all of the tests that are counting the number of elements that are 'displayed'. --- misc/tutorial/102_sorting.ngdoc | 4 +- .../303_customizing_column_menu.ngdoc | 12 +- misc/tutorial/401_AllFeatures.ngdoc | 2 +- src/js/core/directives/ui-grid-column-menu.js | 201 ++++++++++++------ src/js/core/directives/ui-grid-menu-button.js | 123 ++++++----- src/js/core/directives/ui-grid-menu.js | 24 ++- src/less/menu.less | 44 ++-- .../ui-grid/ui-grid-menu-button.html | 11 +- src/templates/ui-grid/uiGridHeaderCell.html | 2 +- src/templates/ui-grid/uiGridMenu.html | 23 +- src/templates/ui-grid/uiGridMenuItem.html | 18 +- 11 files changed, 303 insertions(+), 161 deletions(-) diff --git a/misc/tutorial/102_sorting.ngdoc b/misc/tutorial/102_sorting.ngdoc index ed99db2197..6f4f25207d 100644 --- a/misc/tutorial/102_sorting.ngdoc +++ b/misc/tutorial/102_sorting.ngdoc @@ -238,8 +238,8 @@ columnDef option will cause sorting to be applied after the `cellFilters` are ap }); it('click one menu, then click another menu, expect undisplay and redisplay on second click', function() { - grid1.expectVisibleColumnMenuItems( 0, 3 ); - grid1.expectVisibleColumnMenuItems( 1, 3 ); + grid1.expectVisibleColumnMenuItems( 0, 4 ); + grid1.expectVisibleColumnMenuItems( 1, 4 ); }); it('toggle gender, expect Alexander Foley to move around', function() { diff --git a/misc/tutorial/303_customizing_column_menu.ngdoc b/misc/tutorial/303_customizing_column_menu.ngdoc index ab82190136..813d1c0ea7 100644 --- a/misc/tutorial/303_customizing_column_menu.ngdoc +++ b/misc/tutorial/303_customizing_column_menu.ngdoc @@ -128,8 +128,8 @@ See the example below for usage. }) }); - it('2 menu items in second column, implying no hide option and no remove sort option', function () { - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 2 ); + it('3 menu items in second column, implying no hide option and no remove sort option', function () { + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 3 ); }); it('Long press opens menu in second column', function () { @@ -181,14 +181,14 @@ See the example below for usage. }); }); - it('6 visible items in the third column, implying hide option', function () { - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 2, 6 ); + it('7 visible items in the third column, implying hide option', function () { + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 2, 7 ); }); - it('click header to sort third column, 7 visible items in the third column, implying remove sort option', function () { + it('click header to sort third column, 8 visible items in the third column, implying remove sort option', function () { gridTestUtils.clickHeaderCell( 'grid1', 2 ) .then(function () { - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 2, 7 ); + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 2, 8 ); }); }); }); diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc index f5dfa5c903..33b7d2cc76 100644 --- a/misc/tutorial/401_AllFeatures.ngdoc +++ b/misc/tutorial/401_AllFeatures.ngdoc @@ -112,7 +112,7 @@ All features are enabled to get an idea of performance it('should not duplicate the menu options for pinning when resizing a column', function () { element( by.id('refreshButton') ).click(); gridTestUtils.resizeHeaderCell( 'grid1', 1 ); - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 11); + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 12); }); }); diff --git a/src/js/core/directives/ui-grid-column-menu.js b/src/js/core/directives/ui-grid-column-menu.js index 344e345746..96f77e9f11 100644 --- a/src/js/core/directives/ui-grid-column-menu.js +++ b/src/js/core/directives/ui-grid-column-menu.js @@ -1,7 +1,7 @@ (function(){ angular.module('ui.grid') -.service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil', +.service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil', function ( i18nService, uiGridConstants, gridUtil ) { /** * @ngdoc service @@ -16,12 +16,12 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name initialize - * @description Sets defaults, puts a reference to the $scope on + * @description Sets defaults, puts a reference to the $scope on * the uiGridController * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {controller} uiGridCtrl the uiGridController for the grid * we're on - * + * */ initialize: function( $scope, uiGridCtrl ){ $scope.grid = uiGridCtrl.grid; @@ -29,12 +29,12 @@ function ( i18nService, uiGridConstants, gridUtil ) { // Store a reference to this link/controller in the main uiGrid controller // to allow showMenu later uiGridCtrl.columnMenuScope = $scope; - + // Save whether we're shown or not so the columns can check $scope.menuShown = false; }, - - + + /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService @@ -45,8 +45,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {controller} uiGridCtrl the uiGridController for the grid * we're on - * - */ + * + */ setColMenuItemWatch: function ( $scope ){ var deregFunction = $scope.$watch('col.menuItems', function (n, o) { if (typeof(n) !== 'undefined' && n && angular.isArray(n)) { @@ -62,9 +62,9 @@ function ( i18nService, uiGridConstants, gridUtil ) { else { $scope.menuItems = $scope.defaultMenuItems; } - }); - - $scope.$on( '$destroy', deregFunction ); + }); + + $scope.$on( '$destroy', deregFunction ); }, @@ -81,8 +81,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @name sortable * @description determines whether this column is sortable * @param {$scope} $scope the $scope from the uiGridColumnMenu - * - */ + * + */ sortable: function( $scope ) { if ( $scope.grid.options.enableSorting && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableSorting) { return true; @@ -91,31 +91,31 @@ function ( i18nService, uiGridConstants, gridUtil ) { return false; } }, - + /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name isActiveSort - * @description determines whether the requested sort direction is current active, to + * @description determines whether the requested sort direction is current active, to * allow highlighting in the menu * @param {$scope} $scope the $scope from the uiGridColumnMenu * @param {string} direction the direction that we'd have selected for us to be active - * - */ + * + */ isActiveSort: function( $scope, direction ){ - return (typeof($scope.col) !== 'undefined' && typeof($scope.col.sort) !== 'undefined' && + return (typeof($scope.col) !== 'undefined' && typeof($scope.col.sort) !== 'undefined' && typeof($scope.col.sort.direction) !== 'undefined' && $scope.col.sort.direction === direction); - + }, - + /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name suppressRemoveSort * @description determines whether we should suppress the removeSort option * @param {$scope} $scope the $scope from the uiGridColumnMenu - * - */ + * + */ suppressRemoveSort: function( $scope ) { if ($scope.col && $scope.col.suppressRemoveSort) { return true; @@ -123,7 +123,7 @@ function ( i18nService, uiGridConstants, gridUtil ) { else { return false; } - }, + }, /** @@ -139,8 +139,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @name hideable * @description determines whether a column can be hidden, by checking the enableHiding columnDef option * @param {$scope} $scope the $scope from the uiGridColumnMenu - * - */ + * + */ hideable: function( $scope ) { if (typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.colDef && $scope.col.colDef.enableHiding === false ) { return false; @@ -148,7 +148,7 @@ function ( i18nService, uiGridConstants, gridUtil ) { else { return true; } - }, + }, /** @@ -157,8 +157,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @name getDefaultMenuItems * @description returns the default menu items for a column menu * @param {$scope} $scope the $scope from the uiGridColumnMenu - * - */ + * + */ getDefaultMenuItems: function( $scope ){ return [ { @@ -197,8 +197,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { $scope.unsortColumn(); }, shown: function() { - return service.sortable( $scope ) && - typeof($scope.col) !== 'undefined' && (typeof($scope.col.sort) !== 'undefined' && + return service.sortable( $scope ) && + typeof($scope.col) !== 'undefined' && (typeof($scope.col.sort) !== 'undefined' && typeof($scope.col.sort.direction) !== 'undefined') && $scope.col.sort.direction !== null && !service.suppressRemoveSort( $scope ); } @@ -213,10 +213,20 @@ function ( i18nService, uiGridConstants, gridUtil ) { $event.stopPropagation(); $scope.hideColumn(); } + }, + { + title: i18nService.getSafeText('columnMenu.close'), + screenReaderOnly: true, + shown: function(){ + return true; + }, + action: function($event){ + $event.stopPropagation(); + } } ]; }, - + /** * @ngdoc method @@ -228,8 +238,8 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @param {GridCol} column the column we want to position below * @param {element} $columnElement the column element we want to position below * @returns {hash} containing left, top, offset, height, width - * - */ + * + */ getColumnElementPosition: function( $scope, column, $columnElement ){ var positionData = {}; positionData.left = $columnElement[0].offsetLeft; @@ -244,16 +254,16 @@ function ( i18nService, uiGridConstants, gridUtil ) { positionData.height = gridUtil.elementHeight($columnElement, true); positionData.width = gridUtil.elementWidth($columnElement, true); - + return positionData; }, - + /** * @ngdoc method * @methodOf ui.grid.service:uiGridColumnMenuService * @name repositionMenu - * @description Reposition the menu below the new column. If the menu has no child nodes + * @description Reposition the menu below the new column. If the menu has no child nodes * (i.e. it's not currently visible) then we guess it's width at 100, we'll be called again * later to fix it * @param {$scope} $scope the $scope from the uiGridColumnMenu @@ -261,15 +271,15 @@ function ( i18nService, uiGridConstants, gridUtil ) { * @param {hash} positionData a hash containing left, top, offset, height, width * @param {element} $elm the column menu element that we want to reposition * @param {element} $columnElement the column element that we want to reposition underneath - * - */ + * + */ repositionMenu: function( $scope, column, positionData, $elm, $columnElement ) { var menu = $elm[0].querySelectorAll('.ui-grid-menu'); var containerId = column.renderContainer ? column.renderContainer : 'body'; var renderContainer = column.grid.renderContainers[containerId]; - // It's possible that the render container of the column we're attaching to is - // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft + // It's possible that the render container of the column we're attaching to is + // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft // between the render container and the grid var renderContainerElm = gridUtil.closestElm($columnElement, '.ui-grid-render-container'); var renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left; @@ -279,14 +289,14 @@ function ( i18nService, uiGridConstants, gridUtil ) { // default value the last width for _this_ column, otherwise last width for _any_ column, otherwise default to 170 var myWidth = column.lastMenuWidth ? column.lastMenuWidth : ( $scope.lastMenuWidth ? $scope.lastMenuWidth : 170); var paddingRight = column.lastMenuPaddingRight ? column.lastMenuPaddingRight : ( $scope.lastMenuPaddingRight ? $scope.lastMenuPaddingRight : 10); - + if ( menu.length !== 0 ){ - var mid = menu[0].querySelectorAll('.ui-grid-menu-mid'); + var mid = menu[0].querySelectorAll('.ui-grid-menu-mid'); if ( mid.length !== 0 && !angular.element(mid).hasClass('ng-hide') ) { myWidth = gridUtil.elementWidth(menu, true); $scope.lastMenuWidth = myWidth; column.lastMenuWidth = myWidth; - + // TODO(c0bra): use padding-left/padding-right based on document direction (ltr/rtl), place menu on proper side // Get the column menu right padding paddingRight = parseInt(gridUtil.getStyles(angular.element(menu)[0])['paddingRight'], 10); @@ -294,7 +304,7 @@ function ( i18nService, uiGridConstants, gridUtil ) { column.lastMenuPaddingRight = paddingRight; } } - + var left = positionData.left + renderContainerOffset - containerScrollLeft + positionData.parentLeft + positionData.width - myWidth + paddingRight; if (left < positionData.offset){ left = positionData.offset; @@ -302,32 +312,32 @@ function ( i18nService, uiGridConstants, gridUtil ) { $elm.css('left', left + 'px'); $elm.css('top', (positionData.top + positionData.height) + 'px'); - } + } }; - + return service; }]) -.directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', -function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { +.directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', '$document', +function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $document) { /** * @ngdoc directive * @name ui.grid.directive:uiGridColumnMenu * @description Provides the column menu framework, leverages uiGridMenu underneath - * + * */ var uiGridColumnMenu = { priority: 0, scope: true, - require: '?^uiGrid', + require: '^uiGrid', templateUrl: 'ui-grid/uiGridColumnMenu', replace: true, link: function ($scope, $elm, $attrs, uiGridCtrl) { var self = this; - + uiGridColumnMenuService.initialize( $scope, uiGridCtrl ); $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope ); @@ -336,15 +346,15 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.menuItems = $scope.defaultMenuItems; uiGridColumnMenuService.setColMenuItemWatch( $scope ); - + /** * @ngdoc method * @methodOf ui.grid.directive:uiGridColumnMenu * @name showMenu * @description Shows the column menu. If the menu is already displayed it * calls the menu to ask it to hide (it will animate), then it repositions the menu - * to the right place whilst hidden (it will make an assumption on menu width), - * then it asks the menu to show (it will animate), then it repositions the menu again + * to the right place whilst hidden (it will make an assumption on menu width), + * then it asks the menu to show (it will animate), then it repositions the menu again * once we can calculate it's size. * @param {GridCol} column the column we want to position below * @param {element} $columnElement the column element we want to position below @@ -371,8 +381,7 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.colElement = $columnElement; $scope.colElementPosition = colElementPosition; $scope.$broadcast('show-menu', { originalEvent: event }); - } - + } }; @@ -386,15 +395,13 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { * an infinite loop */ $scope.hideMenu = function( broadcastTrigger ) { - // delete $scope.col; $scope.menuShown = false; - if ( !broadcastTrigger ){ $scope.$broadcast('hide-menu'); } }; - + $scope.$on('menu-hidden', function() { if ( $scope.hideThenShow ){ delete $scope.hideThenShow; @@ -405,9 +412,14 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.menuShown = true; } else { $scope.hideMenu( true ); + + if ($scope.col) { + //Focus on the menu button + gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false); + } } }); - + $scope.$on('menu-shown', function() { $timeout( function() { uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement ); @@ -416,7 +428,7 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { }, 200); }); - + /* Column methods */ $scope.sortColumn = function (event, dir) { event.stopPropagation(); @@ -435,6 +447,58 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.hideMenu(); }; + //Since we are hiding this column the default hide action will fail so we need to focus somewhere else. + var setFocusOnHideColumn = function(){ + $timeout(function(){ + // Get the UID of the first + var focusToGridMenu = function(){ + return gridUtil.focus.byId('grid-menu', $scope.grid); + }; + + var thisIndex; + $scope.grid.columns.some(function(element, index){ + if (angular.equals(element, $scope.col)) { + thisIndex = index; + return true; + } + }); + + var previousVisibleCol; + // Try and find the next lower or nearest column to focus on + $scope.grid.columns.some(function(element, index){ + if (!element.visible){ + return false; + } // This columns index is below the current column index + else if ( index < thisIndex){ + previousVisibleCol = element; + } // This elements index is above this column index and we haven't found one that is lower + else if ( index > thisIndex && !previousVisibleCol) { + // This is the next best thing + previousVisibleCol = element; + // We've found one so use it. + return true; + } // We've reached an element with an index above this column and the previousVisibleCol variable has been set + else if (index > thisIndex && previousVisibleCol) { + // We are done. + return true; + } + }); + // If found then focus on it + if (previousVisibleCol){ + var colClass = previousVisibleCol.getColClass(); + gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){ + if (reason !== 'canceled'){ // If this is canceled then don't perform the action + //The fallback action is to focus on the grid menu + return focusToGridMenu(); + } + }); + } else { + // Fallback action to focus on the grid menu + focusToGridMenu(); + } + }); + }; + $scope.hideColumn = function () { $scope.col.colDef.visible = false; $scope.col.visible = false; @@ -442,15 +506,18 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.grid.queueGridRefresh(); $scope.hideMenu(); $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); - $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col ); + $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col ); + + // We are hiding so the default action of focusing on the button that opened this menu will fail. + setFocusOnHideColumn(); }; }, - - - + + + controller: ['$scope', function ($scope) { var self = this; - + $scope.$watch('menuItems', function (n, o) { self.menuItems = n; }); @@ -461,4 +528,4 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { }]); -})(); \ No newline at end of file +})(); diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index 34d3644690..983eb29064 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -16,7 +16,7 @@ angular.module('ui.grid') * @name initialize * @description Sets up the gridMenu. Most importantly, sets our * scope onto the grid object as grid.gridMenuScope, allowing us - * to operate when passed only the grid. Second most importantly, + * to operate when passed only the grid. Second most importantly, * we register the 'addToGridMenu' and 'removeFromGridMenu' methods * on the core api. * @param {$scope} $scope the scope of this gridMenu @@ -26,7 +26,7 @@ angular.module('ui.grid') grid.gridMenuScope = $scope; $scope.grid = grid; $scope.registeredMenuItems = []; - + // not certain this is needed, but would be bad to create a memory leak $scope.$on('$destroy', function() { if ( $scope.grid && $scope.grid.gridMenuScope ){ @@ -39,7 +39,7 @@ angular.module('ui.grid') $scope.registeredMenuItems = null; } }); - + $scope.registeredMenuItems = []; /** @@ -53,13 +53,13 @@ angular.module('ui.grid') * in the menu when. (Noting that in most cases the shown and active functions * provide a better way to handle visibility of menu items) * @param {Grid} grid the grid on which we are acting - * @param {array} items menu items in the format as described in the tutorial, with + * @param {array} items menu items in the format as described in the tutorial, with * the added note that if you want to use remove you must also specify an `id` field, * which is provided when you want to remove an item. The id should be unique. - * + * */ grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu ); - + /** * @ngdoc function * @name removeFromGridMenu @@ -69,12 +69,12 @@ angular.module('ui.grid') * the specified id is not found * @param {Grid} grid the grid on which we are acting * @param {string} id the id we'd like to remove from the menu - * + * */ grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu ); }, - - + + /** * @ngdoc function * @name addToGridMenu @@ -86,10 +86,10 @@ angular.module('ui.grid') * in the menu when. (Noting that in most cases the shown and active functions * provide a better way to handle visibility of menu items) * @param {Grid} grid the grid on which we are acting - * @param {array} items menu items in the format as described in the tutorial, with + * @param {array} items menu items in the format as described in the tutorial, with * the added note that if you want to use remove you must also specify an `id` field, * which is provided when you want to remove an item. The id should be unique. - * + * */ addToGridMenu: function( grid, menuItems ) { if ( !angular.isArray( menuItems ) ) { @@ -101,9 +101,9 @@ angular.module('ui.grid') } else { gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid'); } - } + } }, - + /** * @ngdoc function @@ -116,18 +116,18 @@ angular.module('ui.grid') * aren't. * @param {Grid} grid the grid on which we are acting * @param {string} id the id we'd like to remove from the menu - * - */ + * + */ removeFromGridMenu: function( grid, id ){ var foundIndex = -1; - + if ( grid && grid.gridMenuScope ){ grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) { if ( value.id === id ){ if (foundIndex > -1) { gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' ); } else { - + foundIndex = index; } } @@ -138,19 +138,19 @@ angular.module('ui.grid') grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 ); } }, - - + + /** * @ngdoc array * @name gridMenuCustomItems * @propertyOf ui.grid.class:GridOptions * @description (optional) An array of menu items that should be added to * the gridMenu. Follow the format documented in the tutorial for column - * menu customisation. The context provided to the action function will - * include context.grid. An alternative if working with dynamic menus is to use the + * menu customisation. The context provided to the action function will + * include context.grid. An alternative if working with dynamic menus is to use the * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles * some of the management of items for you. - * + * */ /** * @ngdoc boolean @@ -158,7 +158,7 @@ angular.module('ui.grid') * @propertyOf ui.grid.class:GridOptions * @description true by default, whether the grid menu should allow hide/show * of columns - * + * */ /** * @ngdoc method @@ -166,54 +166,54 @@ angular.module('ui.grid') * @name getMenuItems * @description Decides the menu items to show in the menu. This is a * combination of: - * - * - the default menu items that are always included, + * + * - the default menu items that are always included, * - any menu items that have been provided through the addMenuItem api. These * are typically added by features within the grid * - any menu items included in grid.options.gridMenuCustomItems. These can be * changed dynamically, as they're always recalculated whenever we show the * menu - * @param {$scope} $scope the scope of this gridMenu, from which we can find all + * @param {$scope} $scope the scope of this gridMenu, from which we can find all * the information that we need - * @returns {array} an array of menu items that can be shown + * @returns {array} an array of menu items that can be shown */ getMenuItems: function( $scope ) { var menuItems = [ // this is where we add any menu items we want to always include ]; - + if ( $scope.grid.options.gridMenuCustomItems ){ - if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){ - gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not'); + if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){ + gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not'); } else { menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems ); } } - + menuItems = menuItems.concat( $scope.registeredMenuItems ); - + if ( $scope.grid.options.gridMenuShowHideColumns !== false ){ menuItems = menuItems.concat( service.showHideColumns( $scope ) ); } - + menuItems.sort(function(a, b){ return a.order - b.order; }); - + return menuItems; }, - - + + /** * @ngdoc array * @name gridMenuTitleFilter * @propertyOf ui.grid.class:GridOptions - * @description (optional) A function that takes a title string + * @description (optional) A function that takes a title string * (usually the col.displayName), and converts it into a display value. The function * must return either a string or a promise. - * + * * Used for internationalization of the grid menu column names - for angular-translate - * you can pass $translate as the function, for i18nService you can pass getSafeText as the + * you can pass $translate as the function, for i18nService you can pass getSafeText as the * function * @example *
@@ -237,15 +237,15 @@ angular.module('ui.grid')
       if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) {
         return showHideColumns;
       }
-      
+
       // add header for columns
       showHideColumns.push({
         title: i18nService.getSafeText('gridMenu.columns'),
         order: 300
       });
-      
-      $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };  
-      
+
+      $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };
+
       $scope.grid.options.columnDefs.forEach( function( colDef, index ){
         if ( colDef.enableHiding !== false ){
           // add hide menu item - shows an OK icon as we only show when column is already visible
@@ -285,23 +285,23 @@ angular.module('ui.grid')
       });
       return showHideColumns;
     },
-    
-    
+
+
     /**
      * @ngdoc method
      * @methodOf ui.grid.gridMenuService
      * @name setMenuItemTitle
      * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu
      * item if it returns a string, otherwise waiting for the promise to resolve or reject then
-     * putting the result into the title 
+     * putting the result into the title
      * @param {object} menuItem the menuItem we want to put the title on
      * @param {object} colDef the colDef from which we can get displayName, name or field
      * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter
-     * 
+     *
      */
     setMenuItemTitle: function( menuItem, colDef, grid ){
       var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field );
-      
+
       if ( typeof(title) === 'string' ){
         menuItem.title = title;
       } else if ( title.then ){
@@ -326,38 +326,42 @@ angular.module('ui.grid')
      * provided a context that has on it a gridColumn, which is the column that
      * we'll operate upon.  We change the visibility, and refresh the grid as appropriate
      * @param {GridCol} gridCol the column that we want to toggle
-     * 
+     *
      */
     toggleColumnVisibility: function( gridCol ) {
-      gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined ); 
-      
+      gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined );
+
       gridCol.grid.refresh();
       gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
       gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol );
     }
   };
-  
+
   return service;
 }])
 
 
 
-.directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 
-function (gridUtil, uiGridConstants, uiGridGridMenuService) {
+.directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService',
+function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
 
   return {
     priority: 0,
     scope: true,
-    require: ['?^uiGrid'],
+    require: ['^uiGrid'],
     templateUrl: 'ui-grid/ui-grid-menu-button',
     replace: true,
 
-
     link: function ($scope, $elm, $attrs, controllers) {
       var uiGridCtrl = controllers[0];
 
+      // For the aria label
+      $scope.i18n = {
+        aria: i18nService.getSafeText('gridMenu.aria')
+      };
+
       uiGridGridMenuService.initialize($scope, uiGridCtrl.grid);
-      
+
       $scope.shown = false;
 
       $scope.toggleMenu = function () {
@@ -370,13 +374,14 @@ function (gridUtil, uiGridConstants, uiGridGridMenuService) {
           $scope.shown = true;
         }
       };
-      
+
       $scope.$on('menu-hidden', function() {
         $scope.shown = false;
+        gridUtil.focus.bySelector($elm, '.ui-grid-icon-container');
       });
     }
   };
 
 }]);
 
-})();
\ No newline at end of file
+})();
diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js
index 5f088a4813..2dad20b1a0 100644
--- a/src/js/core/directives/ui-grid-menu.js
+++ b/src/js/core/directives/ui-grid-menu.js
@@ -30,8 +30,8 @@
  */
 angular.module('ui.grid')
 
-.directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants',
-function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
+.directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'i18nService',
+function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18nService) {
   var uiGridMenu = {
     priority: 0,
     scope: {
@@ -47,6 +47,10 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
       var menuMid;
       var $animate;
 
+      $scope.i18n = {
+        close: i18nService.getSafeText('columnMenu.close')
+      };
+
     // *** Show/Hide functions ******
       self.showMenu = $scope.showMenu = function(event, args) {
         if ( !$scope.shown ){
@@ -87,6 +91,8 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
         $timeout(function() {
           angular.element(document).on(docEventType, applyHideMenu);
         });
+        //automatically set the focus to the first button element in the now open menu.
+        gridUtil.focus.bySelector($elm, 'button[type=button]', true);
       };
 
 
@@ -174,11 +180,12 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
       shown: '=',
       context: '=',
       templateUrl: '=',
-      leaveOpen: '='
+      leaveOpen: '=',
+      screenReaderOnly: '='
     },
     require: ['?^uiGrid', '^uiGridMenu'],
     templateUrl: 'ui-grid/uiGridMenuItem',
-    replace: true,
+    replace: false,
     compile: function($elm, $attrs) {
       return {
         pre: function ($scope, $elm, $attrs, controllers) {
@@ -221,7 +228,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
           };
 
           $scope.itemAction = function($event,title) {
-            // gridUtil.logDebug('itemAction');
+            gridUtil.logDebug('itemAction');
             $event.stopPropagation();
 
             if (typeof($scope.action) === 'function') {
@@ -240,6 +247,13 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) {
 
               if ( !$scope.leaveOpen ){
                 $scope.$emit('hide-menu');
+              } else {
+                /*
+                 * XXX: Fix after column refactor
+                 * Ideally the focus would remain on the item.
+                 * However, since there are two menu items that have their 'show' property toggled instead. This is a quick fix.
+                 */
+                gridUtil.focus.bySelector(angular.element(gridUtil.closestElm($elm, ".ui-grid-menu-items")), 'button[type=button]', true);
               }
             }
           };
diff --git a/src/less/menu.less b/src/less/menu.less
index b4a717c9bd..9d4c1d062a 100644
--- a/src/less/menu.less
+++ b/src/less/menu.less
@@ -39,6 +39,20 @@
 
   .rounded(@gridBorderRadius);
   .box-shadow(e("0 10px 20px rgba(0, 0, 0, 0.2), inset 0 12px 12px -14px rgba(0, 0, 0, 0.2)"));
+
+  // Small hidden close button that only appears when focused.
+  .ui-grid-menu-close-button {
+    position: absolute;
+    right: 0px;
+    top: 0px;
+    #ui-grid-twbs > .btn();
+    #ui-grid-twbs > .button-size(1px; 1px; 10px; 1; 2px);
+    #ui-grid-twbs > .button-variant(transparent, transparent, transparent);
+    > i {
+      opacity: 0.75;
+      color: black;
+    }
+  }
 }
 
 .ui-grid-menu .ui-grid-menu-inner ul {
@@ -47,23 +61,29 @@
   list-style-type: none;
 
   li {
-    padding: 8px;
-    cursor: pointer;
-
-    // Show a shadow when hovering over a menu item
-    &:hover {
-      // background-color: negation(@headerBackgroundColor, #fff);
-      .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2);
-    }
+    padding: 0px;
+    button {
+      min-width: 100%;
+      padding: 8px;
+      text-align: left;
+      background: transparent;
+      border: none;
 
-    &.ui-grid-menu-item-active {
-      .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2);
-      background-color: @selectedColor;
+      // Show a shadow when hovering over a menu item
+      &:hover,
+      &:focus {
+        // background-color: negation(@headerBackgroundColor, #fff);
+        .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2);
+      }
+      &.ui-grid-menu-item-active {
+        .inner-shadow(@vertical: 0, @blur: 14px, @alpha: 0.2);
+        background-color: @selectedColor;
+      }
     }
   }
 
   // Show a bottom border on all but the last menu item
-  li:not(:last-child) {
+  li:not(:last-child) > button {
     border-bottom: @gridBorderWidth solid @borderColor;
   }
 }
diff --git a/src/templates/ui-grid/ui-grid-menu-button.html b/src/templates/ui-grid/ui-grid-menu-button.html
index 69f1029e98..2a548e3963 100644
--- a/src/templates/ui-grid/ui-grid-menu-button.html
+++ b/src/templates/ui-grid/ui-grid-menu-button.html
@@ -1,6 +1,11 @@
-
-
-   +
+
+  
diff --git a/src/templates/ui-grid/uiGridHeaderCell.html b/src/templates/ui-grid/uiGridHeaderCell.html index 308e36c506..8b72b9cf4d 100644 --- a/src/templates/ui-grid/uiGridHeaderCell.html +++ b/src/templates/ui-grid/uiGridHeaderCell.html @@ -7,7 +7,7 @@
{{ col.displayName CUSTOM_FILTERS }} diff --git a/src/templates/ui-grid/uiGridMenu.html b/src/templates/ui-grid/uiGridMenu.html index bb72ed341f..44ce136c92 100644 --- a/src/templates/ui-grid/uiGridMenu.html +++ b/src/templates/ui-grid/uiGridMenu.html @@ -1,10 +1,23 @@
-
    + +
-
\ No newline at end of file +
diff --git a/src/templates/ui-grid/uiGridMenuItem.html b/src/templates/ui-grid/uiGridMenuItem.html index a7512e9c60..5a18f3f1a9 100644 --- a/src/templates/ui-grid/uiGridMenuItem.html +++ b/src/templates/ui-grid/uiGridMenuItem.html @@ -1 +1,17 @@ -
  • {{ name }}
  • +