diff --git a/tests/unit/menubar/menubar_core.js b/tests/unit/menubar/menubar_core.js
index 7d01cf7b610..f9722c061da 100644
--- a/tests/unit/menubar/menubar_core.js
+++ b/tests/unit/menubar/menubar_core.js
@@ -19,4 +19,30 @@ test( "accessibility", function () {
ok( !element.attr( "aria-activedescendant" ), "aria-activedescendant not set" );
});
+test( "Cursor keys should move the focus", function() {
+ expect( 3 );
+
+ var element = $( "#bar1" ).menubar(),
+ firstMenuItem = $( "#bar1 .ui-menubar-item .ui-button:first" );
+
+ firstMenuItem[ 0 ].focus();
+ equal( document.activeElement, firstMenuItem[0], "Focus set on first menuItem" );
+ $( firstMenuItem ).simulate( "keydown", { keyCode: $.ui.keyCode.RIGHT } );
+ ok( !firstMenuItem.hasClass( "ui-state-focus" ), "RIGHT should move focus off of focused item" );
+ $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.LEFT } );
+ equal( document.activeElement, firstMenuItem[0], "LEFT should return focus first menuItem" );
+});
+
+test ( "_destroy should successfully unwrap 'span.ui-button-text' elements" , function() {
+ expect(1);
+
+ var containedButtonTextSpans,
+ element = $( "#bar1" ).menubar();
+
+ element.menubar( "destroy" );
+ containedButtonTextSpans = element.find( "span.ui-button-text" ).length
+ equal( containedButtonTextSpans, 0, "All 'span.ui-button-text' should be removed by destroy" );
+});
+
+
})( jQuery );
diff --git a/tests/unit/menubar/menubar_events.js b/tests/unit/menubar/menubar_events.js
index 9c2fe3394ab..a032aa64766 100644
--- a/tests/unit/menubar/menubar_events.js
+++ b/tests/unit/menubar/menubar_events.js
@@ -43,7 +43,29 @@ test( "hover over a menu item with no sub-menu should close open menu", function
menuItemWithDropdown.trigger("click");
menuItemWithoutDropdown.trigger("click");
- equal($(".ui-menu:visible").length, 0, "After triggering a sub-menu, a click on a peer menu item should close the opened sub-menu");
+ equal($(".ui-menu:visible").length, 0, "After triggering a sub-menu, a click on a peer menu item should close the opened sub-menu");
});
+test( "Cursor keys should move focus within the menu items", function() {
+ expect( 6 );
+
+ var element = $( "#bar1" ).menubar(),
+ firstMenuItem = $( "#bar1 .ui-menubar-item .ui-button:first" ),
+ nextLeftwardMenuElement = firstMenuItem.parent().siblings().last().children().eq( 0 );
+
+ equal( element.find( ":tabbable" ).length, 1, "A Menubar should have 1 tabbable element on init." );
+ firstMenuItem[ 0 ].focus();
+
+ ok( firstMenuItem.hasClass( "ui-state-focus" ), "After a focus event, the first element should have the focus class." );
+ $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.LEFT } );
+
+
+ ok( !firstMenuItem.hasClass( "ui-state-focus" ), "After a keypress event, the first element, should no longer have the focus class." );
+ ok( nextLeftwardMenuElement.hasClass( "ui-state-focus" ), "After a LEFT cursor event from the first element, the last element should have focus." );
+ equal( element.find( ":tabbable" ).length, 1, "A Menubar, after a cursor key action, should have 1 tabbable." );
+ equal( element.find( ":tabbable" )[ 0 ], nextLeftwardMenuElement[ 0 ], "A Menubar, after a cursor key action, should have 1 tabbable." );
+
+} );
+
+
})( jQuery );
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
index 3e8a143bd0d..15dcc5d9e85 100644
--- a/ui/jquery.ui.menubar.js
+++ b/ui/jquery.ui.menubar.js
@@ -15,8 +15,6 @@
*/
(function( $ ) {
-// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
-// there has to be just one item that has tabindex
$.widget( "ui.menubar", {
version: "@VERSION",
options: {
@@ -34,133 +32,85 @@ $.widget( "ui.menubar", {
_create: function() {
// Top-level elements containing the submenu-triggering elem
this.menuItems = this.element.children( this.options.items );
- // Links or buttons in menuItems, triggers of the submenus
- this.items = [];
- this._initializeMenubarsBoundElement();
- this._initializeWidget();
- this._initializeMenuItems();
+ // Links or buttons in menuItems, triggers of the submenus
+ this.items = this.menuItems.children( "button, a" );
// Keep track of open submenus
this.openSubmenus = 0;
- },
- _initializeMenubarsBoundElement: function() {
- this.element
- .addClass("ui-menubar ui-widget-header ui-helper-clearfix")
- .attr( "role", "menubar" );
+ this._initializeWidget();
+ this._initializeMenuItems();
+ this._initializeItems();
},
_initializeWidget: function() {
- var menubar = this;
-
- this._on( {
+ this.element
+ .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+ .attr( "role", "menubar" );
+ this._on( this.element, {
keydown: function( event ) {
- if ( event.keyCode === $.ui.keyCode.ESCAPE && menubar.active && menubar.active.menu( "collapse", event ) !== true ) {
- var active = menubar.active;
- menubar.active.blur();
- menubar._close( event );
+ var active;
+
+ // If we are in a nested sub-sub-menu and we see an ESCAPE
+ // we must close recursively.
+ if ( event.keyCode === $.ui.keyCode.ESCAPE &&
+ this.active &&
+ this.active.menu( "collapse", event ) !== true ) {
+ active = this.active;
+ this.active.blur();
+ this._close( event );
$( event.target ).blur().mouseleave();
active.prev().focus();
}
},
- focusin: function( event ) {
- clearTimeout( menubar.closeTimer );
+ focusin: function() {
+ this.items.eq( 0 ).attr( "tabIndex", -1 );
+ clearTimeout( this.closeTimer );
},
- focusout: function( event ) {
- menubar.closeTimer = setTimeout (function() {
- menubar._close( event );
+ focusout: function() {
+ this.closeTimer = this._delay( function() {
+ this._close( event );
+ this.items.eq( 0 ).attr( "tabIndex", 0 );
}, 150 );
},
- "mouseleave .ui-menubar-item": function( event ) {
- if ( menubar.options.autoExpand ) {
- menubar.closeTimer = setTimeout( function() {
- menubar._close( event );
+ "mouseleave .ui-menubar-item": function() {
+ if ( this.options.autoExpand ) {
+ this.closeTimer = this._delay( function() {
+ this._close();
}, 150 );
}
},
- "mouseenter .ui-menubar-item": function( event ) {
- clearTimeout( menubar.closeTimer );
+ "mouseenter .ui-menubar-item": function() {
+ clearTimeout( this.closeTimer );
}
- });
+ } );
},
_initializeMenuItems: function() {
- var $item,
+ var subMenus,
menubar = this;
this.menuItems
- .addClass("ui-menubar-item")
- .attr( "role", "presentation" );
-
- $.each( this.menuItems, function( index, menuItem ){
- menubar._initializeMenuItem( $( menuItem ), menubar );
- menubar._identifyMenuItemsNeighbors( $( menuItem ), menubar, index );
- } );
- },
-
- _identifyMenuItemsNeighbors: function( $menuItem, menubar, index ) {
- var collectionLength = this.menuItems.toArray().length,
- isFirstElement = ( index === 0 ),
- isLastElement = ( index === ( collectionLength - 1 ) );
-
- if ( isFirstElement ) {
- $menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
- $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
- } else if ( isLastElement ) {
- $menuItem.data( "nextMenuItem", $( this.menuItems[0]) );
- $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
- } else {
- $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
- $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
- }
- },
-
- _initializeMenuItem: function( $menuItem, menubar ) {
- var $item = $menuItem.children("button, a");
-
- menubar._determineSubmenuStatus( $menuItem, menubar );
- menubar._styleMenuItem( $menuItem, menubar );
-
- if ( $menuItem.data("hasSubMenu") ) {
- menubar._initializeSubMenu( $menuItem, menubar );
- }
-
- $item.data( "parentMenuItem", $menuItem );
- menubar.items.push( $item );
- menubar._initializeItem( $item, menubar );
- },
-
- _determineSubmenuStatus: function ( $menuItem, menubar ) {
- var subMenus = $menuItem.children( menubar.options.menuElement ),
- hasSubMenu = subMenus.length > 0;
- $menuItem.data( "hasSubMenu", hasSubMenu );
- },
-
- _styleMenuItem: function( $menuItem, menubar ) {
- $menuItem.css({
- "border-width" : "1px",
- "border-style" : "hidden"
- });
- },
-
- _initializeSubMenu: function( $menuItem, menubar ){
- var subMenus = $menuItem.children( menubar.options.menuElement );
+ .addClass( "ui-menubar-item" )
+ .attr( "role", "presentation" )
+ .css({
+ "border-width": "1px",
+ "border-style": "hidden"
+ });
- subMenus
- .menu({
- position: {
- within: this.options.position.within
- },
- select: function( event, ui ) {
- ui.item.parents("ul.ui-menu:last").hide();
- menubar._close();
- // TODO what is this targetting? there's probably a better way to access it
- $( event.target ).prev().focus();
- menubar._trigger( "select", event, ui );
- },
- menus: this.options.menuElement
- })
+ subMenus = this.menuItems.children( menubar.options.menuElement ).menu({
+ position: {
+ within: this.options.position.within
+ },
+ select: function( event, ui ) {
+ ui.item.parents( "ul.ui-menu:last" ).hide();
+ menubar._close();
+ ui.item.parents( ".ui-menubar-item" ).children().first().focus();
+ menubar._trigger( "select", event, ui );
+ },
+ menus: this.options.menuElement
+ })
.hide()
.attr({
"aria-hidden": "true",
@@ -169,25 +119,29 @@ $.widget( "ui.menubar", {
this._on( subMenus, {
keydown: function( event ) {
+ $(event.target).attr( "tabIndex", 0 );
var parentButton,
menu = $( this );
- if ( menu.is(":hidden") ) {
+ if ( menu.is( ":hidden" ) ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.LEFT:
- parentButton = menubar.active.prev(".ui-button");
+ parentButton = menubar.active.prev( ".ui-button" );
- if ( parentButton.parent().prev().data('hasSubMenu') ) {
+ if ( this.openSubmenus ) {
+ this.openSubmenus--;
+ } else if ( this._hasSubMenu( parentButton.parent().prev() ) ) {
menubar.active.blur();
- menubar._open( event, parentButton.parent().prev().find(".ui-menu") );
+ menubar._open( event, parentButton.parent().prev().find( ".ui-menu" ) );
} else {
- parentButton.parent().prev().find(".ui-button").focus();
+ parentButton.parent().prev().find( ".ui-button" ).focus();
menubar._close( event );
this.open = true;
}
event.preventDefault();
+ $(event.target).attr( "tabIndex", -1 );
break;
case $.ui.keyCode.RIGHT:
this.next( event );
@@ -196,217 +150,241 @@ $.widget( "ui.menubar", {
}
},
focusout: function( event ) {
- event.stopImmediatePropagation();
+ $(event.target).removeClass( "ui-state-focus" );
}
});
+
+ this.menuItems.each(function( index, menuItem ) {
+ menubar._identifyMenuItemsNeighbors( $( menuItem ), menubar, index );
+ });
+
},
- _initializeItem: function( $anItem, menubar ) {
- //only the first item is eligible to receive the focus
- var menuItemHasSubMenu = $anItem.data("parentMenuItem").data("hasSubMenu");
+ _hasSubMenu: function( menuItem ) {
+ return $( menuItem ).children( this.options.menuElement ).length > 0;
+ },
+
+ _identifyMenuItemsNeighbors: function( menuItem, menubar, index ) {
+ var collectionLength = this.menuItems.length,
+ isFirstElement = ( index === 0 ),
+ isLastElement = ( index === ( collectionLength - 1 ) );
- // Only the first item is tab-able
- if ( menubar.items.length === 1 ) {
- $anItem.attr( "tabindex", 1 );
+ if ( isFirstElement ) {
+ menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
+ menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
+ } else if ( isLastElement ) {
+ menuItem.data( "nextMenuItem", $( this.menuItems[0]) );
+ menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
} else {
- $anItem.attr( "tabIndex", -1 );
+ menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
+ menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
}
+ },
+
+ _initializeItems: function() {
+ var menubar = this;
this._focusable( this.items );
this._hoverable( this.items );
- this._applyDOMPropertiesOnItem( $anItem, menubar);
-
- this.__applyMouseAndKeyboardBehaviorForMenuItem ( $anItem, menubar );
- if ( menuItemHasSubMenu ) {
- this.__applyMouseBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
- this.__applyKeyboardBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
+ // let only the first item receive focus
+ this.items.slice(1).attr( "tabIndex", -1 );
- $anItem.attr( "aria-haspopup", "true" );
- if ( menubar.options.menuIcon ) {
- $anItem.addClass("ui-state-default").append("");
- $anItem.removeClass("ui-button-text-only").addClass("ui-button-text-icon-secondary");
- }
- } else {
- this.__applyMouseBehaviorForSubmenulessMenuItem( $anItem, menubar );
- this.__applyKeyboardBehaviorForSubmenulessMenuItem( $anItem, menubar );
- }
+ this.items.each(function( index, item ) {
+ menubar._initializeItem( $( item ), menubar );
+ });
},
- __applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
- menubar._on( $anItem, {
- focus: function( event ){
- $anItem.addClass("ui-state-focus");
- },
- focusout: function( event ){
- $anItem.removeClass("ui-state-focus");
- }
- } );
- },
+ _initializeItem: function( anItem ) {
+ var menubar = this,
+ menuItemHasSubMenu = this._hasSubMenu( anItem.parent() );
- _applyDOMPropertiesOnItem: function( $item, menubar) {
- $item
- .addClass("ui-button ui-widget ui-button-text-only ui-menubar-link")
+ anItem
+ .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
.attr( "role", "menuitem" )
- .wrapInner("");
+ .wrapInner( "" );
if ( menubar.options.buttons ) {
- $item.removeClass("ui-menubar-link").addClass("ui-state-default");
+ anItem.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
}
- },
- __applyMouseBehaviorForSubmenuHavingMenuItem: function ( input, menubar ) {
- var menu = input.next( menubar.options.menuElement ),
- mouseBehaviorCallback = function( event ) {
- // ignore triggered focus event
- if ( event.type === "focus" && !event.originalEvent ) {
- return;
- }
+ menubar._on( anItem, {
+ focus: function(){
+ anItem.attr( "tabIndex", 0 );
+ anItem.addClass( "ui-state-focus" );
event.preventDefault();
- // TODO can we simplify or extract this check? especially the last two expressions
- // there's a similar active[0] == menu[0] check in _open
- if ( event.type === "click" && menu.is(":visible") && this.active && this.active[0] === menu[0] ) {
- this._close();
- return;
- }
- if ( event.type === "mouseenter" ) {
- this.element.find(":focus").focusout();
- if ( this.stashedOpenMenu ) {
- this._open( event, menu);
+ },
+ focusout: function(){
+ anItem.attr( "tabIndex", -1 );
+ anItem.removeClass( "ui-state-focus" );
+ event.preventDefault();
+ }
+ } );
+
+ if ( menuItemHasSubMenu ) {
+ this._on( anItem, {
+ click: this._mouseBehaviorForMenuItemWithSubmenu,
+ focus: this._mouseBehaviorForMenuItemWithSubmenu,
+ mouseenter: this._mouseBehaviorForMenuItemWithSubmenu
+ });
+
+ this._on( anItem, {
+ keydown: function( event ) {
+ switch ( event.keyCode ) {
+ case $.ui.keyCode.SPACE:
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ this._open( event, $( event.target ).next() );
+ event.preventDefault();
+ break;
+ case $.ui.keyCode.LEFT:
+ this.previous( event );
+ event.preventDefault();
+ break;
+ case $.ui.keyCode.RIGHT:
+ this.next( event );
+ event.preventDefault();
+ break;
+ case $.ui.keyCode.TAB:
+ break;
}
- this.stashedOpenMenu = undefined;
}
- if ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
- if ( this.options.autoExpand ) {
- clearTimeout( this.closeTimer );
+ });
+
+ anItem.attr( "aria-haspopup", "true" );
+ if ( menubar.options.menuIcon ) {
+ anItem.addClass( "ui-state-default" ).append( "" );
+ anItem.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
+ }
+ } else {
+ menubar._on( anItem, {
+ click: function() {
+ if ( this.active ) {
+ this._close();
+ } else {
+ this.open = true;
+ this.active = $( anItem ).parent();
+ }
+ },
+ mouseenter: function() {
+ if ( this.open ) {
+ this.stashedOpenMenu = this.active;
+ this._close();
+ }
+ },
+ keydown: function( event ) {
+ if ( event.keyCode === $.ui.keyCode.LEFT ) {
+ this.previous( event );
+ event.preventDefault();
+ } else if ( event.keyCode === $.ui.keyCode.RIGHT ) {
+ this.next( event );
+ event.preventDefault();
}
- this._open( event, menu );
}
- };
-
- menubar._on( input, {
- click: mouseBehaviorCallback,
- focus: mouseBehaviorCallback,
- mouseenter: mouseBehaviorCallback
- });
+ });
+ }
},
- __applyKeyboardBehaviorForSubmenuHavingMenuItem: function( input, menubar ) {
- var keyboardBehaviorCallback = function( event ) {
- switch ( event.keyCode ) {
- case $.ui.keyCode.SPACE:
- case $.ui.keyCode.UP:
- case $.ui.keyCode.DOWN:
- menubar._open( event, $( event.target ).next() );
- event.preventDefault();
- break;
- case $.ui.keyCode.LEFT:
- this.previous( event );
- event.preventDefault();
- break;
- case $.ui.keyCode.RIGHT:
- this.next( event );
- event.preventDefault();
- break;
- }
- };
+ _mouseBehaviorForMenuItemWithSubmenu: function( event ) {
+ var isClickingToCloseOpenMenu, menu;
- menubar._on( input, {
- keydown: keyboardBehaviorCallback
- });
- },
+ // ignore triggered focus event
+ if ( event.type === "focus" && !event.originalEvent ) {
+ return;
+ }
+ event.preventDefault();
- __applyMouseBehaviorForSubmenulessMenuItem: function( $anItem, menubar ) {
- menubar._off( $anItem, "click mouseenter" );
- menubar._hoverable( $anItem );
- menubar._on( $anItem, {
- click: function( event ) {
- if ( this.active ) {
- this._close();
- } else {
- this.open = true;
- this.active = $( $anItem ).parent();
- }
- },
- mouseenter: function( event ) {
- if ( this.open ) {
- this.stashedOpenMenu = this.active;
- this._close();
- }
- }
- });
- },
- __applyKeyboardBehaviorForSubmenulessMenuItem: function( $anItem, menubar ) {
- var behavior = function( event ) {
- if ( event.keyCode === $.ui.keyCode.LEFT ) {
- this.previous( event );
- event.preventDefault();
- } else if ( event.keyCode === $.ui.keyCode.RIGHT ) {
- this.next( event );
- event.preventDefault();
+ menu = $(event.target).parents( ".ui-menubar-item" ).children( "ul" );
+
+ // If we have an open menu and we see a click on the menuItem
+ // and the menu thereunder is the same as the active menu, close it.
+ // Succinctly: toggle menu open / closed on the menuItem
+ isClickingToCloseOpenMenu = event.type === "click" &&
+ menu.is( ":visible" ) &&
+ this.active &&
+ this.active[0] === menu[0];
+
+ if ( isClickingToCloseOpenMenu ) {
+ this._close();
+ return;
+ }
+ if ( event.type === "mouseenter" ) {
+ this.element.find( ":focus" ).focusout();
+ if ( this.stashedOpenMenu ) {
+ this._open( event, menu);
}
- };
- menubar._on( $anItem, {
- keydown: behavior
- });
+ this.stashedOpenMenu = undefined;
+ }
+ // If we already opened a menu and then changed to be "over" another MenuItem ||
+ // we clicked on a new menuItem (whether open or not) or if we auto expand (i.e.
+ // we expand regardless of click if there is a submenu
+ if ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
+ clearTimeout( this.closeTimer );
+ this._open( event, menu );
+ // Stop propagation so that menuItem mouseenter doesn't fire. If it does it
+ // takes the "selected" status off off of the first element of the submenu.
+ event.stopPropagation();
+ }
},
_destroy : function() {
this.menuItems
- .removeClass("ui-menubar-item")
- .removeAttr("role");
+ .removeClass( "ui-menubar-item" )
+ .removeAttr( "role" )
+ .css({
+ "border-width": "",
+ "border-style": ""
+ });
this.element
- .removeClass("ui-menubar ui-widget-header ui-helper-clearfix")
- .removeAttr("role")
- .unbind(".menubar");
+ .removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+ .removeAttr( "role" )
+ .unbind( ".menubar" );
this.items
- .unbind(".menubar")
- .removeClass("ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default")
- .removeAttr("role")
- .removeAttr("aria-haspopup")
- // TODO unwrap?
- .children("span.ui-button-text").each(function( i, e ) {
+ .unbind( ".menubar" )
+ .removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
+ .removeAttr( "role" )
+ .removeAttr( "aria-haspopup" )
+ .children( ".ui-icon" ).remove();
+
+ if ( false ) {
+ // Does not unwrap
+ this.items.children( "span.ui-button-text" ).unwrap();
+ } else {
+ // Does "unwrap"
+ this.items.children( "span.ui-button-text" ).each( function(){
var item = $( this );
item.parent().html( item.html() );
- })
- .end()
- .children(".ui-icon").remove();
+ });
+ }
- this.element.find(":ui-menu")
- .menu("destroy")
+ this.element.find( ":ui-menu" )
+ .menu( "destroy" )
.show()
- .removeAttr("aria-hidden")
- .removeAttr("aria-expanded")
- .removeAttr("tabindex")
- .unbind(".menubar");
+ .removeAttr( "aria-hidden" )
+ .removeAttr( "aria-expanded" )
+ .removeAttr( "tabindex" )
+ .unbind( ".menubar" );
+ },
+
+ _collapseActiveMenu: function() {
+ this.active
+ .menu( "collapseAll" )
+ .hide()
+ .attr({
+ "aria-hidden": "true",
+ "aria-expanded": "false"
+ })
+ .closest( this.options.items ).removeClass( "ui-state-active" );
},
_close: function() {
- if ( !this.active || !this.active.length ) {
+ if ( !this.active ) {
return;
}
- if ( this.active.closest( this.options.items ).data("hasSubMenu") ) {
- this.active
- .menu("collapseAll")
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- this.active
- .prev()
- .removeClass("ui-state-active");
- this.active.closest( this.options.items ).removeClass("ui-state-active");
- } else {
- this.active
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- }
+ this._collapseActiveMenu();
this.active = null;
this.open = false;
@@ -415,55 +393,46 @@ $.widget( "ui.menubar", {
_open: function( event, menu ) {
var button,
- menuItem = menu.closest(".ui-menubar-item");
-
- if ( this.active && this.active.length ) {
- // TODO refactor, almost the same as _close above, but don't remove tabIndex
- if ( this.active.closest( this.options.items ).data("hasSubMenu") ) {
- this.active
- .menu("collapseAll")
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- this.active.closest(this.options.items)
- .removeClass("ui-state-active");
- } else {
- this.active.removeClass("ui-state-active");
- }
+ menuItem = menu.closest( ".ui-menubar-item" );
+
+ if ( this.active && this.active.length &&
+ this._hasSubMenu( this.active.closest( this.options.items ) ) ) {
+ this._collapseActiveMenu();
}
- // set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
- button = menuItem.addClass("ui-state-active").attr( "tabIndex", -1 );
+ button = menuItem.addClass( "ui-state-active" );
this.active = menu
.show()
.position( $.extend({
of: button
}, this.options.position ) )
- .removeAttr("aria-hidden")
- .attr("aria-expanded", "true")
- .menu("focus", event, menu.children(".ui-menu-item").first() )
- // TODO need a comment here why both events are triggered
- .focus()
- .focusin();
+ .removeAttr( "aria-hidden" )
+ .attr( "aria-expanded", "true" )
+ .menu( "focus", event, menu.children( ".ui-menu-item" ).first() )
+ .focus();
this.open = true;
},
next: function( event ) {
- if ( this.open && this.active &&
- this.active.closest( this.options.items ).data("hasSubMenu") &&
- this.active.data("menu").active &&
- this.active.data("menu").active.has(".ui-menu").length ) {
- // Track number of open submenus and prevent moving to next menubar item
- this.openSubmenus++;
- return;
+ function shouldOpenNestedSubMenu() {
+ return this.active &&
+ this._hasSubMenu( this.active.closest( this.options.items ) ) &&
+ this.active.data( "uiMenu" ) &&
+ this.active.data( "uiMenu" ).active &&
+ this.active.data( "uiMenu" ).active.has( ".ui-menu" ).length;
}
- this.openSubmenus = 0;
- this._move( "next", "first", event );
+ if ( this.open ) {
+ if ( shouldOpenNestedSubMenu.call( this ) ) {
+ // Track number of open submenus and prevent moving to next menubar item
+ this.openSubmenus++;
+ return;
+ }
+ }
+ this.openSubmenus = 0;
+ this._move( "next", event );
},
previous: function( event ) {
@@ -473,52 +442,26 @@ $.widget( "ui.menubar", {
return;
}
this.openSubmenus = 0;
- this._move( "prev", "last", event );
+ this._move( "prev", event );
},
- _move: function( direction, filter, event ) {
- var next,
- wrapItem;
-
- var closestMenuItem = $( event.target ).closest(".ui-menubar-item"),
+ _move: function( direction, event ) {
+ var closestMenuItem = $( event.target ).closest( ".ui-menubar-item" ),
nextMenuItem = closestMenuItem.data( direction + "MenuItem" ),
- focusableTarget = nextMenuItem.find("a, button");
+ focusableTarget = nextMenuItem.find( ".ui-button" );
if ( this.open ) {
- if ( nextMenuItem.data("hasSubMenu") ) {
- this._open( event, nextMenuItem.children(".ui-menu") );
+ if ( this._hasSubMenu( nextMenuItem ) ) {
+ this._open( event, nextMenuItem.children( ".ui-menu" ) );
} else {
- this._submenuless_open( event, nextMenuItem );
+ this._collapseActiveMenu();
+ nextMenuItem.find( ".ui-button" ).focus();
+ this.open = true;
}
+ } else {
+ closestMenuItem.find( ".ui-button" );
+ focusableTarget.focus();
}
-
- focusableTarget.focus();
- },
-
- _submenuless_open: function( event, next ) {
- var button,
- menuItem = next.closest(".ui-menubar-item");
-
- if ( this.active && this.active.length ) {
- // TODO refactor, almost the same as _close above, but don't remove tabIndex
- if ( this.active.closest( this.options.items ) ) {
- this.active
- .menu("collapseAll")
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- }
- this.active.closest(this.options.items)
- .removeClass("ui-state-active");
- }
-
- // set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
- button = menuItem.attr( "tabIndex", -1 );
-
- this.open = true;
- this.active = menuItem;
}
});