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

Commit 072f832

Browse files
jeyoshimikara
authored andcommitted
fix(tabs): accessibility and keyboard interaction fixes (#10706)
Closes #10075
1 parent 4efd2a2 commit 072f832

File tree

4 files changed

+45
-29
lines changed

4 files changed

+45
-29
lines changed

src/components/tabs/js/tabsController.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,13 @@ function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipp
307307
event.preventDefault();
308308
if (!locked) select(ctrl.focusIndex);
309309
break;
310+
case $mdConstant.KEY_CODE.TAB:
311+
// On tabbing out of the tablist, reset hasFocus to reset ng-focused and
312+
// its md-focused class if the focused tab is not the active tab.
313+
if (ctrl.focusIndex !== ctrl.selectedIndex) {
314+
ctrl.focusIndex = ctrl.selectedIndex;
315+
}
316+
break;
310317
}
311318
}
312319

@@ -667,12 +674,12 @@ function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipp
667674
}
668675

669676
/**
670-
* This is used to forward focus to dummy elements. This method is necessary to avoid animation
671-
* issues when attempting to focus an item that is out of view.
677+
* This is used to forward focus to tab container elements. This method is necessary to avoid
678+
* animation issues when attempting to focus an item that is out of view.
672679
*/
673680
function redirectFocus () {
674681
ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
675-
getElements().dummies[ ctrl.focusIndex ].focus();
682+
getElements().tabs[ ctrl.focusIndex ].focus();
676683
}
677684

678685
/**

src/components/tabs/js/tabsDirective.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -131,27 +131,28 @@ function MdTabs ($$mdSvgRegistry) {
131131
'<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
132132
'</md-next-button> ' +
133133
'<md-tabs-canvas ' +
134-
'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
135-
'aria-activedescendant="{{$mdTabsCtrl.getFocusedTabId()}}" ' +
136134
'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
137135
'ng-class="{ ' +
138136
'\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
139137
'\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
140138
'}" ' +
141-
'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
142-
'role="tablist"> ' +
139+
'ng-keydown="$mdTabsCtrl.keydown($event)"> ' +
143140
'<md-pagination-wrapper ' +
144141
'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
145-
'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
142+
'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' +
143+
'role="tablist"> ' +
146144
'<md-tab-item ' +
147-
'tabindex="-1" ' +
145+
'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' +
148146
'class="md-tab" ' +
149147
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
150148
'role="tab" ' +
149+
'id="tab-item-{{::tab.id}}" ' +
151150
'md-tab-id="{{::tab.id}}"' +
152151
'aria-selected="{{tab.isActive()}}" ' +
153152
'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
154153
'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
154+
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
155+
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
155156
'ng-class="{ ' +
156157
'\'md-active\': tab.isActive(), ' +
157158
'\'md-focused\': tab.hasFocus(), ' +
@@ -164,16 +165,10 @@ function MdTabs ($$mdSvgRegistry) {
164165
'md-scope="::tab.parent"></md-tab-item> ' +
165166
'<md-ink-bar></md-ink-bar> ' +
166167
'</md-pagination-wrapper> ' +
167-
'<md-tabs-dummy-wrapper class="md-visually-hidden md-dummy-wrapper"> ' +
168+
'<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' +
168169
'<md-dummy-tab ' +
169170
'class="md-tab" ' +
170171
'tabindex="-1" ' +
171-
'id="tab-item-{{::tab.id}}" ' +
172-
'md-tab-id="{{::tab.id}}"' +
173-
'aria-selected="{{tab.isActive()}}" ' +
174-
'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
175-
'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
176-
'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
177172
'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
178173
'md-tabs-template="::tab.label" ' +
179174
'md-scope="::tab.parent"></md-dummy-tab> ' +

src/components/tabs/tabs.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ md-tab {
263263
box-sizing: border-box;
264264
overflow: hidden;
265265
text-overflow: ellipsis;
266-
&.md-focused {
266+
&.md-focused, &:focus {
267267
box-shadow: none;
268268
outline: none;
269269
}

src/components/tabs/tabs.spec.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe('<md-tabs>', function () {
129129

130130
}));
131131

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

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

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

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

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

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

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

181182
expect(tabItems.eq(0)).toBeActiveTab();
182-
expect(dummyTabs.eq(0).attr('aria-selected')).toBe('true');
183+
expect(tabItems.eq(0).attr('aria-selected')).toBe('true');
183184

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

187188
expect(tabItems.eq(0).attr('aria-disabled')).toBe('true');
188-
expect(dummyTabs.eq(0).attr('aria-disabled')).toBe('true');
189189
expect(tabItems.eq(1).attr('aria-disabled')).toBe('false');
190-
expect(dummyTabs.eq(1).attr('aria-disabled')).toBe('false');
191190

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

195194
expect(tabItems.eq(0).attr('aria-disabled')).toBe('false');
196-
expect(dummyTabs.eq(0).attr('aria-disabled')).toBe('false');
197195
expect(tabItems.eq(1).attr('aria-disabled')).toBe('true');
198-
expect(dummyTabs.eq(1).attr('aria-disabled')).toBe('true');
199196
});
200197

198+
it('should reconcile focused and active tabs', inject(function($mdConstant) {
199+
var tabs = setup('<md-tabs>' +
200+
'<md-tab label="super label"></md-tab>' +
201+
'<md-tab label="super label two"></md-tab>' +
202+
'</md-tabs>' +
203+
'<div tabindex="0">Focusable element</div>');
204+
var ctrl = tabs.controller('mdTabs');
205+
var tabItems = tabs.find('md-tab-item');
206+
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.RIGHT_ARROW);
207+
expect(tabItems.eq(0)).toBeActiveTab();
208+
expect(ctrl.getFocusedTabId()).toBe(tabItems.eq(1).attr('id'));
209+
210+
triggerKeydown(tabs.find('md-tabs-canvas').eq(0), $mdConstant.KEY_CODE.TAB);
211+
expect(tabItems.eq(0)).toBeActiveTab();
212+
expect(ctrl.getFocusedTabId()).toBe(tabItems.eq(0).attr('id'));
213+
}));
214+
201215
});
202216

203217
describe('tab label & content DOM', function () {
@@ -353,12 +367,12 @@ describe('<md-tabs>', function () {
353367
var tabs = setup('<md-tabs>' +
354368
'<md-tab label="label!">content!</md-tab>' +
355369
'</md-tabs>');
356-
var tabItem = tabs.find('md-dummy-tab');
370+
var tabItem = tabs.find('md-tab-item');
357371
var tabContent = angular.element(tabs[ 0 ].querySelector('md-tab-content'));
358372

359373
$timeout.flush();
360374

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

363377
expect(tabItem.attr('id')).toBeTruthy();
364378
expect(tabItem.attr('aria-controls')).toBe(tabContent.attr('id'));

0 commit comments

Comments
 (0)