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; } });