Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Menubar #829

Closed
wants to merge 33 commits into from

4 participants

Steven G. Harms Jörn Zaefferer Scott González Mike Sherov
Steven G. Harms

Per comments by @scottgonzalez:

  • Implement unit test
  • Move away from bind to prefer _on
Steven G. Harms added some commits
Steven G. Harms Adds unit test: correct sub-menu-less menu item event behavior
Ensure open sub-menus close on sub-menu-less `click`
or `mouseenter` events.  See 9f53e0.
0f33062
Steven G. Harms Convert from bind() to _on()
Per comment from @scottgonzalez
82d0056
ui/jquery.ui.menubar.js
@@ -137,12 +137,15 @@ $.widget( "ui.menubar", {
137 137 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 138 }
139 139 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
144   - }
145   - });
  140 + that._on( input, {
  141 + click: function( event ) {
  142 + if ( that.open ){ that._close(); }
2
Mike Sherov Collaborator

This wasn't being checked for that.open previously, but you're checking now. Is that the correct behavior?

Steven G. Harms
sgharms added a note

@mikesherov It seems to work the same (mouseenter is fired before the click) but I can see the wisdom in making the fewest required changes. Will change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/unit/menubar/menubar_events.js
@@ -27,4 +27,25 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $( "#bar1" ).menubar();
9
Scott González Owner

Only one var statement per function.

Mike Sherov Collaborator

Yes, I wonder if this was JSHinted using grunt, as that should have caught the multiple vars.

Steven G. Harms
sgharms added a note
  1. Why is that a convention, for my edification. I personally like it but have been unable to influence others at work to adopt it, so I'm curious
  2. I did not use grunt, i just use jslint in my editor. I didn't see anything about running grunt in the README. Ought it be?
Scott González Owner

This was recently debated actually. The most important reason that I know of is to make the scope clear, but that would apply to many var statements at the top of the function as well.

As for grunt, you can run grunt or run the unit tests, both will lint the source files. It sounds like your editor isn't reading in our .jshintrc file.

Mike Sherov Collaborator
  1. I think onevar is a personal pref of the project. The rule really is "it doesn't what the style is, as long as its consistent" :) we've decided on it, and as such everyone must follow it :P. it's just that simple

  2. If grunt isn't mentioned in the contribution guide, we should add it, @scottgonzalez

Scott González Owner

@mikesherov It's cute that you think we've gotten around to adding a contributing.md :-P I sense one coming from you very soon...

Steven G. Harms
sgharms added a note

@scottgonzalez @mikesherov : is there an editor that supports directory-specific jshint rules? I'm a Vim guy myself.

Mike Sherov Collaborator

Sublime Text 2 has a ton of plugins. One is to run JSHint and look for directory level .jshintrc files. That's what @scottgonzalez and I use.

Scott González Owner

@wycats Can you point @sgharms at a vim plugin that will find and use the correct .jshintrc based on the current file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/unit/menubar/menubar_events.js
@@ -27,4 +27,25 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $( "#bar1" ).menubar();
  34 + var links = $("#bar1 > li a");
1
Scott González Owner

spacing between parens and quotes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/unit/menubar/menubar_events.js
@@ -27,4 +27,25 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $( "#bar1" ).menubar();
  34 + var links = $("#bar1 > li a");
  35 +
  36 + var menuItemWithDropdown = links.eq(1);
1
Scott González Owner

Don't align operators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/unit/menubar/menubar_events.js
@@ -27,4 +27,25 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $( "#bar1" ).menubar();
  34 + var links = $("#bar1 > li a");
  35 +
  36 + var menuItemWithDropdown = links.eq(1);
  37 + var menuItemWithoutDropdown = links.eq(0);
  38 +
  39 + menuItemWithDropdown.trigger('click', {});
6
Scott González Owner

double quotes, spacing, no second argument

Scott González Owner

Same for all calls to .trigger().

Steven G. Harms
sgharms added a note

Is there a reason to prefer simulate to trigger?

Scott González Owner

.simulate() is a higher-level concept, which is meant to come closer to real user interaction.

Mike Sherov Collaborator

@scottgonzalez, should I be using .simulate in tests instead of .click() as a general rule?

Scott González Owner

@mikesherov We should probably be using .simulate() for all events, but in practice we generally use it as needed. At some point I'd like to finally improve simulate, maybe I'll make that a priority for the 1.11 release. We can go down the road of updating all tests to only use .simulate() and then start enforcing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/unit/menubar/menubar_events.js
@@ -27,4 +27,25 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $( "#bar1" ).menubar();
  34 + var links = $("#bar1 > li a");
  35 +
  36 + var menuItemWithDropdown = links.eq(1);
  37 + var menuItemWithoutDropdown = links.eq(0);
  38 +
  39 + menuItemWithDropdown.trigger('click', {});
  40 + menuItemWithoutDropdown.trigger('mouseenter', {});
  41 +
  42 + equal( $(".ui-menu:visible").length, 0 );
1
Scott González Owner

Add a message as the third parameter, so that the failure output is clear. Same for assertion below

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,12 +137,15 @@ $.widget( "ui.menubar", {
137 137 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 138 }
139 139 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
144   - }
145   - });
  140 + that._on( input, {
  141 + click: function( event ) {
  142 + if ( that.open ){ that._close(); }
  143 + },
  144 +
  145 + mouseenter: function( event ) {
  146 + if ( that.open ){ that._close(); }
2
Scott González Owner
if ( this.open ) {
    this._close();
}
Scott González Owner

See above code, use this, not that; also spacing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,12 +137,15 @@ $.widget( "ui.menubar", {
137 137 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 138 }
139 139 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
144   - }
145   - });
  140 + that._on( input, {
  141 + click: function( event ) {
  142 + if ( that.open ){ that._close(); }
  143 + },
  144 +
  145 + mouseenter: function( event ) {
3
Scott González Owner

There are two spaces after the colon for some reason (happened above too).

Steven G. Harms
sgharms added a note

Simple, I double space after :

Mike Sherov Collaborator

@sgharms, @scottgonzalez meant you shouldn't have two spaces there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jörn Zaefferer
Owner

There are three more uses of bind which should also get replaced. Also, when you look at the file (not the diff), you can see the whitespace being messed up.

Steven G. Harms

@jzaefferer I've got those bind calls sorted out in another commit. I'll handle them globally when I use @scottgonzalez comments

Jörn Zaefferer
Owner

Okay, so you're still working on this, right? When you push an update, please make sure that you fix your whitespace settings first. It affects all files.

Steven G. Harms
Steven G. Harms

@jzaefferer I have removed the other bind calls thanks to @scottgonzalez for help earlier today. Also, this passed the grunt lint task. My understanding of the jQ UI is imperfect and my JS-fu fairly naïve, so if I've erred let me know.

ui/jquery.ui.menubar.js
((30 lines not shown))
81 81 event.preventDefault();
82 82 break;
83 83 }
84   - });
  84 + }
  85 + });
  86 + if ( this.items.length > 0 ) {
2
Mike Sherov Collaborator

Why is this conditional empty?

Steven G. Harms
sgharms added a note

@mikesherov Good catch. It's vestigial. Will rm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -31,7 +31,7 @@ $.widget( "ui.menubar", {
31 31 }
32 32 },
33 33 _create: function() {
34   - var that = this;
  34 + var that = this, subMenus;
4
Steven G. Harms
sgharms added a note

@scottgonzalez @jzaefferer When I look at this _create() method, it seems there's a lot of initializing sub-tasks going on.

  • We're initializing menuItems
  • We're instantiating a $.ui.menus underneath all of these menuItems
  • We're setting behavior on the menuItems and on the sub-menu items
  • We're defining callbacks that are applied on the sub-menu items....
  • etc.

Combine with the jQuery 'dot-chaining' syntax, the method seems to strain easy readability (in my book). This makes the contribution bar higher (IMNSHO) and makes testing / debugging harder. Maybe this isn't so for jQuery gurus such as yourselves, but I think the code would be more approachable if the long methods used intention-revealing sub-methods. I don't see this done elsewhere in the jQuery UI plugin set, but I wanted to get your thoughts on why this is or if this is just my relative unfamiliarity with plugin design.

Questions:

  1. Is it against jQuery standard to split these _create()-homed activities into intention-revealing sub-methods?
  2. Doing so might mean that you have a single var declaration in the sub-method and thus chaining becomes less attractive (given the "only one var" statement) style rule
Scott González Owner

It's fine to split up _create() if the split makes sense. I don't see how the one-var rule has any impact on chaining method calls.

Steven G. Harms
sgharms added a note

I'll do that in another request, then.

Scott González Owner

Uninitialized vars at the top, on their own line:

var subMenus,
    that = this;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Scott González

This is wrong; there should never be a namespace when using ._on().

Steven G. Harms

@scottgonzalez Will amend momentarily.

Steven G. Harms

@scottgonzalez Updated to remove namespacing of the event.

Steven G. Harms menubar: decompose _create
The `_create()` method had quite a lot going on
in it that made it hard to refactor, analyze,
or understand.  

Use sub-functions to encapsulate certain
activities.
ec67b65
Steven G. Harms

@jzaefferer, @scottgonzalez Can we merge this?

ui/jquery.ui.menubar.js
((6 lines not shown))
62 62 })
63 63 .hide()
64 64 .attr({
65 65 "aria-hidden": "true",
66 66 "aria-expanded": "false"
67   - })
68   - // TODO use _on
69   - .bind( "keydown.menubar", function( event ) {
  67 + });
  68 + this._on( subMenus, {
  69 +
1
Scott González Owner

no blank line here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((6 lines not shown))
62 62 })
63 63 .hide()
64 64 .attr({
65 65 "aria-hidden": "true",
66 66 "aria-expanded": "false"
67   - })
68   - // TODO use _on
69   - .bind( "keydown.menubar", function( event ) {
  67 + });
  68 + this._on( subMenus, {
  69 +
  70 + "keydown": function(event) {
3
Scott González Owner

no need for quotes here, spacing inside parens

Steven G. Harms
sgharms added a note

OK so here we do want function( event )? c/d

Scott González Owner

Yes, there is always a space inside parens.

NOTE: There is actually one exception which is when a string is the only parameter, the quotes count as a space.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((37 lines not shown))
85 87 this.items.each(function() {
86 88 var input = $(this),
87 89 // TODO menu var is only used on two places, doesn't quite justify the .each
88   - menu = input.next( that.options.menuElement );
  90 + menu = input.next( that.options.menuElement ),
  91 + mouseBehaviorCallback, keyboardBehaviorCallback;
1
Scott González Owner

Uninitialized vars at the top.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((49 lines not shown))
95   - if ( event.type === "focus" && !event.originalEvent ) {
96   - return;
  93 + mouseBehaviorCallback = function( event ) {
  94 + // ignore triggered focus event
  95 + if ( event.type === "focus" && !event.originalEvent ) {
  96 + return;
  97 + }
  98 + event.preventDefault();
  99 + // TODO can we simplify or extractthis check? especially the last two expressions
  100 + // there's a similar active[0] == menu[0] check in _open
  101 + if ( event.type === "click" && menu.is( ":visible" ) && this.active && this.active[0] === menu[0] ) {
  102 + this._close();
  103 + return;
  104 + }
  105 + if ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
  106 + if( this.options.autoExpand ) {
3
Scott González Owner

spacing

Steven G. Harms
sgharms added a note

We don't want spacing around this.options.autoExpand, contra https://github.com/jquery/jquery-ui/pull/829/files#r2337662?

As such:

function( event ) but if(thisIsTheCase) ? c/d

Scott González Owner

No, you're missing a space after the if.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((43 lines not shown))
89 92
90   - // might be a non-menu button
91   - if ( menu.length ) {
92   - // TODO use _on
93   - input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
94   - // ignore triggered focus event
95   - if ( event.type === "focus" && !event.originalEvent ) {
96   - return;
  93 + mouseBehaviorCallback = function( event ) {
  94 + // ignore triggered focus event
  95 + if ( event.type === "focus" && !event.originalEvent ) {
  96 + return;
  97 + }
  98 + event.preventDefault();
  99 + // TODO can we simplify or extractthis check? especially the last two expressions
1
Scott González Owner

extractthis typo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((112 lines not shown))
121   - break;
122   - case $.ui.keyCode.LEFT:
123   - that.previous( event );
124   - event.preventDefault();
125   - break;
126   - case $.ui.keyCode.RIGHT:
127   - that.next( event );
128   - event.preventDefault();
129   - break;
130   - }
131   - })
132   - .attr( "aria-haspopup", "true" );
  133 + // might be a non-menu button
  134 + if ( menu.length ) {
  135 + that._on(input, {
  136 + "click": mouseBehaviorCallback,
1
Scott González Owner

no need for quotes on these

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,10 +147,15 @@ $.widget( "ui.menubar", {
137 147 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 148 }
139 149 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
  150 + that._on(input, {
4
Scott González Owner

spacing

Steven G. Harms
sgharms added a note

So because _on is like an event thing, we put spaces after its ( ?

Scott González Owner

Always spaces. There's an exception for ({ when an object is the only parameter, but that's not the case here.

Jörn Zaefferer Owner

This also needs a keydown handler, see above. Not necessarily as part of this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,10 +147,15 @@ $.widget( "ui.menubar", {
137 147 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 148 }
139 149 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
  150 + that._on(input, {
  151 + click: function(event) {
  152 + this._close();
1
Scott González Owner

spacing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,10 +147,15 @@ $.widget( "ui.menubar", {
137 147 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 148 }
139 149 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
  150 + that._on(input, {
  151 + click: function(event) {
  152 + this._close();
  153 + },
  154 +
  155 + mouseenter: function(event) {
1
Scott González Owner

spacing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -137,10 +147,15 @@ $.widget( "ui.menubar", {
137 147 input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
138 148 }
139 149 } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
  150 + that._on(input, {
  151 + click: function(event) {
  152 + this._close();
  153 + },
  154 +
  155 + mouseenter: function(event) {
  156 + if (this.open){
3
Scott González Owner

spacing

Steven G. Harms
sgharms added a note

So the spacing is to be:

if (this.open) {
  • no space around inner if condition terms
  • do space between if-defining ()
  • do space after if-defining )
Scott González Owner

if ( this.open ) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Scott González

@jzaefferer @kborchers Can one of you review the actual changes here? I just reviewed style.

ui/jquery.ui.menubar.js
((66 lines not shown))
97 108 }
  109 +
  110 + this._open( event, menu );
  111 + }
  112 + };
  113 +
  114 + keyboardBehaviorCallback = function( event ) {
  115 + switch ( event.keyCode ) {
  116 + case $.ui.keyCode.SPACE:
  117 + case $.ui.keyCode.UP:
  118 + case $.ui.keyCode.DOWN:
  119 + this._open( event, $( this ).next() );
1
Jörn Zaefferer Owner

Using on changes the context of this, so this line breaks the keyboard handler. Test by moving the keyboard focus to an item with a submenu and try to open that using cursor-down etc.

The focus handling is broken, also in the menubar branch, maybe something you could look into later as well? Moving focus to the first submenu-free item should also bind cursor left/right to move focus inside the menubar, and the tabindex accordingly. Currently you have to activate with the mouse first, before being able to use the keyboard, and even then there's two items that can be focused, not just one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jörn Zaefferer
Owner

The space and cursor up/down handling broke. See my comments above for details.

Steven G. Harms
Steven G. Harms added some commits
Steven G. Harms Revert "menubar: decompose _create"
This reverts commit ec67b65.

This is not *quite* ready for pull request.
c57319c
Steven G. Harms menubar: spacing / style fix 36b5135
Steven G. Harms menubar: add visual test 1a9f136
Steven G. Harms menubar: restore keyboard navigation
Per @jzaefferer, keyboard behavior was broken,
this should be fixed now.
ab98a00
Steven G. Harms menubar: keyboard focus / mouse interaction
1.  Select menubar via keyboard
2.  Keyboard to an element with submenu
3.  Press down
4.  Mouse over another element with submenu
5.  Unfocus the top-level menu item
2fe73a1
Steven G. Harms menubar: more spaces baf9882
Steven G. Harms

@jzaefferer I think I have the keyboard navigation fixed. I also put some preliminary code that, I think, addresses the TODO on lines 18-19.

@scottgonzalez I've also tried to put a lot more spaces in. Let me know if this is still looking OK.

Steven G. Harms added some commits
Steven G. Harms menubar: visual test: add to index page 0700982
Steven G. Harms menubar: kbd / mouse interaction
1.  Mouse over a menu
1.  Click to expand the sub menu
1.  Mouse away
1.  Hit escape to collapse the sub menu
1.  Key left, now there are multiple highlighted
    top-level menu items.  Wrong.
2500c96
ui/jquery.ui.menubar.js
@@ -31,9 +31,9 @@ $.widget( "ui.menubar", {
31 31 }
32 32 },
33 33 _create: function() {
34   - var that = this;
35   - this.menuItems = this.element.children( this.options.items );
36   - this.items = this.menuItems.children( "button, a" );
  34 + var that = this, subMenus;
  35 + this.menuItems = this.element.children( this.options.items ); // Top-level <li>s
2
Scott González Owner

Comments always go above the line.

Scott González Owner

Actually, these comments are all technically incorrect. The markup is configurable, use menu terminology, not tag names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -31,9 +31,9 @@ $.widget( "ui.menubar", {
31 31 }
32 32 },
33 33 _create: function() {
34   - var that = this;
35   - this.menuItems = this.element.children( this.options.items );
36   - this.items = this.menuItems.children( "button, a" );
  34 + var that = this, subMenus;
  35 + this.menuItems = this.element.children( this.options.items ); // Top-level <li>s
  36 + this.items = this.menuItems.children( "button, a" ); // Links in those top-level <li>s
1
Scott González Owner

comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -46,7 +46,7 @@ $.widget( "ui.menubar", {
46 46 .attr( "role", "menubar" );
47 47 this._focusable( this.items );
48 48 this._hoverable( this.items );
49   - this.items.siblings( this.options.menuElement )
  49 + subMenus = this.items.siblings( this.options.menuElement ) // sub-contained <ul>
1
Scott González Owner

comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((143 lines not shown))
  137 + click: mouseBehaviorCallback,
  138 + focus: mouseBehaviorCallback,
  139 + mouseenter: mouseBehaviorCallback,
  140 + keydown: keyboardBehaviorCallback
  141 + });
  142 +
  143 + input.attr( "aria-haspopup", "true" );
  144 +
  145 + // TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
  146 + if ( that.options.menuIcon ) {
  147 + input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
  148 + input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
  149 + }
  150 +
  151 + if ( !menu.length ) {
  152 + that._off( input, "click mouseenter keydown");
1
Scott González Owner

space before closing paren.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
((155 lines not shown))
  149 + }
  150 +
  151 + if ( !menu.length ) {
  152 + that._off( input, "click mouseenter keydown");
  153 + that._hoverable( input );
  154 + that._on( input, {
  155 + click: function( event ) {
  156 + this._close();
  157 + },
  158 + mouseenter: function( event ) {
  159 + if ( this.open ) {
  160 + this._close();
  161 + }
  162 + },
  163 + keydown: function( event ) {
  164 + switch ( event.keyCode ) {
1
Scott González Owner

We can probably just use if here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -160,6 +189,7 @@ $.widget( "ui.menubar", {
160 189 var active = that.active;
161 190 that.active.blur();
162 191 that._close( event );
  192 + $(event.target).blur().mouseleave();
1
Scott González Owner

spacing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.menubar.js
@@ -203,7 +233,7 @@ $.widget( "ui.menubar", {
203 233 .removeAttr( "role" )
204 234 .removeAttr( "aria-haspopup" )
205 235 // TODO unwrap?
206   - .children( "span.ui-button-text" ).each(function( i, e ) {
  236 + .children( "span.ui-button-text" ).each( function( i, e ) {
1
Scott González Owner

It was right before :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Steven G. Harms

@scottgonzalez Once more unto the breach.

ui/jquery.ui.menubar.js
((34 lines not shown))
81 84 event.preventDefault();
82 85 break;
83 86 }
84   - });
85   - this.items.each(function() {
86   - var input = $(this),
  87 + }
  88 + });
  89 + this.items.each( function() {
1
Scott González Owner

no space here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Scott González

You accidentally committed the .orig file.

Steven G. Harms added some commits
Steven G. Harms

@scottgonzalez : once more unto the breach, once more.

Scott González

Looks good. I'll let @jzaefferer take it from here.

Jörn Zaefferer
Owner

@sgharms thanks for all the updates. Its working much better, but still has its quirks. Again, probably issues that existed before you started, but I hope you're interested in helping address those as well.

To help, I've just updated the Menubar wiki page, describing what the menubar keyboard and focus handling should look like:

The relevant part:

  • Cursor left/right: If no menu is open, focus just moves from one top-level item to the next. If there's a menu open, cursor right might open the active's menu submenu, while cursor left would close it. If a menu is open and the next item as a menu, moving to that item opens the next item's menu. When the next item doesn't have a menu, the previous menu is closed. When then moving to another item with a menu, that menu opens.
  • Cursor up/down: If the active item has a menu, that menu is opened. If its already open, focus is moved within the menu.
  • Escape: Closes the active menu, if there is one.
  • Tab/Shift-Tab: Moves focus to the next or previous focusable element. Must be outside the menubar. To move focus within the menubar, use the other keys as described.

Most of that is implemented, but has a bunch of bugs. Sometimes focus gets stuck on the "About" item, sometimes all items within the menubar are focusable with Tab.

Let me know if you want to work on that. We can keep this PR around a little longer then.

Steven G. Harms

@jzaefferer: Yes, I can work on these outstanding items.

Jörn Zaefferer
Owner

@sgharms would like to land this. Do you have an update? Thanks.

Steven G. Harms

@jzaefferer : I was out for the Christmas holidays, etc. I can pick this back up again now. Do you have a deadline or some such that I should be aware of?

Steven G. Harms

@jzaefferer I took a look at the bullet points you outlined in your update. All of the functionality you describe is there except for the left / right behavior. Granted this is my preliminary read on the topic, so I may not have fully grokked the code base, but as far as I can tell, this functionality is not easy to provide without adding all sorts of special cases.

Consider _move: To handle a menuItem that has no submenus the logic on line 300 must change (as it has no sub .ui-menu)

https://github.com/jquery/jquery-ui/blob/menubar/ui/jquery.ui.menubar.js#L300

Then, at 311, because next is undefined, we hit the else case and use the wrapItem which really doesn't make sense either ( as if we had wrapped around the edge of the menubar ).

With a few adjustments to this logic we can get the right behavior, but then in the _open and _close methods, collapseAll breaks, so then we need to special case those methods. Then, since those collapseAll are chained, we need to break those chains and then DRY out the calls. Yuck!

Am I missing some easy way to handle this? What I'm really missing is some sort of polymorphic quality where I could say menuItem.close() and have some faith that that menuItem whether with or without a submenu would Just Do The Right Thing.

Maybe you can suggest an approach?

Jörn Zaefferer
Owner

I've looked through the code, but right now I don't see a good aproach. Just need to make it work, then refactor.

Steven G. Harms
Steven G. Harms

@jzaefferer : I've got the items you listed in your update working locally. Let me do some more testing and cleaning up and I'll have a substantially updated file ready. I'd guess by the weekend.

sgharms added some commits
Steven G. Harms sgharms menubar: massive refactor for readability
Emergent in the menubar widget since my first
patchfix (9f53e0a) is that there is significant
behavioral difference between:

* A menuitem that *does* have a sub-menu
* A menuitem that *does not* have a sub-menu

Typically, from an OO POV, the caller should be
able to call `menuItem.close()` on either one of
these cases and not have to bear the burden of
knowing whether or not said `menuItem` has
submenus in it.

Ergo, "properly" speaking, I would want to create
two new constructors and assign the logic there.
But the, "properly" those things should be their
own class, perhaps rightly a class that should be
part of the JQuery UI widget set.

As an intermediary measure I have tried to use a
very simple procedural style (à la C) so that the
code is small, comprehensible, and doesn't rely on
using `$.each` iteration to apply or remove
behavior to elements.  I believe, by using this
behavior, the code is much more digestible.  It
as, at the very least, for me.

Beyond the coding philosophy part of the refactor,
I attempted to bring standard sane behavior for
"menu item without submenu."  This behavior may
need to be modified as, up to the point of
writing, there has been no fully definition of
these menuItems' standard of behavior.
8237fb2
Steven G. Harms sgharms meubar: formatting per JQuery style guide
* foo( "this" ) -> foo("this")
* Ensure padding space around argument calls ( foo, bar )
1af1f8c
Steven G. Harms

@jzaefferer I found some extra time and was able to put this guy together. We're pretty far afield from where I started with this in late November, but I think that the code is better for it. 8237fb2 is pretty different from what I typically see in JQuery code, but, to me, it makes much more sense.

Also, in effort to keep @scottgonzalez sane in 2013, I believe I followed the style standard propertly.

Jörn Zaefferer
Owner

Great, thanks for the update. I'll will review this properly soon.

From a quick test of the demo in latest Chrome on OSX: There's either no focus outline, no focus style or just no way to focus using the Tab key, so keyboard control is not possible. There's also layout pixel shifting when opening one of the menus (click on the first, hover on the second).

Something I didn't write down on the wiki page, but seems like it should get adressed: When clicking to open a submenu, hovering the non-menu-item closes the menu, and another hover on the closed menu should probably reopen it, instead of requiring another click. When it gets closed by a click, it should require the click to reopen. If that makes sense to you to, we can put it on the wiki.

Steven G. Harms

@jzaefferer

  1. Actually you can tab into it, there's just no visual feedback. Example, open the visual test, tab, right cursor error, down should open the File menu as expected. I agree though, we need visual feedback. I'm not the world's best CSS guy so figuring this out may take me a bit.
  2. This is do-able and mentally bear-able now that the code is in a format that doesn't make me want to be stampeded to death by a :sheep:

I'll try to take care of these, but I wanted to get the gigantic update in after so much silence on my end.

Steven G. Harms

@jzaefferer : Addressed point 1 in commit 908fea5.

Steven G. Harms sgharms menubar: re-open submenu when returning to it after hover on menu-les…
…s item

Per @jzaefferer:

> Something I didn't write down on the wiki page,
> but seems like it should get adressed: When
> clicking to open a submenu, hovering the
> non-menu-item closes the menu, and another hover
> on the closed menu should probably reopen it,
> instead of requiring another click. When it gets
> closed by a click, it should require the click to
> reopen. If that makes sense to you to, we can put
> it on the wiki.
bb8e2e0
Steven G. Harms

@jzaefferer : Addressed point 2 in bb8e2e0

Steven G. Harms

@jzaefferer I think that takes care of your concerns from your last update. I'll await your review.

Jörn Zaefferer
Owner

Yep, much better. Another minimal round of testing, I hope to have more time for this soonish:

The "About" button/link on the second menubar in the demo shouldn't have the dropdown icon, since there is no menu.

Otherwise, to keep you busy a bit further, can you take a look at the TODOs at the top here? http://wiki.jqueryui.com/w/page/38666403/Menubar

Steven G. Harms

@jzaefferer Thanks for the wiki URL, I didn't have that anywhere.

Steven G. Harms

@jzaefferer aafdc17 should take care of the issue mentioned in the demo. Simple change :cake: Strangely I didn't know that the demo directory existed. I'll see what I can knock out from the wiki.

Jörn Zaefferer
Owner

@sgharms along with the issues described on the wiki, here's a few more, all with the default demo:

Tab to the first menubar, then cursor right, down, left.

  • Expected: Same as pressing escaping, closing the menu and moving focus back to the bar
  • Actual: Menu closes, but there's no focus indicator. Cursor left/right still opens menus, but can't move focus to the non-menu item.

Tab to the first menubar, then cursor right twice, then shift-tab.

  • Expected: Focus moves to previous tabbable.
  • Actual: Focus moves from "Edit" to "File". Looks like bad tab-index handling.

Tab to the second menubar, press cursor left/right.

  • Expected: Moving focus to other items in the second menubar.
  • Actual: Nothing, can't move with cursor keys. Works fine on the other two, so likely related to the menuIcon option.

There's more, but I suspect they are related to those above and should go away when those are fixed.

Jörn Zaefferer
Owner

@sgharms would be great if you could take another look at the issues from my previous comment. And afterwards we should either just land this PR (yay) or create a new one (okay), since this one has gotten pretty long.

Steven G. Harms

Looking at last update....

Steven G. Harms

@jzaefferer As I read your bullet #2, I'm confused between what it says and what is said about tab/shift-tab in the wiki:

Wiki says:

Tab/Shift-Tab: Moves focus to the next or previous focusable element. Must be outside the menubar. To move focus within the menubar, use the other keys as described.

That seems to imply to me that it should go to something else on the page entirely. In the case of the demo page, it should loop around to the third menubar demo at the bottom. Tab should be able to move between menubars and other inputs but should not be used to move between ui-buttons. Is that right?

Assuming your "expected" scenario is the proper behavior to tab to the last menubar of the demo page?

Jörn Zaefferer
Owner

Tab/Shift-Tab: Moves focus to the next or previous focusable element. Must be outside the menubar. To move focus within the menubar, use the other keys as described.

That's correct, but I don't see that contradicting what I wrote above.

it should loop around to the third menubar demo at the bottom.

Not entirely sure if that's correct, but it certainly shouldn't move focus within the menubar, as it does right now.

Tab should be able to move between menubars and other inputs, but should not be used to move between ui-buttons

Correct.

Steven G. Harms

@jzaefferer I think this takes care of those three bullets you mentioned.

Jörn Zaefferer
Owner

Okay, thanks for the update. Though it doesn't look like we're done here. Would be great if you could do more manual keyboard testing as well, as these seem pretty obvious:

Tab to first menubar, press cursor right, right, down, left. Should now open the File submenu, instead just closes the Edit submenu. Now press Shift-Tab. Should move focus to something else, instead moves focus to About button inside the menubar.

Tab to second menubar. Use cursor left/right. Note that focus doesn't wrap around like it does on the other two menus.

Something new I noticed: On touchscreens without hover events, the second menubar is broken. The click event should still open submenus when there's no hover event. I'm fine with ignoring that for this PR and addressing it later though.

Steven G. Harms
Steven G. Harms

@jzaefferer Fixed the egregious problems from the previous commitset.

I still have some funniness around TAB behavior but the menus should be visitable, wrap properly, extend properly, and save extended state properly.

Jörn Zaefferer
Owner

That looks much better. There's still the issue where shift-tab moves focus to the About item instead of outside the menubar ("funniness around TAB behaviour"?). Should we address that in a new PR?

Jörn Zaefferer
Owner

I've landed this, squashed into one commit, in 86417ef and merged master in 2253df8 (updating the menubar files to use jQuery 1.9.1). I've made a note of the open issues mentioned here on the wiki page: http://wiki.jqueryui.com/w/page/38666403/Menubar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 33 unique commits by 2 authors.

Nov 19, 2012
Steven G. Harms Adds unit test: correct sub-menu-less menu item event behavior
Ensure open sub-menus close on sub-menu-less `click`
or `mouseenter` events.  See 9f53e0.
0f33062
Steven G. Harms Convert from bind() to _on()
Per comment from @scottgonzalez
82d0056
Dec 01, 2012
Steven G. Harms menubar: remove remaining bind() calls; code standards compliance. 15bd67f
Steven G. Harms menubar: rm stray, empty conditional d4ca89b
Dec 03, 2012
Steven G. Harms menubar: rm namespacing on events in _on f30c853
Dec 04, 2012
Steven G. Harms menubar: decompose _create
The `_create()` method had quite a lot going on
in it that made it hard to refactor, analyze,
or understand.  

Use sub-functions to encapsulate certain
activities.
ec67b65
Dec 08, 2012
Steven G. Harms Revert "menubar: decompose _create"
This reverts commit ec67b65.

This is not *quite* ready for pull request.
c57319c
Steven G. Harms menubar: spacing / style fix 36b5135
Steven G. Harms menubar: add visual test 1a9f136
Steven G. Harms menubar: restore keyboard navigation
Per @jzaefferer, keyboard behavior was broken,
this should be fixed now.
ab98a00
Steven G. Harms menubar: keyboard focus / mouse interaction
1.  Select menubar via keyboard
2.  Keyboard to an element with submenu
3.  Press down
4.  Mouse over another element with submenu
5.  Unfocus the top-level menu item
2fe73a1
Steven G. Harms menubar: more spaces baf9882
Dec 09, 2012
Steven G. Harms menubar: visual test: add to index page 0700982
Steven G. Harms menubar: kbd / mouse interaction
1.  Mouse over a menu
1.  Click to expand the sub menu
1.  Mouse away
1.  Hit escape to collapse the sub menu
1.  Key left, now there are multiple highlighted
    top-level menu items.  Wrong.
2500c96
Dec 10, 2012
Steven G. Harms menubar: spacing and formatting bb87639
Steven G. Harms menubar: rm erroneously committed .orig file b74695a
Steven G. Harms menubar: spacing 706d882
Jan 17, 2013
Steven G. Harms sgharms menubar: massive refactor for readability
Emergent in the menubar widget since my first
patchfix (9f53e0a) is that there is significant
behavioral difference between:

* A menuitem that *does* have a sub-menu
* A menuitem that *does not* have a sub-menu

Typically, from an OO POV, the caller should be
able to call `menuItem.close()` on either one of
these cases and not have to bear the burden of
knowing whether or not said `menuItem` has
submenus in it.

Ergo, "properly" speaking, I would want to create
two new constructors and assign the logic there.
But the, "properly" those things should be their
own class, perhaps rightly a class that should be
part of the JQuery UI widget set.

As an intermediary measure I have tried to use a
very simple procedural style (à la C) so that the
code is small, comprehensible, and doesn't rely on
using `$.each` iteration to apply or remove
behavior to elements.  I believe, by using this
behavior, the code is much more digestible.  It
as, at the very least, for me.

Beyond the coding philosophy part of the refactor,
I attempted to bring standard sane behavior for
"menu item without submenu."  This behavior may
need to be modified as, up to the point of
writing, there has been no fully definition of
these menuItems' standard of behavior.
8237fb2
Steven G. Harms sgharms meubar: formatting per JQuery style guide
* foo( "this" ) -> foo("this")
* Ensure padding space around argument calls ( foo, bar )
1af1f8c
Jan 18, 2013
Steven G. Harms sgharms menubar: mark active menuItem with .ui-state-focus 908fea5
Steven G. Harms sgharms menubar: re-open submenu when returning to it after hover on menu-les…
…s item

Per @jzaefferer:

> Something I didn't write down on the wiki page,
> but seems like it should get adressed: When
> clicking to open a submenu, hovering the
> non-menu-item closes the menu, and another hover
> on the closed menu should probably reopen it,
> instead of requiring another click. When it gets
> closed by a click, it should require the click to
> reopen. If that makes sense to you to, we can put
> it on the wiki.
bb8e2e0
Steven G. Harms sgharms menubar: relocate focus event onto *item* v. menuItem
I should have finished my coffee before I
committed this.
0efe66e
Steven G. Harms sgharms menubar: fix pixel-shifting visual error fdc200d
Jan 20, 2013
Steven G. Harms sgharms menubar: apply drop-down glyph only on menus w/ subMenu aafdc17
Mar 14, 2013
Steven G. Harms sgharms LEFT cursor in an expanded menu approximates ESCAPE 1e8089b
Mar 16, 2013
Steven G. Harms sgharms Remove unused variable: seenFirstItem 09d51e8
Steven G. Harms sgharms Do not remove tabIndex varaible
When this is removed it makes the menu item with a
submenu eligible for tabbing / shift-tabbing to /
from.
a4a95cc
Steven G. Harms sgharms rm stray debugger b2a3814
Steven G. Harms sgharms Change selector for next .ui-menubar-link -> .ui-button bab98f2
Mar 22, 2013
Steven G. Harms sgharms Repair LEFT cursor 79b06b2
Steven G. Harms sgharms Refactor _move by menuItems knowing their neighbors 480de7f
Steven G. Harms sgharms Method rename 56f6469
Steven G. Harms sgharms Correct submenus triggering bad focusout behavior 100f552
This page is out of date. Refresh to see the latest.
19 tests/unit/menubar/menubar_events.js
@@ -27,4 +27,23 @@ test( "handle click on menu item", function() {
27 27 equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
28 28 });
29 29
  30 +test( "hover over a menu item with no sub-menu should close open menu", function() {
  31 + expect( 2 );
  32 +
  33 + var element = $("#bar1").menubar(),
  34 + links = $("#bar1 > li a"),
  35 + menuItemWithDropdown = links.eq(1),
  36 + menuItemWithoutDropdown = links.eq(0);
  37 +
  38 + menuItemWithDropdown.trigger("click");
  39 + menuItemWithoutDropdown.trigger("mouseenter");
  40 +
  41 + equal($(".ui-menu:visible").length, 0, "After triggering a sub-menu, a mouseenter on a peer menu item should close the opened sub-menu");
  42 +
  43 + menuItemWithDropdown.trigger("click");
  44 + menuItemWithoutDropdown.trigger("click");
  45 +
  46 + equal($(".ui-menu:visible").length, 0, "After triggering a sub-menu, a click on a peer menu item should close the opened sub-menu");
  47 +});
  48 +
30 49 })( jQuery );
5 tests/visual/index.html
@@ -47,6 +47,11 @@
47 47 <li><a href="menu/menu.html">General</a></li>
48 48 </ul>
49 49
  50 + <h2>Menubar</h2>
  51 + <ul>
  52 + <li><a href="menubar/menubar.html">General</a></li>
  53 + </ul>
  54 +
50 55 <h2>Position</h2>
51 56 <ul>
52 57 <li><a href="position/position.html">General</a></li>
79 tests/visual/menubar/menubar.html
... ... @@ -0,0 +1,79 @@
  1 +<!doctype html>
  2 +<html>
  3 +<head>
  4 + <meta charset="utf-8">
  5 + <title>Menu Visual Test: Default</title>
  6 + <link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css">
  7 + <script src="../../../jquery-1.8.2.js"></script>
  8 + <script src="../../../ui/jquery.ui.core.js"></script>
  9 + <script src="../../../ui/jquery.ui.widget.js"></script>
  10 + <script src="../../../ui/jquery.ui.position.js"></script>
  11 + <script src="../../../ui/jquery.ui.menu.js"></script>
  12 + <script src="../../../ui/jquery.ui.menubar.js"></script>
  13 + <style>
  14 + body { font-size:62.5%; }
  15 + .ui-menu { width: 200px; margin-bottom: 2em; }
  16 + .menu4 { height: 200px; overflow-y: auto; overflow-x: hidden; }
  17 + .address-item { border-bottom: 1px solid #999; }
  18 + .address-header { display: block; margin-bottom: .2em; font-weight: bold; }
  19 + .address-content { display: block; margin-bottom: .2em; padding-left: 10px; }
  20 + </style>
  21 + <script type="text/javascript" charset="utf-8">
  22 + $(document).ready( function() {
  23 + $(".menubar").menubar();
  24 + } );
  25 + </script>
  26 +</head>
  27 +<body>
  28 +
  29 +<h2>Default menubar</h2>
  30 +
  31 +<ul class="menubar">
  32 + <li><a href="#About">About</a></li>
  33 + <li>
  34 + <a href="#File">File</a>
  35 + <ul>
  36 + <li><a href="#Open...">Open...</a></li>
  37 + <li class="ui-state-disabled"><a href="#">Open recent...</a></li>
  38 + <li><a href="#Save">Save</a></li>
  39 + <li><a href="#Save%20as...">Save as...</a></li>
  40 + <li><a href="#Close">Close</a></li>
  41 + <li><a href="#Quit">Quit</a></li>
  42 + </ul>
  43 + </li>
  44 + <li><a href="#Stubby">Stubby</a></li>
  45 + <li>
  46 + <a href="#Edit">Edit</a>
  47 + <ul>
  48 + <li><a href="#Copy">Copy</a></li>
  49 + <li><a href="#Cut">Cut</a></li>
  50 + <li class="ui-state-disabled"><a href="#">Paste</a></li>
  51 + </ul>
  52 + </li>
  53 + <li>
  54 + <a href="#View">View</a>
  55 + <ul>
  56 + <li><a href="#Fullscreen">Fullscreen</a></li>
  57 + <li><a href="#Fit%20into%20view">Fit into view</a></li>
  58 + <li>
  59 + <a href="#Encoding">Encoding</a>
  60 + <ul>
  61 + <li><a href="#Auto-detect">Auto-detect</a></li>
  62 + <li><a href="#UTF-8">UTF-8</a></li>
  63 + <li>
  64 + <a href="#UTF-16">UTF-16</a>
  65 + <ul>
  66 + <li><a href="#Option%201">Option 1</a></li>
  67 + <li><a href="#Option%202">Option 2</a></li>
  68 + <li><a href="#Option%203">Option 3</a></li>
  69 + <li><a href="#Option%204">Option 4</a></li>
  70 + </ul>
  71 + </li>
  72 + </ul>
  73 + </li>
  74 + <li><a href="#Customize...">Customize...</a></li>
  75 + </ul>
  76 + </li>
  77 +</ul>
  78 +</body>
  79 +</html>
555 ui/jquery.ui.menubar.js
@@ -30,224 +30,367 @@ $.widget( "ui.menubar", {
30 30 at: "left bottom"
31 31 }
32 32 },
  33 +
33 34 _create: function() {
34   - var that = this;
  35 + // Top-level elements containing the submenu-triggering elem
35 36 this.menuItems = this.element.children( this.options.items );
36   - this.items = this.menuItems.children( "button, a" );
  37 + // Links or buttons in menuItems, triggers of the submenus
  38 + this.items = [];
37 39
38   - this.menuItems
39   - .addClass( "ui-menubar-item" )
40   - .attr( "role", "presentation" );
41   - // let only the first item receive focus
42   - this.items.slice(1).attr( "tabIndex", -1 );
  40 + this._initializeMenubarsBoundElement();
  41 + this._initializeWidget();
  42 + this._initializeMenuItems();
43 43
  44 + // Keep track of open submenus
  45 + this.openSubmenus = 0;
  46 + },
  47 +
  48 + _initializeMenubarsBoundElement: function() {
44 49 this.element
45   - .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
  50 + .addClass("ui-menubar ui-widget-header ui-helper-clearfix")
46 51 .attr( "role", "menubar" );
47   - this._focusable( this.items );
48   - this._hoverable( this.items );
49   - this.items.siblings( this.options.menuElement )
  52 + },
  53 +
  54 + _initializeWidget: function() {
  55 + var menubar = this;
  56 +
  57 + this._on( {
  58 + keydown: function( event ) {
  59 + if ( event.keyCode === $.ui.keyCode.ESCAPE && menubar.active && menubar.active.menu( "collapse", event ) !== true ) {
  60 + var active = menubar.active;
  61 + menubar.active.blur();
  62 + menubar._close( event );
  63 + $( event.target ).blur().mouseleave();
  64 + active.prev().focus();
  65 + }
  66 + },
  67 + focusin: function( event ) {
  68 + clearTimeout( menubar.closeTimer );
  69 + },
  70 + focusout: function( event ) {
  71 + menubar.closeTimer = setTimeout (function() {
  72 + menubar._close( event );
  73 + }, 150 );
  74 + },
  75 + "mouseleave .ui-menubar-item": function( event ) {
  76 + if ( menubar.options.autoExpand ) {
  77 + menubar.closeTimer = setTimeout( function() {
  78 + menubar._close( event );
  79 + }, 150 );
  80 + }
  81 + },
  82 + "mouseenter .ui-menubar-item": function( event ) {
  83 + clearTimeout( menubar.closeTimer );
  84 + }
  85 + });
  86 + },
  87 +
  88 + _initializeMenuItems: function() {
  89 + var $item,
  90 + menubar = this;
  91 +
  92 + this.menuItems
  93 + .addClass("ui-menubar-item")
  94 + .attr( "role", "presentation" );
  95 +
  96 + $.each( this.menuItems, function( index, menuItem ){
  97 + menubar._initializeMenuItem( $( menuItem ), menubar );
  98 + menubar._identifyMenuItemsNeighbors( $( menuItem ), menubar, index );
  99 + } );
  100 + },
  101 +
  102 + _identifyMenuItemsNeighbors: function( $menuItem, menubar, index ) {
  103 + var collectionLength = this.menuItems.toArray().length,
  104 + isFirstElement = ( index === 0 ),
  105 + isLastElement = ( index === ( collectionLength - 1 ) );
  106 +
  107 + if ( isFirstElement ) {
  108 + $menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
  109 + $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
  110 + } else if ( isLastElement ) {
  111 + $menuItem.data( "nextMenuItem", $( this.menuItems[0]) );
  112 + $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
  113 + } else {
  114 + $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
  115 + $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
  116 + }
  117 + },
  118 +
  119 + _initializeMenuItem: function( $menuItem, menubar ) {
  120 + var $item = $menuItem.children("button, a");
  121 +
  122 + menubar._determineSubmenuStatus( $menuItem, menubar );
  123 + menubar._styleMenuItem( $menuItem, menubar );
  124 +
  125 + if ( $menuItem.data("hasSubMenu") ) {
  126 + menubar._initializeSubMenu( $menuItem, menubar );
  127 + }
  128 +
  129 + $item.data( "parentMenuItem", $menuItem );
  130 + menubar.items.push( $item );
  131 + menubar._initializeItem( $item, menubar );
  132 + },
  133 +
  134 + _determineSubmenuStatus: function ( $menuItem, menubar ) {
  135 + var subMenus = $menuItem.children( menubar.options.menuElement ),
  136 + hasSubMenu = subMenus.length > 0;
  137 + $menuItem.data( "hasSubMenu", hasSubMenu );
  138 + },
  139 +
  140 + _styleMenuItem: function( $menuItem, menubar ) {
  141 + $menuItem.css({
  142 + "border-width" : "1px",
  143 + "border-style" : "hidden"
  144 + });
  145 + },
  146 +
  147 + _initializeSubMenu: function( $menuItem, menubar ){
  148 + var subMenus = $menuItem.children( menubar.options.menuElement );
  149 +
  150 + subMenus
50 151 .menu({
51 152 position: {
52 153 within: this.options.position.within
53 154 },
54 155 select: function( event, ui ) {
55   - ui.item.parents( "ul.ui-menu:last" ).hide();
56   - that._close();
  156 + ui.item.parents("ul.ui-menu:last").hide();
  157 + menubar._close();
57 158 // TODO what is this targetting? there's probably a better way to access it
58   - $(event.target).prev().focus();
59   - that._trigger( "select", event, ui );
  159 + $( event.target ).prev().focus();
  160 + menubar._trigger( "select", event, ui );
60 161 },
61   - menus: that.options.menuElement
  162 + menus: this.options.menuElement
62 163 })
63 164 .hide()
64 165 .attr({
65 166 "aria-hidden": "true",
66 167 "aria-expanded": "false"
67   - })
68   - // TODO use _on
69   - .bind( "keydown.menubar", function( event ) {
70   - var menu = $( this );
71   - if ( menu.is( ":hidden" ) ) {
  168 + });
  169 +
  170 + this._on( subMenus, {
  171 + keydown: function( event ) {
  172 + var parentButton,
  173 + menu = $( this );
  174 + if ( menu.is(":hidden") ) {
72 175 return;
73 176 }
74 177 switch ( event.keyCode ) {
75 178 case $.ui.keyCode.LEFT:
76   - that.previous( event );
  179 + parentButton = menubar.active.prev(".ui-button");
  180 +
  181 + if ( parentButton.parent().prev().data('hasSubMenu') ) {
  182 + menubar.active.blur();
  183 + menubar._open( event, parentButton.parent().prev().find(".ui-menu") );
  184 + } else {
  185 + parentButton.parent().prev().find(".ui-button").focus();
  186 + menubar._close( event );
  187 + this.open = true;
  188 + }
  189 +
77 190 event.preventDefault();
78 191 break;
79 192 case $.ui.keyCode.RIGHT:
80   - that.next( event );
  193 + this.next( event );
81 194 event.preventDefault();
82 195 break;
83 196 }
84   - });
85   - this.items.each(function() {
86   - var input = $(this),
87   - // TODO menu var is only used on two places, doesn't quite justify the .each
88   - menu = input.next( that.options.menuElement );
89   -
90   - // might be a non-menu button
91   - if ( menu.length ) {
92   - // TODO use _on
93   - input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
94   - // ignore triggered focus event
95   - if ( event.type === "focus" && !event.originalEvent ) {
96   - return;
97   - }
98   - event.preventDefault();
99   - // TODO can we simplify or extractthis check? especially the last two expressions
100   - // there's a similar active[0] == menu[0] check in _open
101   - if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
102   - that._close();
103   - return;
104   - }
105   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
106   - if( that.options.autoExpand ) {
107   - clearTimeout( that.closeTimer );
108   - }
  197 + },
  198 + focusout: function( event ) {
  199 + event.stopImmediatePropagation();
  200 + }
  201 + });
  202 + },
109 203
110   - that._open( event, menu );
111   - }
112   - })
113   - // TODO use _on
114   - .bind( "keydown", function( event ) {
115   - switch ( event.keyCode ) {
116   - case $.ui.keyCode.SPACE:
117   - case $.ui.keyCode.UP:
118   - case $.ui.keyCode.DOWN:
119   - that._open( event, $( this ).next() );
120   - event.preventDefault();
121   - break;
122   - case $.ui.keyCode.LEFT:
123   - that.previous( event );
124   - event.preventDefault();
125   - break;
126   - case $.ui.keyCode.RIGHT:
127   - that.next( event );
128   - event.preventDefault();
129   - break;
130   - }
131   - })
132   - .attr( "aria-haspopup", "true" );
  204 + _initializeItem: function( $anItem, menubar ) {
  205 + //only the first item is eligible to receive the focus
  206 + var menuItemHasSubMenu = $anItem.data("parentMenuItem").data("hasSubMenu");
  207 +
  208 + // Only the first item is tab-able
  209 + if ( menubar.items.length === 1 ) {
  210 + $anItem.attr( "tabindex", 1 );
  211 + } else {
  212 + $anItem.attr( "tabIndex", -1 );
  213 + }
  214 +
  215 + this._focusable( this.items );
  216 + this._hoverable( this.items );
  217 + this._applyDOMPropertiesOnItem( $anItem, menubar);
  218 +
  219 + this.__applyMouseAndKeyboardBehaviorForMenuItem ( $anItem, menubar );
  220 +
  221 + if ( menuItemHasSubMenu ) {
  222 + this.__applyMouseBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
  223 + this.__applyKeyboardBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
  224 +
  225 + $anItem.attr( "aria-haspopup", "true" );
  226 + if ( menubar.options.menuIcon ) {
  227 + $anItem.addClass("ui-state-default").append("<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>");
  228 + $anItem.removeClass("ui-button-text-only").addClass("ui-button-text-icon-secondary");
  229 + }
  230 + } else {
  231 + this.__applyMouseBehaviorForSubmenulessMenuItem( $anItem, menubar );
  232 + this.__applyKeyboardBehaviorForSubmenulessMenuItem( $anItem, menubar );
  233 + }
  234 + },
  235 +
  236 + __applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
  237 + menubar._on( $anItem, {
  238 + focus: function( event ){
  239 + $anItem.addClass("ui-state-focus");
  240 + },
  241 + focusout: function( event ){
  242 + $anItem.removeClass("ui-state-focus");
  243 + }
  244 + } );
  245 + },
  246 +
  247 + _applyDOMPropertiesOnItem: function( $item, menubar) {
  248 + $item
  249 + .addClass("ui-button ui-widget ui-button-text-only ui-menubar-link")
  250 + .attr( "role", "menuitem" )
  251 + .wrapInner("<span class='ui-button-text'></span>");
  252 +
  253 + if ( menubar.options.buttons ) {
  254 + $item.removeClass("ui-menubar-link").addClass("ui-state-default");
  255 + }
  256 + },
133 257
134   - // TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
135   - if ( that.options.menuIcon ) {
136   - input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
137   - input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
  258 + __applyMouseBehaviorForSubmenuHavingMenuItem: function ( input, menubar ) {
  259 + var menu = input.next( menubar.options.menuElement ),
  260 + mouseBehaviorCallback = function( event ) {
  261 + // ignore triggered focus event
  262 + if ( event.type === "focus" && !event.originalEvent ) {
  263 + return;
138 264 }
139   - } else {
140   - // TODO use _on
141   - input.bind( "click.menubar mouseenter.menubar", function( event ) {
142   - if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
143   - that._close();
  265 + event.preventDefault();
  266 + // TODO can we simplify or extract this check? especially the last two expressions
  267 + // there's a similar active[0] == menu[0] check in _open
  268 + if ( event.type === "click" && menu.is(":visible") && this.active && this.active[0] === menu[0] ) {
  269 + this._close();
  270 + return;
  271 + }
  272 + if ( event.type === "mouseenter" ) {
  273 + this.element.find(":focus").focusout();
  274 + if ( this.stashedOpenMenu ) {
  275 + this._open( event, menu);
144 276 }
145   - });
146   - }
  277 + this.stashedOpenMenu = undefined;
  278 + }
  279 + if ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
  280 + if ( this.options.autoExpand ) {
  281 + clearTimeout( this.closeTimer );
  282 + }
  283 + this._open( event, menu );
  284 + }
  285 + };
147 286
148   - input
149   - .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
150   - .attr( "role", "menuitem" )
151   - .wrapInner( "<span class='ui-button-text'></span>" );
  287 + menubar._on( input, {
  288 + click: mouseBehaviorCallback,
  289 + focus: mouseBehaviorCallback,
  290 + mouseenter: mouseBehaviorCallback
  291 + });
  292 + },
152 293
153   - if ( that.options.buttons ) {
154   - input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
  294 + __applyKeyboardBehaviorForSubmenuHavingMenuItem: function( input, menubar ) {
  295 + var keyboardBehaviorCallback = function( event ) {
  296 + switch ( event.keyCode ) {
  297 + case $.ui.keyCode.SPACE:
  298 + case $.ui.keyCode.UP:
  299 + case $.ui.keyCode.DOWN:
  300 + menubar._open( event, $( event.target ).next() );
  301 + event.preventDefault();
  302 + break;
  303 + case $.ui.keyCode.LEFT:
  304 + this.previous( event );
  305 + event.preventDefault();
  306 + break;
  307 + case $.ui.keyCode.RIGHT:
  308 + this.next( event );
  309 + event.preventDefault();
  310 + break;
155 311 }
  312 + };
  313 +
  314 + menubar._on( input, {
  315 + keydown: keyboardBehaviorCallback
156 316 });
157   - that._on( {
158   - keydown: function( event ) {
159   - if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
160   - var active = that.active;
161   - that.active.blur();
162   - that._close( event );
163   - active.prev().focus();
  317 + },
  318 +
  319 + __applyMouseBehaviorForSubmenulessMenuItem: function( $anItem, menubar ) {
  320 + menubar._off( $anItem, "click mouseenter" );
  321 + menubar._hoverable( $anItem );
  322 + menubar._on( $anItem, {
  323 + click: function( event ) {
  324 + if ( this.active ) {
  325 + this._close();
  326 + } else {
  327 + this.open = true;
  328 + this.active = $( $anItem ).parent();
164 329 }
165 330 },
166   - focusin: function( event ) {
167   - clearTimeout( that.closeTimer );
168   - },
169   - focusout: function( event ) {
170   - that.closeTimer = setTimeout( function() {
171   - that._close( event );
172   - }, 150);
173   - },
174   - "mouseleave .ui-menubar-item": function( event ) {
175   - if ( that.options.autoExpand ) {
176   - that.closeTimer = setTimeout( function() {
177   - that._close( event );
178   - }, 150);
  331 + mouseenter: function( event ) {
  332 + if ( this.open ) {
  333 + this.stashedOpenMenu = this.active;
  334 + this._close();
179 335 }
180   - },
181   - "mouseenter .ui-menubar-item": function( event ) {
182   - clearTimeout( that.closeTimer );
183 336 }
184 337 });
185   -
186   - // Keep track of open submenus
187   - this.openSubmenus = 0;
  338 + },
  339 + __applyKeyboardBehaviorForSubmenulessMenuItem: function( $anItem, menubar ) {
  340 + var behavior = function( event ) {
  341 + if ( event.keyCode === $.ui.keyCode.LEFT ) {
  342 + this.previous( event );
  343 + event.preventDefault();
  344 + } else if ( event.keyCode === $.ui.keyCode.RIGHT ) {
  345 + this.next( event );