Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

fix(tabs): Accessibility and keyboard interaction fixes #10706

Merged
merged 1 commit into from
Jun 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/components/tabs/js/tabsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@ function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipp
event.preventDefault();
if (!locked) select(ctrl.focusIndex);
break;
case $mdConstant.KEY_CODE.TAB:
// On tabbing out of the tablist, reset hasFocus to reset ng-focused and
// its md-focused class if the focused tab is not the active tab.
if (ctrl.focusIndex !== ctrl.selectedIndex) {
ctrl.focusIndex = ctrl.selectedIndex;
}
break;
}
}

Expand Down Expand Up @@ -667,12 +674,12 @@ function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipp
}

/**
* This is used to forward focus to dummy elements. This method is necessary to avoid animation
* issues when attempting to focus an item that is out of view.
* This is used to forward focus to tab container elements. This method is necessary to avoid
* animation issues when attempting to focus an item that is out of view.
*/
function redirectFocus () {
ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
getElements().dummies[ ctrl.focusIndex ].focus();
getElements().tabs[ ctrl.focusIndex ].focus();
}

/**
Expand Down
21 changes: 8 additions & 13 deletions src/components/tabs/js/tabsDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,27 +131,28 @@ function MdTabs ($$mdSvgRegistry) {
'<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
'</md-next-button> ' +
'<md-tabs-canvas ' +
'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
'aria-activedescendant="{{$mdTabsCtrl.getFocusedTabId()}}" ' +
'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
'ng-class="{ ' +
'\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
'\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
'}" ' +
'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
'role="tablist"> ' +
'ng-keydown="$mdTabsCtrl.keydown($event)"> ' +
'<md-pagination-wrapper ' +
'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' +
'role="tablist"> ' +
'<md-tab-item ' +
'tabindex="-1" ' +
'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' +
'class="md-tab" ' +
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
'role="tab" ' +
'id="tab-item-{{::tab.id}}" ' +
'md-tab-id="{{::tab.id}}"' +
'aria-selected="{{tab.isActive()}}" ' +
'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
'ng-class="{ ' +
'\'md-active\': tab.isActive(), ' +
'\'md-focused\': tab.hasFocus(), ' +
Expand All @@ -164,16 +165,10 @@ function MdTabs ($$mdSvgRegistry) {
'md-scope="::tab.parent"></md-tab-item> ' +
'<md-ink-bar></md-ink-bar> ' +
'</md-pagination-wrapper> ' +
'<md-tabs-dummy-wrapper class="md-visually-hidden md-dummy-wrapper"> ' +
'<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' +
'<md-dummy-tab ' +
'class="md-tab" ' +
'tabindex="-1" ' +
'id="tab-item-{{::tab.id}}" ' +
'md-tab-id="{{::tab.id}}"' +
'aria-selected="{{tab.isActive()}}" ' +
'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
'md-tabs-template="::tab.label" ' +
'md-scope="::tab.parent"></md-dummy-tab> ' +
Expand Down
2 changes: 1 addition & 1 deletion src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ md-tab {
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
&.md-focused {
&.md-focused, &:focus {
box-shadow: none;
outline: none;
}
Expand Down
38 changes: 26 additions & 12 deletions src/components/tabs/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('<md-tabs>', function () {

}));

it('should select tab on space or enter', inject(function ($mdConstant) {
it('should select tab on space or enter', inject(function ($document, $mdConstant) {
var tabs = setup('<md-tabs>' +
'<md-tab></md-tab>' +
'<md-tab></md-tab>' +
Expand All @@ -140,10 +140,15 @@ describe('<md-tabs>', function () {
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW);
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.ENTER);
expect(tabItems.eq(1)).toBeActiveTab();
expect(tabItems.eq(1).attr('tabindex')).toBe('0');

triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.LEFT_ARROW);
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.SPACE);
expect(tabItems.eq(0)).toBeActiveTab();
// Active tab should be added to the tab order as per ARIA practices.
expect(tabItems.eq(0).attr('tabindex')).toBe('0');
// Deactivated tab should be removed from the tab order.
expect(tabItems.eq(1).attr('tabindex')).toBe('-1');
}));

it('should bind to selected', function () {
Expand All @@ -157,14 +162,11 @@ describe('<md-tabs>', function () {

expect(tabItems.eq(0)).toBeActiveTab();
expect(tabs.scope().current).toBe(0);
expect(dummyTabs.eq(0).attr('aria-selected')).toBe('true');

tabs.scope().$apply('current = 1');
expect(tabItems.eq(1)).toBeActiveTab();

expect(tabItems.eq(0).attr('aria-selected')).toBe('false');
expect(dummyTabs.eq(0).attr('aria-selected')).toBe('false');
expect(dummyTabs.eq(1).attr('aria-selected')).toBe('true');

tabItems.eq(2).triggerHandler('click');
expect(tabs.scope().current).toBe(2);
Expand All @@ -176,28 +178,40 @@ describe('<md-tabs>', function () {
'<md-tab ng-disabled="disabled1"></md-tab>' +
'</md-tabs>');
var tabItems = tabs.find('md-tab-item');
var dummyTabs = tabs.find('md-dummy-tab');

expect(tabItems.eq(0)).toBeActiveTab();
expect(dummyTabs.eq(0).attr('aria-selected')).toBe('true');
expect(tabItems.eq(0).attr('aria-selected')).toBe('true');

tabs.scope().$apply('disabled0 = true');
expect(tabItems.eq(1)).toBeActiveTab();

expect(tabItems.eq(0).attr('aria-disabled')).toBe('true');
expect(dummyTabs.eq(0).attr('aria-disabled')).toBe('true');
expect(tabItems.eq(1).attr('aria-disabled')).toBe('false');
expect(dummyTabs.eq(1).attr('aria-disabled')).toBe('false');

tabs.scope().$apply('disabled0 = false; disabled1 = true');
expect(tabItems.eq(0)).toBeActiveTab();

expect(tabItems.eq(0).attr('aria-disabled')).toBe('false');
expect(dummyTabs.eq(0).attr('aria-disabled')).toBe('false');
expect(tabItems.eq(1).attr('aria-disabled')).toBe('true');
expect(dummyTabs.eq(1).attr('aria-disabled')).toBe('true');
});

it('should reconcile focused and active tabs', inject(function($mdConstant) {
var tabs = setup('<md-tabs>' +
'<md-tab label="super label"></md-tab>' +
'<md-tab label="super label two"></md-tab>' +
'</md-tabs>' +
'<div tabindex="0">Focusable element</div>');
var ctrl = tabs.controller('mdTabs');
var tabItems = tabs.find('md-tab-item');
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW);
expect(tabItems.eq(0)).toBeActiveTab();
expect(ctrl.getFocusedTabId()).toBe(tabItems.eq(1).attr('id'));

triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.TAB);
expect(tabItems.eq(0)).toBeActiveTab();
expect(ctrl.getFocusedTabId()).toBe(tabItems.eq(0).attr('id'));
}));

});

describe('tab label & content DOM', function () {
Expand Down Expand Up @@ -353,12 +367,12 @@ describe('<md-tabs>', function () {
var tabs = setup('<md-tabs>' +
'<md-tab label="label!">content!</md-tab>' +
'</md-tabs>');
var tabItem = tabs.find('md-dummy-tab');
var tabItem = tabs.find('md-tab-item');
var tabContent = angular.element(tabs[ 0 ].querySelector('md-tab-content'));

$timeout.flush();

expect(tabs.find('md-tabs-canvas').attr('role')).toBe('tablist');
expect(tabs.find('md-pagination-wrapper').attr('role')).toBe('tablist');

expect(tabItem.attr('id')).toBeTruthy();
expect(tabItem.attr('aria-controls')).toBe(tabContent.attr('id'));
Expand Down