From 0c05afc515db3f9aad68cd9fde01801041826478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Wed, 23 May 2012 10:23:31 -0400 Subject: [PATCH 01/13] Tabs: ARIA! DO NOT MERGE TO MASTER! The demo is changed purely for testing. Needs tests. --- demos/tabs/default.html | 30 +++-- ui/jquery.ui.tabs.js | 240 ++++++++++++++++++++++++++++++++-------- 2 files changed, 213 insertions(+), 57 deletions(-) diff --git a/demos/tabs/default.html b/demos/tabs/default.html index 1d2f6f2a1b1..264fd677559 100644 --- a/demos/tabs/default.html +++ b/demos/tabs/default.html @@ -11,7 +11,9 @@ @@ -21,19 +23,31 @@
-

Proin elit arcu, rutrum commodo, vehicula tempus, commodo a, risus. Curabitur nec arcu. Donec sollicitudin mi sit amet mauris. Nam elementum quam ullamcorper ante. Etiam aliquet massa et lorem. Mauris dapibus lacus auctor risus. Aenean tempor ullamcorper leo. Vivamus sed magna quis ligula eleifend adipiscing. Duis orci. Aliquam sodales tortor vitae ipsum. Aliquam nulla. Duis aliquam molestie erat. Ut et mauris vel pede varius sollicitudin. Sed ut dolor nec orci tincidunt interdum. Phasellus ipsum. Nunc tristique tempus lectus.

+

tab 1

-

Morbi tincidunt, dui sit amet facilisis feugiat, odio metus gravida ante, ut pharetra massa metus id nunc. Duis scelerisque molestie turpis. Sed fringilla, massa eget luctus malesuada, metus eros molestie lectus, ut tempus eros massa ut dolor. Aenean aliquet fringilla sem. Suspendisse sed ligula in ligula suscipit aliquam. Praesent in eros vestibulum mi adipiscing adipiscing. Morbi facilisis. Curabitur ornare consequat nunc. Aenean vel metus. Ut posuere viverra nulla. Aliquam erat volutpat. Pellentesque convallis. Maecenas feugiat, tellus pellentesque pretium posuere, felis lorem euismod felis, eu ornare leo nisi vel felis. Mauris consectetur tortor et purus.

+

tab 2

-

Mauris eleifend est et turpis. Duis id erat. Suspendisse potenti. Aliquam vulputate, pede vel vehicula accumsan, mi neque rutrum erat, eu congue orci lorem eget lorem. Vestibulum non ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce sodales. Quisque eu urna vel enim commodo pellentesque. Praesent eu risus hendrerit ligula tempus pretium. Curabitur lorem enim, pretium nec, feugiat nec, luctus a, lacus.

-

Duis cursus. Maecenas ligula eros, blandit nec, pharetra at, semper at, magna. Nullam ac lacus. Nulla facilisi. Praesent viverra justo vitae neque. Praesent blandit adipiscing velit. Suspendisse potenti. Donec mattis, pede vel pharetra blandit, magna ligula faucibus eros, id euismod lacus dolor eget odio. Nam scelerisque. Donec non libero sed nulla mattis commodo. Ut sagittis. Donec nisi lectus, feugiat porttitor, tempor ac, tempor vitae, pede. Aenean vehicula velit eu tellus interdum rutrum. Maecenas commodo. Pellentesque nec elit. Fusce in lacus. Vivamus a libero vitae lectus hendrerit hendrerit.

+

tab 3

+
+
+

tab 4

+
+
+

tab 6

+
+
+

tab 7

diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index a693899da0c..55b64f0d851 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -52,7 +52,9 @@ $.widget( "ui.tabs", { this.running = false; - this.element.addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ); + this.element + .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ) + .toggleClass( "ui-tabs-collapsible", options.collapsible ); this._processTabs(); @@ -102,22 +104,18 @@ $.widget( "ui.tabs", { ) ).sort(); } - this._refresh(); - - // highlight selected tab - this.panels.hide(); - this.lis.removeClass( "ui-tabs-active ui-state-active" ); // check for length avoids error when initializing empty list - if ( options.active !== false && this.anchors.length ) { - this.active = this._findActive( options.active ); - panel = this._getPanelForTab( this.active ); - - panel.show(); - this.lis.eq( options.active ).addClass( "ui-tabs-active ui-state-active" ); - this.load( options.active ); + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( this.options.active ); } else { this.active = $(); } + + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } }, _getCreateEventData: function() { @@ -127,6 +125,71 @@ $.widget( "ui.tabs", { }; }, + _tabKeydown: function( event ) { + var focusedTab = $( this.document[0].activeElement ).closest( "li" ), + selectedIndex = this.lis.index( focusedTab ), + goingForward = true, + lastTabIndex = this.anchors.length - 1; + + function normalizeIndex( index ) { + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = lastTabIndex; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + case $.ui.keyCode.ENTER: + event.preventDefault(); + clearTimeout( this.activating ); + // TODO: should keyboard collapse content? + this.option( "active", selectedIndex ); + return; + default: + return; + } + + event.preventDefault(); + + while ( $.inArray( selectedIndex, this.options.disabled ) !== -1 ) { + selectedIndex = goingForward ? selectedIndex + 1 : selectedIndex - 1; + selectedIndex = normalizeIndex( selectedIndex ); + } + + this.lis.eq( selectedIndex ).focus(); + clearTimeout( this.activating ); + this.activating = this._delay(function() { + this.option( "active", selectedIndex ); + // TODO: what should this delay be? + }, 500 ); + }, + + _panelKeydown: function( event ) { + if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + event.preventDefault(); + this.active.focus(); + } + }, + _setOption: function( key, value ) { if ( key === "active" ) { // _activate() will handle invalid values and update this.options @@ -142,9 +205,12 @@ $.widget( "ui.tabs", { this._super( key, value); - // setting collapsible: false while collapsed; open first panel - if ( key === "collapsible" && !value && this.options.active === false ) { - this._activate( 0 ); + if ( key === "collapsible" ) { + this.element.toggleClass( "ui-tabs-collapsible", value ); + // setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } } if ( key === "event" ) { @@ -172,8 +238,6 @@ $.widget( "ui.tabs", { }); this._processTabs(); - this._refresh(); - this.panels.not( this._getPanelForTab( this.active ) ).hide(); // was collapsed or no tabs if ( options.active === false || !this.anchors.length ) { @@ -189,34 +253,67 @@ $.widget( "ui.tabs", { // make sure active index is correct options.active = this.lis.index( this.active ); } + + this._refresh(); }, _refresh: function() { - var options = this.options; + this._setupDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); - this.element.toggleClass( "ui-tabs-collapsible", options.collapsible ); - this.list.addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); - this.lis.addClass( "ui-state-default ui-corner-top" ); - this.anchors.addClass( "ui-tabs-anchor" ); - this.panels.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ); - - this._setupDisabled( options.disabled ); - this._setupEvents( options.event ); + this.lis.not( this.active ).attr({ + "aria-selected": "false", + tabIndex: -1 + }); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); - // remove all handlers, may run on existing tabs - this.lis.unbind( ".tabs" ); - this._focusable( this.lis ); - this._hoverable( this.lis ); + // make sure one tab is in the tab order + if ( !this.active.length ) { + this.lis.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .addClass( "ui-tabs-active ui-state-active" ) + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + this._getPanelForTab( this.active ) + .show() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } }, _processTabs: function() { var that = this; - this.list = this._getList(); - this.lis = this.list.find( "> li:has(a[href])" ); + this.list = this._getList() + .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .attr( "role", "tablist" ); + + this.lis = this.list.find( "> li:has(a[href])" ) + .addClass( "ui-state-default ui-corner-top" ) + .attr({ + role: "tab", + tabIndex: -1 + }); + this.anchors = this.lis.map(function() { - return $( "a", this )[ 0 ]; - }); + return $( "a", this )[ 0 ]; + }) + .addClass( "ui-tabs-anchor" ) + .attr({ + role: "presentation", + tabIndex: -1 + }); + this.panels = $(); this.anchors.each(function( i, a ) { @@ -236,6 +333,7 @@ $.widget( "ui.tabs", { panel = that._createPanel( id ); panel.insertAfter( that.panels[ i - 1 ] || that.list ); } + panel.attr( "aria-live", "polite" ); } if ( panel.length) { @@ -243,6 +341,10 @@ $.widget( "ui.tabs", { } tab.attr( "aria-controls", selector.substring( 1 ) ); }); + + this.panels + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .attr( "role", "tabpanel" ); }, // allow overriding how to find the list for rare usage scenarios (#7715) @@ -286,8 +388,14 @@ $.widget( "ui.tabs", { events[ eventName ] = "_eventHandler"; }); } - this.anchors.unbind( ".tabs" ); + + this.anchors.add( this.lis ).add( this.panels ).unbind( ".tabs" ); this._bind( this.anchors, events ); + this._bind( this.lis, { keydown: "_tabKeydown" } ); + this._bind( this.panels, { keydown: "_panelKeydown" } ); + + this._focusable( this.lis ); + this._hoverable( this.lis ); }, _eventHandler: function( event ) { @@ -373,6 +481,32 @@ $.widget( "ui.tabs", { toHide.hide(); show(); } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + eventData.oldTab.attr( "aria-selected", "false" ); + // if we're switching tabs, remove the old tab from the tab order + // if we're opening from collapsed state, remove the previous tab from the tab order + // if we're collapsing, then keep the collapsing tab in the tab order + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.lis.filter(function() { + return $( this ).attr( "tabIndex" ) === 0; + }) + .attr( "tabIndex", -1 ); + } + + toShow.attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + eventData.newTab.attr({ + "aria-selected": "true", + tabIndex: 0 + }); }, _activate: function( index ) { @@ -423,10 +557,14 @@ $.widget( "ui.tabs", { this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ); - this.list.removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ); + this.list + .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .removeAttr( "role" ); this.anchors .removeClass( "ui-tabs-anchor" ) + .removeAttr( "role" ) + .removeAttr( "tabIndex" ) .unbind( ".tabs" ) .removeData( "href.tabs" ) .removeData( "load.tabs" ); @@ -435,20 +573,22 @@ $.widget( "ui.tabs", { if ( $.data( this, "ui-tabs-destroy" ) ) { $( this ).remove(); } else { - $( this ).removeClass([ - "ui-state-default", - "ui-corner-top", - "ui-tabs-active", - "ui-state-active", - "ui-state-disabled", - "ui-tabs-panel", - "ui-widget-content", - "ui-corner-bottom" - ].join( " " ) ); + $( this ) + .removeClass([ + "ui-state-default", + "ui-corner-top", + "ui-tabs-active", + "ui-state-active", + "ui-state-disabled", + "ui-tabs-panel", + "ui-widget-content", + "ui-corner-bottom" + ].join( " " ) ) + .removeAttr( "tabIndex" ) + .removeAttr( "aria-live" ) + .removeAttr( "aria-busy" ); } }); - - return this; }, enable: function( index ) { @@ -525,6 +665,7 @@ $.widget( "ui.tabs", { // but as of 1.8, $.ajax() always returns a jqXHR object. if ( this.xhr && this.xhr.statusText !== "canceled" ) { tab.addClass( "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); this.xhr .success(function( response ) { @@ -544,6 +685,7 @@ $.widget( "ui.tabs", { } tab.removeClass( "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); if ( jqXHR === that.xhr ) { delete that.xhr; From 111e71c28c20710a2c7d004478287f01cfa49abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Wed, 30 May 2012 15:03:37 -0400 Subject: [PATCH 02/13] Tabs: Added aria-labelledby to panels and tabs. Added aria-disabled to tabs. Clean up ARIA attributes on destroy. --- ui/jquery.ui.tabs.js | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index f6858081925..cc1c86f1c07 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -322,21 +322,22 @@ $.widget( "ui.tabs", { this.panels = $(); - this.anchors.each(function( i, a ) { - var selector, panel, id, - tab = $( a ).closest( "li" ); + this.anchors.each(function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ); // inline tab - if ( isLocal( a ) ) { - selector = a.hash; + if ( isLocal( anchor ) ) { + selector = anchor.hash; panel = that.element.find( that._sanitizeSelector( selector ) ); // remote tab } else { - id = that._tabId( tab ); - selector = "#" + id; + panelId = that._tabId( tab ); + selector = "#" + panelId; panel = that.element.find( selector ); if ( !panel.length ) { - panel = that._createPanel( id ); + panel = that._createPanel( panelId ); panel.insertAfter( that.panels[ i - 1 ] || that.list ); } panel.attr( "aria-live", "polite" ); @@ -347,7 +348,11 @@ $.widget( "ui.tabs", { } tab .data( "ui-tabs-aria-controls", tab.attr( "aria-controls" ) ) - .attr( "aria-controls", selector.substring( 1 ) ); + .attr({ + "aria-controls": selector.substring( 1 ), + "aria-labelledby": anchorId + }); + panel.attr( "aria-labelledby", anchorId ); }); this.panels @@ -378,8 +383,15 @@ $.widget( "ui.tabs", { // disable tabs for ( var i = 0, li; ( li = this.lis[ i ] ); i++ ) { - $( li ).toggleClass( "ui-state-disabled", - ( disabled === true || $.inArray( i, disabled ) !== -1 ) ); + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + $( li ) + .addClass( "ui-state-disabled" ) + .attr( "aria-disabled", "true" ); + } else { + $( li ) + .removeClass( "ui-state-disabled" ) + .removeAttr( "aria-disabled" ); + } } this.options.disabled = disabled; @@ -613,7 +625,8 @@ $.widget( "ui.tabs", { .removeAttr( "tabIndex" ) .unbind( ".tabs" ) .removeData( "href.tabs" ) - .removeData( "load.tabs" ); + .removeData( "load.tabs" ) + .removeUniqueId(); this.lis.unbind( ".tabs" ).add( this.panels ).each(function() { if ( $.data( this, "ui-tabs-destroy" ) ) { @@ -632,7 +645,12 @@ $.widget( "ui.tabs", { ].join( " " ) ) .removeAttr( "tabIndex" ) .removeAttr( "aria-live" ) - .removeAttr( "aria-busy" ); + .removeAttr( "aria-busy" ) + .removeAttr( "aria-selected" ) + .removeAttr( "aria-labelledby" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "role" ); } }); From 9fc48bcf5aa373701ba9efa0302c63235c1015d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Wed, 30 May 2012 15:44:40 -0400 Subject: [PATCH 03/13] Tabs: Allow collapsing via keyboard. --- ui/jquery.ui.tabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index cc1c86f1c07..55dd7da8077 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -162,8 +162,8 @@ $.widget( "ui.tabs", { case $.ui.keyCode.ENTER: event.preventDefault(); clearTimeout( this.activating ); - // TODO: should keyboard collapse content? - this.option( "active", selectedIndex ); + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); return; default: return; From 66c371a5dfbe28a43aa3e84fbfb6f405f5a1aec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Wed, 30 May 2012 15:47:58 -0400 Subject: [PATCH 04/13] Tabs: Allow navigating through tabs without automatic activation. --- ui/jquery.ui.tabs.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index 55dd7da8077..ae961618f78 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -178,10 +178,13 @@ $.widget( "ui.tabs", { this.lis.eq( selectedIndex ).focus(); clearTimeout( this.activating ); - this.activating = this._delay(function() { - this.option( "active", selectedIndex ); - // TODO: what should this delay be? - }, 500 ); + + // Navigating with control key will prevent automatic activation + if ( !event.ctrlKey ) { + this.activating = this._delay(function() { + this.option( "active", selectedIndex ); + }, 500 ); + } }, _panelKeydown: function( event ) { From 6fe06faee4e34b4fc3372fb9a6d5afb7123e6a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 31 May 2012 14:31:09 -0400 Subject: [PATCH 05/13] Tabs: Space toggles, Enter activates. --- ui/jquery.ui.tabs.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index ae961618f78..add15ee89b1 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -159,12 +159,18 @@ $.widget( "ui.tabs", { selectedIndex = 0; break; case $.ui.keyCode.SPACE: - case $.ui.keyCode.ENTER: + // toggle (cancel delayed activation, allow collapsing) event.preventDefault(); clearTimeout( this.activating ); // Determine if we should collapse or activate this._activate( selectedIndex === this.options.active ? false : selectedIndex ); return; + case $.ui.keyCode.ENTER: + // activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; default: return; } From 3d735cf6f45e0d055b9b079677c698bb84b5b35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 31 May 2012 14:59:22 -0400 Subject: [PATCH 06/13] Tabs: Add alt+page up/down to move between tabs when focus is inside panels. --- ui/jquery.ui.tabs.js | 52 ++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index add15ee89b1..6bd5c5e0c3f 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -129,18 +129,7 @@ $.widget( "ui.tabs", { _tabKeydown: function( event ) { var focusedTab = $( this.document[0].activeElement ).closest( "li" ), selectedIndex = this.lis.index( focusedTab ), - goingForward = true, - lastTabIndex = this.anchors.length - 1; - - function normalizeIndex( index ) { - if ( index > lastTabIndex ) { - index = 0; - } - if ( index < 0 ) { - index = lastTabIndex; - } - return index; - } + goingForward = true; switch ( event.keyCode ) { case $.ui.keyCode.RIGHT: @@ -153,7 +142,7 @@ $.widget( "ui.tabs", { selectedIndex--; break; case $.ui.keyCode.END: - selectedIndex = lastTabIndex; + selectedIndex = this.anchors.length - 1; break; case $.ui.keyCode.HOME: selectedIndex = 0; @@ -176,14 +165,8 @@ $.widget( "ui.tabs", { } event.preventDefault(); - - while ( $.inArray( selectedIndex, this.options.disabled ) !== -1 ) { - selectedIndex = goingForward ? selectedIndex + 1 : selectedIndex - 1; - selectedIndex = normalizeIndex( selectedIndex ); - } - - this.lis.eq( selectedIndex ).focus(); clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); // Navigating with control key will prevent automatic activation if ( !event.ctrlKey ) { @@ -194,10 +177,37 @@ $.widget( "ui.tabs", { }, _panelKeydown: function( event ) { - if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + // ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { event.preventDefault(); this.active.focus(); + return; + } + + // alt+page up/down moves focus to the previous/next tab (and activates) + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + } + }, + + _focusNextTab: function( index, goingForward ) { + var lastTabIndex = this.anchors.length - 1; + + while ( $.inArray( index, this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + } + + this.lis.eq( index ).focus(); + return index; }, _setOption: function( key, value ) { From 6196abf58013855ae57be5c208ae6910d3508ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 31 May 2012 15:06:49 -0400 Subject: [PATCH 07/13] Tabs: Fixed looping with keyboard nav. --- ui/jquery.ui.tabs.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index 6bd5c5e0c3f..f12de803589 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -194,16 +194,20 @@ $.widget( "ui.tabs", { }, _focusNextTab: function( index, goingForward ) { - var lastTabIndex = this.anchors.length - 1; + var lastTabIndex = this.lis.length - 1; - while ( $.inArray( index, this.options.disabled ) !== -1 ) { - index = goingForward ? index + 1 : index - 1; + function constrain() { if ( index > lastTabIndex ) { index = 0; } if ( index < 0 ) { index = lastTabIndex; } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; } this.lis.eq( index ).focus(); From 1f0ea5b896cc67c110f2b5f812fae6f9676f4ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 31 May 2012 15:25:06 -0400 Subject: [PATCH 08/13] Tabs: Prevent focusing a disabled tab by clicking on it. --- ui/jquery.ui.tabs.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index f12de803589..2713327e4de 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -55,7 +55,24 @@ $.widget( "ui.tabs", { this.element .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ) - .toggleClass( "ui-tabs-collapsible", options.collapsible ); + .toggleClass( "ui-tabs-collapsible", options.collapsible ) + // Prevent users from focusing disabled tabs via click + .delegate( ".ui-tabs-nav > li", "mousedown", function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + }) + // support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .delegate( ".ui-tabs-anchor", "focus", function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + }); this._processTabs(); From 6b4457c1d3243b2e205cc71599f2db8f5d0af26b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Tue, 5 Jun 2012 20:28:49 -0400 Subject: [PATCH 09/13] Tabs: Handle alt+page up/down even on tabs. --- ui/jquery.ui.tabs.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index 2713327e4de..7c90c276fa6 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -148,6 +148,10 @@ $.widget( "ui.tabs", { selectedIndex = this.lis.index( focusedTab ), goingForward = true; + if ( this._handlePageNav( event ) ) { + return; + } + switch ( event.keyCode ) { case $.ui.keyCode.RIGHT: case $.ui.keyCode.DOWN: @@ -194,19 +198,26 @@ $.widget( "ui.tabs", { }, _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } + // ctrl+up moves focus to the current tab if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { event.preventDefault(); this.active.focus(); - return; } + }, - // alt+page up/down moves focus to the previous/next tab (and activates) + // alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; } if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; } }, From a0c63389cf758471a031154b1627c06705ddd7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 7 Jun 2012 19:33:05 -0400 Subject: [PATCH 10/13] Tabs: Swap space and enter. --- ui/jquery.ui.tabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index 7c90c276fa6..db378d59237 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -168,14 +168,14 @@ $.widget( "ui.tabs", { case $.ui.keyCode.HOME: selectedIndex = 0; break; - case $.ui.keyCode.SPACE: + case $.ui.keyCode.ENTER: // toggle (cancel delayed activation, allow collapsing) event.preventDefault(); clearTimeout( this.activating ); // Determine if we should collapse or activate this._activate( selectedIndex === this.options.active ? false : selectedIndex ); return; - case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: // activate only, no collapsing event.preventDefault(); clearTimeout( this.activating ); From 74316a4af714872351e09854af98f1fd0e039f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 7 Jun 2012 19:42:07 -0400 Subject: [PATCH 11/13] Tabs: Update aria-selected during navigation so that AT announces the tab as selected immediately. --- ui/jquery.ui.tabs.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index db378d59237..a8eec5c88b0 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -188,6 +188,11 @@ $.widget( "ui.tabs", { event.preventDefault(); clearTimeout( this.activating ); selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.lis.eq( selectedIndex ).attr( "aria-selected", "true" ); // Navigating with control key will prevent automatic activation if ( !event.ctrlKey ) { From c31592b1fee3e572b786feb62431173efc2f0fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 7 Jun 2012 20:43:10 -0400 Subject: [PATCH 12/13] Tabs: Drop delayed activation to 300ms and move to instance property like menu. --- ui/jquery.ui.tabs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index a8eec5c88b0..76e651861f0 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -30,6 +30,7 @@ function isLocal( anchor ) { $.widget( "ui.tabs", { version: "@VERSION", + delay: 300, options: { active: null, collapsible: false, @@ -198,7 +199,7 @@ $.widget( "ui.tabs", { if ( !event.ctrlKey ) { this.activating = this._delay(function() { this.option( "active", selectedIndex ); - }, 500 ); + }, this.delay ); } }, From 636a91f0d0866c4df8514209f4eb10c60ab3e3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Scott=20Gonz=C3=A1lez?= Date: Thu, 7 Jun 2012 21:07:05 -0400 Subject: [PATCH 13/13] Tabs: Only update aria-selected immediately if the user isn't navigating with ctrl. --- ui/jquery.ui.tabs.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/jquery.ui.tabs.js b/ui/jquery.ui.tabs.js index 76e651861f0..3ac5e1e0cec 100644 --- a/ui/jquery.ui.tabs.js +++ b/ui/jquery.ui.tabs.js @@ -189,14 +189,15 @@ $.widget( "ui.tabs", { event.preventDefault(); clearTimeout( this.activating ); selectedIndex = this._focusNextTab( selectedIndex, goingForward ); - // Update aria-selected immediately so that AT think the tab is already selected. - // Otherwise AT may confuse the user by stating that they need to activate the tab - // but the tab will already be activated by the time the announcement finishes. - focusedTab.attr( "aria-selected", "false" ); - this.lis.eq( selectedIndex ).attr( "aria-selected", "true" ); // Navigating with control key will prevent automatic activation if ( !event.ctrlKey ) { + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.lis.eq( selectedIndex ).attr( "aria-selected", "true" ); + this.activating = this._delay(function() { this.option( "active", selectedIndex ); }, this.delay );