From 4a2d39395c1d551a5d64e7398e81f1d3e246c2b7 Mon Sep 17 00:00:00 2001
From: "Steven G. Harms"
Date: Sun, 7 Apr 2013 13:24:41 -0700
Subject: [PATCH 1/4] Menubar: move to next item applied focus() to too many
items
`_move` was believed to specify just one item,
called `focusableTarget`. Using the broader
selectors this would return anything matching `a` or
`button`. This means that the next item _as well
as_ any `button`s or `a`s found thereunder would
_also_ have their focus() event fired.
---
tests/unit/menubar/menubar_events.js | 13 +++++++++++++
ui/jquery.ui.menubar.js | 6 +++++-
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/tests/unit/menubar/menubar_events.js b/tests/unit/menubar/menubar_events.js
index 9c2fe3394ab..f308e23e7e9 100644
--- a/tests/unit/menubar/menubar_events.js
+++ b/tests/unit/menubar/menubar_events.js
@@ -46,4 +46,17 @@ test( "hover over a menu item with no sub-menu should close open menu", function
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 ( "_findNextFocusableTarget should find one and only one item", function() {
+ expect(2);
+
+ var element = $("#bar1").menubar(),
+ menubarWidget = element.data("ui-menubar"),
+ firstMenuItem = $("#bar1 .ui-menubar-item").eq(0),
+ expectedFocusableTarget = $("#bar1 .ui-menubar-item .ui-widget").eq(0),
+ result = menubarWidget._findNextFocusableTarget( firstMenuItem );
+
+ equal( expectedFocusableTarget[0], result[0], "_findNextFocusableTarget should return the focusable element underneath the menuItem" );
+ equal( 1, result.length, "One and only one item should be returned." );
+});
+
})( jQuery );
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
index 3e8a143bd0d..306bd120061 100644
--- a/ui/jquery.ui.menubar.js
+++ b/ui/jquery.ui.menubar.js
@@ -476,13 +476,17 @@ $.widget( "ui.menubar", {
this._move( "prev", "last", event );
},
+ _findNextFocusableTarget: function( menuItem ) {
+ return menuItem.find(".ui-button");
+ },
+
_move: function( direction, filter, event ) {
var next,
wrapItem;
var closestMenuItem = $( event.target ).closest(".ui-menubar-item"),
nextMenuItem = closestMenuItem.data( direction + "MenuItem" ),
- focusableTarget = nextMenuItem.find("a, button");
+ focusableTarget = this._findNextFocusableTarget( nextMenuItem );
if ( this.open ) {
if ( nextMenuItem.data("hasSubMenu") ) {
From d163b239976338143a60ea1d1c71ab45bb6df1f3 Mon Sep 17 00:00:00 2001
From: "Steven G. Harms"
Date: Mon, 8 Apr 2013 07:15:10 -0700
Subject: [PATCH 2/4] Menubar: Bug: Arrow right w/ open submenu error
1. Load page
1. Tab to first bar
1. Right cursor
1. Down cursor
1. Right cursor
1. Error
Test the object that is tested for an `active` for
definition before querying that property.
Create new method for assessing whether or not a
new submenu should be opened. The testing here is
a smell. This logic should be simplified.
---
ui/jquery.ui.menubar.js | 92 ++++++++++++++++++++++++-----------------
1 file changed, 55 insertions(+), 37 deletions(-)
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
index 306bd120061..39b20d38b91 100644
--- a/ui/jquery.ui.menubar.js
+++ b/ui/jquery.ui.menubar.js
@@ -64,12 +64,13 @@ $.widget( "ui.menubar", {
active.prev().focus();
}
},
- focusin: function( event ) {
+ focusin: function() {
clearTimeout( menubar.closeTimer );
},
focusout: function( event ) {
menubar.closeTimer = setTimeout (function() {
menubar._close( event );
+ menubar._reenableTabIndexOnFirstMenuItem();
}, 150 );
},
"mouseleave .ui-menubar-item": function( event ) {
@@ -79,15 +80,14 @@ $.widget( "ui.menubar", {
}, 150 );
}
},
- "mouseenter .ui-menubar-item": function( event ) {
+ "mouseenter .ui-menubar-item": function() {
clearTimeout( menubar.closeTimer );
}
});
},
_initializeMenuItems: function() {
- var $item,
- menubar = this;
+ var menubar = this;
this.menuItems
.addClass("ui-menubar-item")
@@ -122,6 +122,8 @@ $.widget( "ui.menubar", {
menubar._determineSubmenuStatus( $menuItem, menubar );
menubar._styleMenuItem( $menuItem, menubar );
+ $menuItem.data( "name", $item.text() );
+
if ( $menuItem.data("hasSubMenu") ) {
menubar._initializeSubMenu( $menuItem, menubar );
}
@@ -137,7 +139,7 @@ $.widget( "ui.menubar", {
$menuItem.data( "hasSubMenu", hasSubMenu );
},
- _styleMenuItem: function( $menuItem, menubar ) {
+ _styleMenuItem: function( $menuItem ) {
$menuItem.css({
"border-width" : "1px",
"border-style" : "hidden"
@@ -178,7 +180,9 @@ $.widget( "ui.menubar", {
case $.ui.keyCode.LEFT:
parentButton = menubar.active.prev(".ui-button");
- if ( parentButton.parent().prev().data('hasSubMenu') ) {
+ if ( this.openSubmenus ) {
+ this.openSubmenus--;
+ } else if ( parentButton.parent().prev().data("hasSubMenu") ) {
menubar.active.blur();
menubar._open( event, parentButton.parent().prev().find(".ui-menu") );
} else {
@@ -196,7 +200,7 @@ $.widget( "ui.menubar", {
}
},
focusout: function( event ) {
- event.stopImmediatePropagation();
+ $(event.target).removeClass("ui-state-focus");
}
});
},
@@ -235,10 +239,10 @@ $.widget( "ui.menubar", {
__applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
menubar._on( $anItem, {
- focus: function( event ){
+ focus: function(){
$anItem.addClass("ui-state-focus");
},
- focusout: function( event ){
+ focusout: function(){
$anItem.removeClass("ui-state-focus");
}
} );
@@ -320,7 +324,7 @@ $.widget( "ui.menubar", {
menubar._off( $anItem, "click mouseenter" );
menubar._hoverable( $anItem );
menubar._on( $anItem, {
- click: function( event ) {
+ click: function() {
if ( this.active ) {
this._close();
} else {
@@ -328,7 +332,7 @@ $.widget( "ui.menubar", {
this.active = $( $anItem ).parent();
}
},
- mouseenter: function( event ) {
+ mouseenter: function() {
if ( this.open ) {
this.stashedOpenMenu = this.active;
this._close();
@@ -367,7 +371,7 @@ $.widget( "ui.menubar", {
.removeAttr("role")
.removeAttr("aria-haspopup")
// TODO unwrap?
- .children("span.ui-button-text").each(function( i, e ) {
+ .children("span.ui-button-text").each(function() {
var item = $( this );
item.parent().html( item.html() );
})
@@ -445,25 +449,30 @@ $.widget( "ui.menubar", {
.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();
+ .focus() // Establish focus on the submenu item
+ .focusin(); // Move focus within the containing submenu
+
this.open = true;
},
+ _shouldOpenNestedSubMenu: function() {
+ return this.open &&
+ this.active &&
+ this.active.closest( this.options.items ).data("hasSubMenu") &&
+ this.active.data("uiMenu") &&
+ this.active.data("uiMenu").active &&
+ this.active.data("uiMenu").active.has(".ui-menu").length;
+ },
+
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 ) {
+ if ( this._shouldOpenNestedSubMenu() ) {
// Track number of open submenus and prevent moving to next menubar item
this.openSubmenus++;
return;
}
this.openSubmenus = 0;
this._move( "next", "first", event );
-
},
previous: function( event ) {
@@ -481,9 +490,6 @@ $.widget( "ui.menubar", {
},
_move: function( direction, filter, event ) {
- var next,
- wrapItem;
-
var closestMenuItem = $( event.target ).closest(".ui-menubar-item"),
nextMenuItem = closestMenuItem.data( direction + "MenuItem" ),
focusableTarget = this._findNextFocusableTarget( nextMenuItem );
@@ -494,18 +500,16 @@ $.widget( "ui.menubar", {
} else {
this._submenuless_open( event, nextMenuItem );
}
+ } else {
+ closestMenuItem.find(".ui-button").attr( "tabindex", -1 );
+ focusableTarget.focus();
}
-
- focusableTarget.focus();
},
- _submenuless_open: function( event, next ) {
- var button,
- menuItem = next.closest(".ui-menubar-item");
+ _submenuless_open: function( event, nextMenuItem) {
+ var menuItem = $(event.target).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 ) ) {
+ if ( this.active && this.active.length && menuItem.data("hasSubMenu") ) {
this.active
.menu("collapseAll")
.hide()
@@ -513,16 +517,30 @@ $.widget( "ui.menubar", {
"aria-hidden": "true",
"aria-expanded": "false"
});
- }
- this.active.closest(this.options.items)
- .removeClass("ui-state-active");
+ menuItem.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 );
+ nextMenuItem.find(".ui-button").focus();
this.open = true;
- this.active = menuItem;
+ },
+
+ _closeOpenMenu: function( menu ) {
+ menu
+ .menu("collapseAll")
+ .hide()
+ .attr({
+ "aria-hidden": "true",
+ "aria-expanded": "false"
+ });
+ },
+
+ _deactivateMenusParentButton: function( menu ) {
+ menu.parent(".ui-menubar-item").removeClass("ui-state-active");
+ },
+
+ _reenableTabIndexOnFirstMenuItem: function() {
+ $(this.menuItems[0]).find(".ui-widget").attr( "tabindex", 1 );
}
});
From d89437f45e47d07f92b3378fb1531042d3f6c7fb Mon Sep 17 00:00:00 2001
From: "Steven G. Harms"
Date: Thu, 18 Apr 2013 08:17:29 -0700
Subject: [PATCH 3/4] menubar: Repair TAB order bugs
I believe this fixes tab order.
Regrettably I cannot get the unit test (included
in this commit) which verifies the function of TAB
by use of `simulate` functioning.
The test under investigation is:
"TAB order should be sane mirroring dialog's test "
in `menubar_events.js`.
I'm including it in the PR in hopes that
additional review will present the flaw in my
implementation. I've tried mirroring the use in
dialog's test suite with no success.
Curiously my use of `simulate` _seems_ right
insofar as my test using it to trigger RIGHT and
LEFT cursor key events _is_ behaving as expected.
---
tests/unit/menubar/menubar_core.js | 14 +++++++
tests/unit/menubar/menubar_events.js | 63 ++++++++++++++++++++++++++--
ui/jquery.ui.menubar.js | 33 +++++++++------
3 files changed, 94 insertions(+), 16 deletions(-)
diff --git a/tests/unit/menubar/menubar_core.js b/tests/unit/menubar/menubar_core.js
index 7d01cf7b610..ab3cd01b297 100644
--- a/tests/unit/menubar/menubar_core.js
+++ b/tests/unit/menubar/menubar_core.js
@@ -19,4 +19,18 @@ 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" );
+} );
+
})( jQuery );
diff --git a/tests/unit/menubar/menubar_events.js b/tests/unit/menubar/menubar_events.js
index f308e23e7e9..869a8a941ce 100644
--- a/tests/unit/menubar/menubar_events.js
+++ b/tests/unit/menubar/menubar_events.js
@@ -50,13 +50,70 @@ test ( "_findNextFocusableTarget should find one and only one item", function()
expect(2);
var element = $("#bar1").menubar(),
- menubarWidget = element.data("ui-menubar"),
- firstMenuItem = $("#bar1 .ui-menubar-item").eq(0),
- expectedFocusableTarget = $("#bar1 .ui-menubar-item .ui-widget").eq(0),
+ menubarWidget = element.data( "ui-menubar" ),
+ firstMenuItem = $( "#bar1 .ui-menubar-item" ).eq( 0 ),
+ expectedFocusableTarget = $("#bar1 .ui-menubar-item .ui-widget").eq( 0 ),
result = menubarWidget._findNextFocusableTarget( firstMenuItem );
equal( expectedFocusableTarget[0], result[0], "_findNextFocusableTarget should return the focusable element underneath the menuItem" );
equal( 1, result.length, "One and only one item should be returned." );
});
+asyncTest( "TAB order should be sane mirroring dialog's test", function() {
+ expect( 3 );
+
+ var element = $( "#bar1" ).menubar(),
+ firstMenuItem = $( "#bar1 .ui-menubar-item .ui-button:first" );
+
+ function checkTab() {
+ setTimeout( start );
+ ok( !firstMenuItem.hasClass( "ui-state-focus" ), "The manually focused item should no longer have focus after TAB" );
+ //setTimeout( start );
+ }
+
+ firstMenuItem[ 0 ].focus();
+ ok( $( firstMenuItem ).hasClass( "ui-state-focus" ), "Should have focus class" );
+
+ setTimeout(function() {
+ equal( document.activeElement, firstMenuItem[0], "Focus set on first menuItem" );
+ $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.TAB } );
+ setTimeout( checkTab );
+ })
+
+} );
+
+asyncTest( "TAB order should be sane", function() {
+ expect( 3 );
+
+
+ var element = $( "#bar1" ).menubar(),
+ debugDelay = 0,
+ firstMenuItem = $( "#bar1 .ui-menubar-item .ui-button:first" );
+
+ /* Make the qunit fixture visible if we're debugging this test*/
+ if ( debugDelay ) {
+ $('').appendTo("head");
+ $( "#qunit-fixture" ).css({ right: "300px", top: "300px", left:0 });
+ }
+
+ setTimeout(function(){
+ firstMenuItem[ 0 ].focus();
+
+ function postFocus(){
+ ok( !firstMenuItem.hasClass( "ui-state-focus" ), "The manually focused item should no longer have focus after TAB" );
+ setTimeout( start );
+ };
+
+ setTimeout(function() {
+ ok( firstMenuItem.hasClass( "ui-state-focus" ), "Should have focus class" );
+ equal( document.activeElement, firstMenuItem, "Focus set on first menuItem" );
+ $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.TAB } );
+ setTimeout( postFocus );
+ });
+
+ }, debugDelay );
+
+} );
+
+
})( jQuery );
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
index 39b20d38b91..e863dbcfbd0 100644
--- a/ui/jquery.ui.menubar.js
+++ b/ui/jquery.ui.menubar.js
@@ -43,6 +43,9 @@ $.widget( "ui.menubar", {
// Keep track of open submenus
this.openSubmenus = 0;
+
+ // Scorched earth: NOTHING can be tabbed to
+ this.menuItems.find("*").slice(1).attr("tabindex", -1);
},
_initializeMenubarsBoundElement: function() {
@@ -65,6 +68,7 @@ $.widget( "ui.menubar", {
}
},
focusin: function() {
+ this._disableTabIndexOnFirstMenuItem();
clearTimeout( menubar.closeTimer );
},
focusout: function( event ) {
@@ -105,7 +109,7 @@ $.widget( "ui.menubar", {
isLastElement = ( index === ( collectionLength - 1 ) );
if ( isFirstElement ) {
- $menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
+ $menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
$menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
} else if ( isLastElement ) {
$menuItem.data( "nextMenuItem", $( this.menuItems[0]) );
@@ -171,6 +175,7 @@ $.widget( "ui.menubar", {
this._on( subMenus, {
keydown: function( event ) {
+ $(event.target).attr("tabIndex", 1);
var parentButton,
menu = $( this );
if ( menu.is(":hidden") ) {
@@ -192,6 +197,7 @@ $.widget( "ui.menubar", {
}
event.preventDefault();
+ $(event.target).attr("tabIndex", -1);
break;
case $.ui.keyCode.RIGHT:
this.next( event );
@@ -206,15 +212,8 @@ $.widget( "ui.menubar", {
},
_initializeItem: function( $anItem, menubar ) {
- //only the first item is eligible to receive the focus
var menuItemHasSubMenu = $anItem.data("parentMenuItem").data("hasSubMenu");
- // Only the first item is tab-able
- if ( menubar.items.length === 1 ) {
- $anItem.attr( "tabindex", 1 );
- } else {
- $anItem.attr( "tabIndex", -1 );
- }
this._focusable( this.items );
this._hoverable( this.items );
@@ -239,10 +238,12 @@ $.widget( "ui.menubar", {
__applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
menubar._on( $anItem, {
- focus: function(){
+ focus: function(){
+ $anItem.attr("tabIndex", 1);
$anItem.addClass("ui-state-focus");
},
focusout: function(){
+ $anItem.attr("tabIndex", -1);
$anItem.removeClass("ui-state-focus");
}
} );
@@ -287,7 +288,6 @@ $.widget( "ui.menubar", {
this._open( event, menu );
}
};
-
menubar._on( input, {
click: mouseBehaviorCallback,
focus: mouseBehaviorCallback,
@@ -312,6 +312,9 @@ $.widget( "ui.menubar", {
this.next( event );
event.preventDefault();
break;
+ case $.ui.keyCode.TAB:
+ event.stopPropagation();
+ break;
}
};
@@ -439,7 +442,7 @@ $.widget( "ui.menubar", {
}
// 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()
@@ -501,7 +504,7 @@ $.widget( "ui.menubar", {
this._submenuless_open( event, nextMenuItem );
}
} else {
- closestMenuItem.find(".ui-button").attr( "tabindex", -1 );
+ closestMenuItem.find(".ui-button");
focusableTarget.focus();
}
},
@@ -539,8 +542,12 @@ $.widget( "ui.menubar", {
menu.parent(".ui-menubar-item").removeClass("ui-state-active");
},
+ _disableTabIndexOnFirstMenuItem: function() {
+ this.items[0].attr( "tabIndex", -1 );
+ },
+
_reenableTabIndexOnFirstMenuItem: function() {
- $(this.menuItems[0]).find(".ui-widget").attr( "tabindex", 1 );
+ this.items[0].attr( "tabIndex", 1 );
}
});
From d81479b288d37a20ca194166e266e752727c555d Mon Sep 17 00:00:00 2001
From: "Steven G. Harms"
Date: Mon, 29 Apr 2013 18:05:47 -0700
Subject: [PATCH 4/4] menubar: implement jQuery UI consistency
This code base had been noted for being "unusual"
in comparison with other widgets e.g. `dialog` by
@scottgonzalez et al. Further drift was
introduced by @sgharms.
This commit reflects the work of bringing
`menubar` closer to `dialog` by means of removing
extremely unusual code invocations and by
implementing feedback from a detailed review by
@jzaefferer.
---
tests/unit/menubar/menubar_core.js | 14 +-
tests/unit/menubar/menubar_events.js | 72 +--
ui/jquery.ui.menubar.js | 646 ++++++++++++---------------
3 files changed, 305 insertions(+), 427 deletions(-)
diff --git a/tests/unit/menubar/menubar_core.js b/tests/unit/menubar/menubar_core.js
index ab3cd01b297..f9722c061da 100644
--- a/tests/unit/menubar/menubar_core.js
+++ b/tests/unit/menubar/menubar_core.js
@@ -31,6 +31,18 @@ test( "Cursor keys should move the focus", function() {
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 869a8a941ce..a032aa64766 100644
--- a/tests/unit/menubar/menubar_events.js
+++ b/tests/unit/menubar/menubar_events.js
@@ -43,75 +43,27 @@ 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 ( "_findNextFocusableTarget should find one and only one item", function() {
- expect(2);
-
- var element = $("#bar1").menubar(),
- menubarWidget = element.data( "ui-menubar" ),
- firstMenuItem = $( "#bar1 .ui-menubar-item" ).eq( 0 ),
- expectedFocusableTarget = $("#bar1 .ui-menubar-item .ui-widget").eq( 0 ),
- result = menubarWidget._findNextFocusableTarget( firstMenuItem );
-
- equal( expectedFocusableTarget[0], result[0], "_findNextFocusableTarget should return the focusable element underneath the menuItem" );
- equal( 1, result.length, "One and only one item should be returned." );
-});
-
-asyncTest( "TAB order should be sane mirroring dialog's test", function() {
- expect( 3 );
+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" );
-
- function checkTab() {
- setTimeout( start );
- ok( !firstMenuItem.hasClass( "ui-state-focus" ), "The manually focused item should no longer have focus after TAB" );
- //setTimeout( start );
- }
+ 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" ), "Should have focus class" );
-
- setTimeout(function() {
- equal( document.activeElement, firstMenuItem[0], "Focus set on first menuItem" );
- $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.TAB } );
- setTimeout( checkTab );
- })
-
-} );
-
-asyncTest( "TAB order should be sane", function() {
- expect( 3 );
-
-
- var element = $( "#bar1" ).menubar(),
- debugDelay = 0,
- firstMenuItem = $( "#bar1 .ui-menubar-item .ui-button:first" );
-
- /* Make the qunit fixture visible if we're debugging this test*/
- if ( debugDelay ) {
- $('').appendTo("head");
- $( "#qunit-fixture" ).css({ right: "300px", top: "300px", left:0 });
- }
-
- setTimeout(function(){
- firstMenuItem[ 0 ].focus();
- function postFocus(){
- ok( !firstMenuItem.hasClass( "ui-state-focus" ), "The manually focused item should no longer have focus after TAB" );
- setTimeout( start );
- };
+ 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 } );
- setTimeout(function() {
- ok( firstMenuItem.hasClass( "ui-state-focus" ), "Should have focus class" );
- equal( document.activeElement, firstMenuItem, "Focus set on first menuItem" );
- $( document.activeElement ).simulate( "keydown", { keyCode: $.ui.keyCode.TAB } );
- setTimeout( postFocus );
- });
- }, debugDelay );
+ 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." );
} );
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
index e863dbcfbd0..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,139 +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;
- // Scorched earth: NOTHING can be tabbed to
- this.menuItems.find("*").slice(1).attr("tabindex", -1);
+ this._initializeWidget();
+ this._initializeMenuItems();
+ this._initializeItems();
},
- _initializeMenubarsBoundElement: function() {
+ _initializeWidget: function() {
this.element
- .addClass("ui-menubar ui-widget-header ui-helper-clearfix")
+ .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
.attr( "role", "menubar" );
- },
-
- _initializeWidget: function() {
- var menubar = this;
-
- this._on( {
+ 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() {
- this._disableTabIndexOnFirstMenuItem();
- clearTimeout( menubar.closeTimer );
+ this.items.eq( 0 ).attr( "tabIndex", -1 );
+ clearTimeout( this.closeTimer );
},
- focusout: function( event ) {
- menubar.closeTimer = setTimeout (function() {
- menubar._close( event );
- menubar._reenableTabIndexOnFirstMenuItem();
+ 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() {
- clearTimeout( menubar.closeTimer );
+ clearTimeout( this.closeTimer );
}
- });
+ } );
},
_initializeMenuItems: function() {
- var menubar = this;
+ 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 );
-
- $menuItem.data( "name", $item.text() );
-
- 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 ) {
- $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",
@@ -175,29 +119,29 @@ $.widget( "ui.menubar", {
this._on( subMenus, {
keydown: function( event ) {
- $(event.target).attr("tabIndex", 1);
+ $(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 ( this.openSubmenus ) {
this.openSubmenus--;
- } else if ( parentButton.parent().prev().data("hasSubMenu") ) {
+ } 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);
+ $(event.target).attr( "tabIndex", -1 );
break;
case $.ui.keyCode.RIGHT:
this.next( event );
@@ -206,214 +150,241 @@ $.widget( "ui.menubar", {
}
},
focusout: function( event ) {
- $(event.target).removeClass("ui-state-focus");
+ $(event.target).removeClass( "ui-state-focus" );
}
});
+
+ this.menuItems.each(function( index, menuItem ) {
+ menubar._identifyMenuItemsNeighbors( $( menuItem ), menubar, index );
+ });
+
},
- _initializeItem: function( $anItem, menubar ) {
- 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 ) );
+
+ 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]) );
+ }
+ },
+
+ _initializeItems: function() {
+ var menubar = this;
this._focusable( this.items );
this._hoverable( this.items );
- this._applyDOMPropertiesOnItem( $anItem, menubar);
- this.__applyMouseAndKeyboardBehaviorForMenuItem ( $anItem, menubar );
+ // let only the first item receive focus
+ this.items.slice(1).attr( "tabIndex", -1 );
- if ( menuItemHasSubMenu ) {
- this.__applyMouseBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
- this.__applyKeyboardBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
+ this.items.each(function( index, item ) {
+ menubar._initializeItem( $( item ), menubar );
+ });
+ },
- $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 );
+ _initializeItem: function( anItem ) {
+ var menubar = this,
+ menuItemHasSubMenu = this._hasSubMenu( anItem.parent() );
+
+ anItem
+ .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
+ .attr( "role", "menuitem" )
+ .wrapInner( "" );
+
+ if ( menubar.options.buttons ) {
+ anItem.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
}
- },
- __applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
- menubar._on( $anItem, {
+ menubar._on( anItem, {
focus: function(){
- $anItem.attr("tabIndex", 1);
- $anItem.addClass("ui-state-focus");
+ anItem.attr( "tabIndex", 0 );
+ anItem.addClass( "ui-state-focus" );
+ event.preventDefault();
},
focusout: function(){
- $anItem.attr("tabIndex", -1);
- $anItem.removeClass("ui-state-focus");
+ anItem.attr( "tabIndex", -1 );
+ anItem.removeClass( "ui-state-focus" );
+ event.preventDefault();
}
} );
- },
-
- _applyDOMPropertiesOnItem: function( $item, menubar) {
- $item
- .addClass("ui-button ui-widget ui-button-text-only ui-menubar-link")
- .attr( "role", "menuitem" )
- .wrapInner("");
- if ( menubar.options.buttons ) {
- $item.removeClass("ui-menubar-link").addClass("ui-state-default");
- }
- },
+ if ( menuItemHasSubMenu ) {
+ this._on( anItem, {
+ click: this._mouseBehaviorForMenuItemWithSubmenu,
+ focus: this._mouseBehaviorForMenuItemWithSubmenu,
+ mouseenter: this._mouseBehaviorForMenuItemWithSubmenu
+ });
- __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;
- }
- 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);
+ 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;
- case $.ui.keyCode.TAB:
- event.stopPropagation();
- 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() {
- if ( this.active ) {
- this._close();
- } else {
- this.open = true;
- this.active = $( $anItem ).parent();
- }
- },
- mouseenter: function() {
- 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() {
+ .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;
@@ -422,60 +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");
+ 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() )
- .focus() // Establish focus on the submenu item
- .focusin(); // Move focus within the containing submenu
-
+ .removeAttr( "aria-hidden" )
+ .attr( "aria-expanded", "true" )
+ .menu( "focus", event, menu.children( ".ui-menu-item" ).first() )
+ .focus();
this.open = true;
},
- _shouldOpenNestedSubMenu: function() {
- return this.open &&
- this.active &&
- this.active.closest( this.options.items ).data("hasSubMenu") &&
- this.active.data("uiMenu") &&
- this.active.data("uiMenu").active &&
- this.active.data("uiMenu").active.has(".ui-menu").length;
- },
-
next: function( event ) {
- if ( this._shouldOpenNestedSubMenu() ) {
- // 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;
+ }
+
+ 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", "first", event );
+ this._move( "next", event );
},
previous: function( event ) {
@@ -485,69 +442,26 @@ $.widget( "ui.menubar", {
return;
}
this.openSubmenus = 0;
- this._move( "prev", "last", event );
+ this._move( "prev", event );
},
- _findNextFocusableTarget: function( menuItem ) {
- return menuItem.find(".ui-button");
- },
-
- _move: function( direction, filter, event ) {
- 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 = this._findNextFocusableTarget( nextMenuItem );
+ 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");
+ closestMenuItem.find( ".ui-button" );
focusableTarget.focus();
}
- },
-
- _submenuless_open: function( event, nextMenuItem) {
- var menuItem = $(event.target).closest(".ui-menubar-item");
-
- if ( this.active && this.active.length && menuItem.data("hasSubMenu") ) {
- this.active
- .menu("collapseAll")
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- menuItem.removeClass("ui-state-active");
- }
-
- nextMenuItem.find(".ui-button").focus();
-
- this.open = true;
- },
-
- _closeOpenMenu: function( menu ) {
- menu
- .menu("collapseAll")
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- },
-
- _deactivateMenusParentButton: function( menu ) {
- menu.parent(".ui-menubar-item").removeClass("ui-state-active");
- },
-
- _disableTabIndexOnFirstMenuItem: function() {
- this.items[0].attr( "tabIndex", -1 );
- },
-
- _reenableTabIndexOnFirstMenuItem: function() {
- this.items[0].attr( "tabIndex", 1 );
}
});