Skip to content

Commit a1e23ad

Browse files
committed
Tabs: Resolve nested interactive controls issue
- Ensures focus is applied directly to <a> elements rather than <li>
1 parent 317922a commit a1e23ad

File tree

1 file changed

+38
-20
lines changed

1 file changed

+38
-20
lines changed

ui/widgets/tabs.js

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ $.widget( "ui.tabs", {
176176
},
177177

178178
_tabKeydown: function( event ) {
179-
var focusedAnchor = $( this.document[ 0 ].activeElement ).closest( "a" ),
180-
focusedTab = focusedAnchor.closest( "li" ),
179+
var focusedTab = $( this.document[ 0 ].activeElement ).closest( "li" ),
181180
selectedIndex = this.tabs.index( focusedTab ),
182181
goingForward = true;
183182

@@ -203,12 +202,14 @@ $.widget( "ui.tabs", {
203202
break;
204203
case $.ui.keyCode.SPACE:
205204

205+
// Activate only, no collapsing
206206
event.preventDefault();
207207
clearTimeout( this.activating );
208208
this._activate( selectedIndex );
209209
return;
210210
case $.ui.keyCode.ENTER:
211211

212+
// Toggle (cancel delayed activation, allow collapsing)
212213
event.preventDefault();
213214
clearTimeout( this.activating );
214215

@@ -227,8 +228,11 @@ $.widget( "ui.tabs", {
227228
// Navigating with control/command key will prevent automatic activation
228229
if ( !event.ctrlKey && !event.metaKey ) {
229230

230-
focusedAnchor.attr( "aria-selected", "false" );
231-
this.anchors.eq( selectedIndex ).attr( "aria-selected", "true" );
231+
// Update aria-selected immediately so that AT think the tab is already selected.
232+
// Otherwise AT may confuse the user by stating that they need to activate the tab,
233+
// but the tab will already be activated by the time the announcement finishes.
234+
focusedTab.attr( "aria-selected", "false" );
235+
this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
232236

233237
this.activating = this._delay( function() {
234238
this.option( "active", selectedIndex );
@@ -282,7 +286,7 @@ $.widget( "ui.tabs", {
282286

283287
_focusNextTab: function( index, goingForward ) {
284288
index = this._findNextTab( index, goingForward );
285-
this.anchors.eq( index ).trigger( "focus" );
289+
this.tabs.eq( index ).trigger( "focus" );
286290
return index;
287291
},
288292

@@ -407,32 +411,42 @@ $.widget( "ui.tabs", {
407411
}
408412
} );
409413

410-
this.tabs = this.tablist.find( "> li:has(a[href])" )
414+
this.tabs = this.tablist.find( "> li:has(a[href]) > a" )
411415
.attr( {
412-
role: "presentation"
416+
role: "tab",
417+
tabindex: -1
413418
} );
414419
this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
415-
416-
this.anchors = this.tabs.map( function() {
417-
return $( "a", this )[ 0 ];
418-
} )
420+
this.tablist.find( "> li:has(a[href])" )
419421
.attr( {
420-
role: "tab",
421-
tabIndex: -1
422+
role: "presentation"
422423
} );
424+
425+
this.anchors = this.tabs;
426+
this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
423427
this._addClass( this.anchors, "ui-tabs-anchor" );
424428

425429
this.panels = $();
426430

427431
this.anchors.each( function( i, anchor ) {
428432
var selector, panel, panelId,
429433
anchorId = $( anchor ).uniqueId().attr( "id" ),
430-
tab = $( anchor ).closest( "li" ),
434+
tab = $( anchor ),
431435
originalAriaControls = tab.attr( "aria-controls" );
432436

433437
// Inline tab
434438
if ( that._isLocal( anchor ) ) {
435439

440+
// The "scrolling to a fragment" section of the HTML spec:
441+
// https://html.spec.whatwg.org/#scrolling-to-a-fragment
442+
// uses a concept of document's indicated part:
443+
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
444+
// Slightly below there's an algorithm to compute the indicated
445+
// part:
446+
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
447+
// First, the algorithm tries the hash as-is, without decoding.
448+
// Then, if one is not found, the same is attempted with a decoded
449+
// hash. Replicate this logic.
436450
selector = anchor.hash;
437451
panelId = selector.substring( 1 );
438452
panel = that.element.find( "#" + CSS.escape( panelId ) );
@@ -441,8 +455,11 @@ $.widget( "ui.tabs", {
441455
panel = that.element.find( "#" + CSS.escape( panelId ) );
442456
}
443457

458+
// remote tab
444459
} else {
445460

461+
// If the tab doesn't already have aria-controls,
462+
// generate an id by using a throw-away element
446463
panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
447464
selector = "#" + panelId;
448465
panel = that.element.find( selector );
@@ -537,7 +554,7 @@ $.widget( "ui.tabs", {
537554
this._on( this.tabs, { keydown: "_tabKeydown" } );
538555
this._on( this.panels, { keydown: "_panelKeydown" } );
539556

540-
this._focusable( this.anchors );
557+
this._focusable( this.tabs );
541558
this._hoverable( this.tabs );
542559
},
543560

@@ -580,7 +597,7 @@ $.widget( "ui.tabs", {
580597
var options = this.options,
581598
active = this.active,
582599
anchor = $( event.currentTarget ),
583-
tab = anchor.closest( "li" ),
600+
tab = anchor,
584601
clickedIsActive = tab[ 0 ] === active[ 0 ],
585602
collapsing = clickedIsActive && options.collapsible,
586603
toShow = collapsing ? $() : this._getPanelForTab( tab ),
@@ -641,7 +658,7 @@ $.widget( "ui.tabs", {
641658
}
642659

643660
function show() {
644-
that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
661+
that._addClass( eventData.newTab, "ui-tabs-active", "ui-state-active" );
645662

646663
if ( toShow.length && that.options.show ) {
647664
that._show( toShow, that.options.show, complete );
@@ -654,12 +671,12 @@ $.widget( "ui.tabs", {
654671
// Start out by hiding, then showing, then completing
655672
if ( toHide.length && this.options.hide ) {
656673
this._hide( toHide, this.options.hide, function() {
657-
that._removeClass( eventData.oldTab.closest( "li" ),
674+
that._removeClass( eventData.oldTab,
658675
"ui-tabs-active", "ui-state-active" );
659676
show();
660677
} );
661678
} else {
662-
this._removeClass( eventData.oldTab.closest( "li" ),
679+
this._removeClass( eventData.oldTab,
663680
"ui-tabs-active", "ui-state-active" );
664681
toHide.hide();
665682
show();
@@ -818,7 +835,7 @@ $.widget( "ui.tabs", {
818835
index = this._getIndex( index );
819836
var that = this,
820837
tab = this.tabs.eq( index ),
821-
anchor = tab.find( ".ui-tabs-anchor" ),
838+
anchor = tab,
822839
panel = this._getPanelForTab( tab ),
823840
eventData = {
824841
tab: tab,
@@ -894,3 +911,4 @@ if ( $.uiBackCompat === true ) {
894911
return $.ui.tabs;
895912

896913
} );
914+

0 commit comments

Comments
 (0)