Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Menubar #829

Closed
wants to merge 33 commits into from

4 participants

@sgharms

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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
- }
- });
+ that._on( input, {
+ click: function( event ) {
+ if ( that.open ){ that._close(); }
@mikesherov Collaborator

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

@sgharms
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() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $( "#bar1" ).menubar();
@scottgonzalez Owner

Only one var statement per function.

@mikesherov Collaborator

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

@sgharms
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?
@scottgonzalez 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.

@mikesherov 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

@scottgonzalez 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...

@sgharms
sgharms added a note

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

@mikesherov 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.

@scottgonzalez 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() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $( "#bar1" ).menubar();
+ var links = $("#bar1 > li a");
@scottgonzalez 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() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $( "#bar1" ).menubar();
+ var links = $("#bar1 > li a");
+
+ var menuItemWithDropdown = links.eq(1);
@scottgonzalez 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() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $( "#bar1" ).menubar();
+ var links = $("#bar1 > li a");
+
+ var menuItemWithDropdown = links.eq(1);
+ var menuItemWithoutDropdown = links.eq(0);
+
+ menuItemWithDropdown.trigger('click', {});
@scottgonzalez Owner

double quotes, spacing, no second argument

@scottgonzalez Owner

Same for all calls to .trigger().

@sgharms
sgharms added a note

Is there a reason to prefer simulate to trigger?

@scottgonzalez Owner

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

@mikesherov Collaborator

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

@scottgonzalez 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() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $( "#bar1" ).menubar();
+ var links = $("#bar1 > li a");
+
+ var menuItemWithDropdown = links.eq(1);
+ var menuItemWithoutDropdown = links.eq(0);
+
+ menuItemWithDropdown.trigger('click', {});
+ menuItemWithoutDropdown.trigger('mouseenter', {});
+
+ equal( $(".ui-menu:visible").length, 0 );
@scottgonzalez 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
- }
- });
+ that._on( input, {
+ click: function( event ) {
+ if ( that.open ){ that._close(); }
+ },
+
+ mouseenter: function( event ) {
+ if ( that.open ){ that._close(); }
@scottgonzalez Owner
if ( this.open ) {
    this._close();
}
@scottgonzalez 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
- }
- });
+ that._on( input, {
+ click: function( event ) {
+ if ( that.open ){ that._close(); }
+ },
+
+ mouseenter: function( event ) {
@scottgonzalez Owner

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

@sgharms
sgharms added a note

Simple, I double space after :

@mikesherov 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
@jzaefferer
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.

@sgharms

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

@jzaefferer
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.

@sgharms
@sgharms

@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))
event.preventDefault();
break;
}
- });
+ }
+ });
+ if ( this.items.length > 0 ) {
@mikesherov Collaborator

Why is this conditional empty?

@sgharms
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", {
}
},
_create: function() {
- var that = this;
+ var that = this, subMenus;
@sgharms
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
@scottgonzalez 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.

@sgharms
sgharms added a note

I'll do that in another request, then.

@scottgonzalez 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
@scottgonzalez

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

@sgharms

@scottgonzalez Will amend momentarily.

@sgharms

@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
@sgharms

@jzaefferer, @scottgonzalez Can we merge this?

ui/jquery.ui.menubar.js
((6 lines not shown))
})
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
- })
- // TODO use _on
- .bind( "keydown.menubar", function( event ) {
+ });
+ this._on( subMenus, {
+
@scottgonzalez 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))
})
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
- })
- // TODO use _on
- .bind( "keydown.menubar", function( event ) {
+ });
+ this._on( subMenus, {
+
+ "keydown": function(event) {
@scottgonzalez Owner

no need for quotes here, spacing inside parens

@sgharms
sgharms added a note

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

@scottgonzalez 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))
this.items.each(function() {
var input = $(this),
// TODO menu var is only used on two places, doesn't quite justify the .each
- menu = input.next( that.options.menuElement );
+ menu = input.next( that.options.menuElement ),
+ mouseBehaviorCallback, keyboardBehaviorCallback;
@scottgonzalez 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))
- if ( event.type === "focus" && !event.originalEvent ) {
- return;
+ mouseBehaviorCallback = function( event ) {
+ // ignore triggered focus event
+ if ( event.type === "focus" && !event.originalEvent ) {
+ return;
+ }
+ event.preventDefault();
+ // TODO can we simplify or extractthis 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 ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
+ if( this.options.autoExpand ) {
@scottgonzalez Owner

spacing

@sgharms
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

@scottgonzalez 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))
- // might be a non-menu button
- if ( menu.length ) {
- // TODO use _on
- input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
- // ignore triggered focus event
- if ( event.type === "focus" && !event.originalEvent ) {
- return;
+ mouseBehaviorCallback = function( event ) {
+ // ignore triggered focus event
+ if ( event.type === "focus" && !event.originalEvent ) {
+ return;
+ }
+ event.preventDefault();
+ // TODO can we simplify or extractthis check? especially the last two expressions
@scottgonzalez 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))
- break;
- case $.ui.keyCode.LEFT:
- that.previous( event );
- event.preventDefault();
- break;
- case $.ui.keyCode.RIGHT:
- that.next( event );
- event.preventDefault();
- break;
- }
- })
- .attr( "aria-haspopup", "true" );
+ // might be a non-menu button
+ if ( menu.length ) {
+ that._on(input, {
+ "click": mouseBehaviorCallback,
@scottgonzalez 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
+ that._on(input, {
@scottgonzalez Owner

spacing

@sgharms
sgharms added a note

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

@scottgonzalez Owner

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

@jzaefferer 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
+ that._on(input, {
+ click: function(event) {
+ this._close();
@scottgonzalez 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
+ that._on(input, {
+ click: function(event) {
+ this._close();
+ },
+
+ mouseenter: function(event) {
@scottgonzalez 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", {
input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
}
} else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
+ that._on(input, {
+ click: function(event) {
+ this._close();
+ },
+
+ mouseenter: function(event) {
+ if (this.open){
@scottgonzalez Owner

spacing

@sgharms
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 )
@scottgonzalez Owner

if ( this.open ) {

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

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

ui/jquery.ui.menubar.js
((66 lines not shown))
}
+
+ this._open( event, menu );
+ }
+ };
+
+ keyboardBehaviorCallback = function( event ) {
+ switch ( event.keyCode ) {
+ case $.ui.keyCode.SPACE:
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ this._open( event, $( this ).next() );
@jzaefferer 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
@jzaefferer
Owner

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

@sgharms
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
@sgharms

@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", {
}
},
_create: function() {
- var that = this;
- this.menuItems = this.element.children( this.options.items );
- this.items = this.menuItems.children( "button, a" );
+ var that = this, subMenus;
+ this.menuItems = this.element.children( this.options.items ); // Top-level <li>s
@scottgonzalez Owner

Comments always go above the line.

@scottgonzalez 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", {
}
},
_create: function() {
- var that = this;
- this.menuItems = this.element.children( this.options.items );
- this.items = this.menuItems.children( "button, a" );
+ var that = this, subMenus;
+ this.menuItems = this.element.children( this.options.items ); // Top-level <li>s
+ this.items = this.menuItems.children( "button, a" ); // Links in those top-level <li>s
@scottgonzalez 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", {
.attr( "role", "menubar" );
this._focusable( this.items );
this._hoverable( this.items );
- this.items.siblings( this.options.menuElement )
+ subMenus = this.items.siblings( this.options.menuElement ) // sub-contained <ul>
@scottgonzalez 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))
+ click: mouseBehaviorCallback,
+ focus: mouseBehaviorCallback,
+ mouseenter: mouseBehaviorCallback,
+ keydown: keyboardBehaviorCallback
+ });
+
+ input.attr( "aria-haspopup", "true" );
+
+ // TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
+ if ( that.options.menuIcon ) {
+ input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
+ input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
+ }
+
+ if ( !menu.length ) {
+ that._off( input, "click mouseenter keydown");
@scottgonzalez 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))
+ }
+
+ if ( !menu.length ) {
+ that._off( input, "click mouseenter keydown");
+ that._hoverable( input );
+ that._on( input, {
+ click: function( event ) {
+ this._close();
+ },
+ mouseenter: function( event ) {
+ if ( this.open ) {
+ this._close();
+ }
+ },
+ keydown: function( event ) {
+ switch ( event.keyCode ) {
@scottgonzalez 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", {
var active = that.active;
that.active.blur();
that._close( event );
+ $(event.target).blur().mouseleave();
@scottgonzalez 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", {
.removeAttr( "role" )
.removeAttr( "aria-haspopup" )
// TODO unwrap?
- .children( "span.ui-button-text" ).each(function( i, e ) {
+ .children( "span.ui-button-text" ).each( function( i, e ) {
@scottgonzalez Owner

It was right before :-)

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

@scottgonzalez Once more unto the breach.

ui/jquery.ui.menubar.js
((34 lines not shown))
event.preventDefault();
break;
}
- });
- this.items.each(function() {
- var input = $(this),
+ }
+ });
+ this.items.each( function() {
@scottgonzalez Owner

no space here

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

You accidentally committed the .orig file.

Steven G. Harms added some commits
@sgharms

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

@scottgonzalez

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

@jzaefferer
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.

@sgharms

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

@jzaefferer
Owner

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

@sgharms

@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?

@sgharms

@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?

@jzaefferer
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.

@sgharms
@sgharms

@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.

Steven G. Harms added some commits
Steven G. Harms 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 meubar: formatting per JQuery style guide
* foo( "this" ) -> foo("this")
* Ensure padding space around argument calls ( foo, bar )
1af1f8c
@sgharms

@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.

@jzaefferer
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.

@sgharms

@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.

@sgharms

@jzaefferer : Addressed point 1 in commit 908fea5.

Steven G. Harms 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
@sgharms

@jzaefferer : Addressed point 2 in bb8e2e0

Steven G. Harms added some commits
Steven G. Harms menubar: relocate focus event onto *item* v. menuItem
I should have finished my coffee before I
committed this.
0efe66e
Steven G. Harms menubar: fix pixel-shifting visual error fdc200d
@sgharms

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

@jzaefferer
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

@sgharms

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

@sgharms

@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.

@jzaefferer
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.

@jzaefferer
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.

@sgharms

Looking at last update....

@sgharms

@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?

@jzaefferer
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 added some commits
Steven G. Harms Remove unused variable: seenFirstItem 09d51e8
Steven G. Harms 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 rm stray debugger b2a3814
Steven G. Harms Change selector for next .ui-menubar-link -> .ui-button bab98f2
@sgharms

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

@jzaefferer
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.

@sgharms
@sgharms

@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.

@jzaefferer
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?

@jzaefferer
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

@jzaefferer jzaefferer closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 19, 2012
  1. Adds unit test: correct sub-menu-less menu item event behavior

    Steven G. Harms authored
    Ensure open sub-menus close on sub-menu-less `click`
    or `mouseenter` events.  See 9f53e0.
  2. Convert from bind() to _on()

    Steven G. Harms authored
    Per comment from @scottgonzalez
Commits on Dec 1, 2012
  1. menubar: rm stray, empty conditional

    Steven G. Harms authored
Commits on Dec 3, 2012
  1. menubar: rm namespacing on events in _on

    Steven G. Harms authored
Commits on Dec 4, 2012
  1. menubar: decompose _create

    Steven G. Harms authored
    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.
Commits on Dec 8, 2012
  1. Revert "menubar: decompose _create"

    Steven G. Harms authored
    This reverts commit ec67b65.
    
    This is not *quite* ready for pull request.
  2. menubar: spacing / style fix

    Steven G. Harms authored
  3. menubar: add visual test

    Steven G. Harms authored
  4. menubar: restore keyboard navigation

    Steven G. Harms authored
    Per @jzaefferer, keyboard behavior was broken,
    this should be fixed now.
  5. menubar: keyboard focus / mouse interaction

    Steven G. Harms authored
    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
  6. menubar: more spaces

    Steven G. Harms authored
Commits on Dec 9, 2012
  1. menubar: visual test: add to index page

    Steven G. Harms authored
  2. menubar: kbd / mouse interaction

    Steven G. Harms authored
    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.
Commits on Dec 10, 2012
  1. menubar: spacing and formatting

    Steven G. Harms authored
  2. menubar: rm erroneously committed .orig file

    Steven G. Harms authored
  3. menubar: spacing

    Steven G. Harms authored
Commits on Jan 17, 2013
  1. menubar: massive refactor for readability

    Steven G. Harms authored
    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.
  2. meubar: formatting per JQuery style guide

    Steven G. Harms authored
    * foo( "this" ) -> foo("this")
    * Ensure padding space around argument calls ( foo, bar )
Commits on Jan 18, 2013
  1. menubar: mark active menuItem with .ui-state-focus

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

    Steven G. Harms authored
    …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.
  3. menubar: relocate focus event onto *item* v. menuItem

    Steven G. Harms authored
    I should have finished my coffee before I
    committed this.
  4. menubar: fix pixel-shifting visual error

    Steven G. Harms authored
Commits on Jan 20, 2013
  1. menubar: apply drop-down glyph only on menus w/ subMenu

    Steven G. Harms authored
Commits on Mar 14, 2013
  1. LEFT cursor in an expanded menu approximates ESCAPE

    Steven G. Harms authored
Commits on Mar 16, 2013
  1. Remove unused variable: seenFirstItem

    Steven G. Harms authored
  2. Do not remove tabIndex varaible

    Steven G. Harms authored
    When this is removed it makes the menu item with a
    submenu eligible for tabbing / shift-tabbing to /
    from.
  3. rm stray debugger

    Steven G. Harms authored
  4. Change selector for next .ui-menubar-link -> .ui-button

    Steven G. Harms authored
Commits on Mar 22, 2013
  1. Repair LEFT cursor

    Steven G. Harms authored
  2. Refactor _move by menuItems knowing their neighbors

    Steven G. Harms authored
  3. Method rename

    Steven G. Harms authored
  4. Correct submenus triggering bad focusout behavior

    Steven G. Harms authored
This page is out of date. Refresh to see the latest.
View
19 tests/unit/menubar/menubar_events.js
@@ -27,4 +27,23 @@ test( "handle click on menu item", function() {
equal( logOutput(), "click,(1,2),afterclick,(2,1),(3,3),(1,2)", "Click order not valid." );
});
+test( "hover over a menu item with no sub-menu should close open menu", function() {
+ expect( 2 );
+
+ var element = $("#bar1").menubar(),
+ links = $("#bar1 > li a"),
+ menuItemWithDropdown = links.eq(1),
+ menuItemWithoutDropdown = links.eq(0);
+
+ menuItemWithDropdown.trigger("click");
+ menuItemWithoutDropdown.trigger("mouseenter");
+
+ equal($(".ui-menu:visible").length, 0, "After triggering a sub-menu, a mouseenter on a peer menu item should close the opened sub-menu");
+
+ 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");
+});
+
})( jQuery );
View
5 tests/visual/index.html
@@ -47,6 +47,11 @@
<li><a href="menu/menu.html">General</a></li>
</ul>
+ <h2>Menubar</h2>
+ <ul>
+ <li><a href="menubar/menubar.html">General</a></li>
+ </ul>
+
<h2>Position</h2>
<ul>
<li><a href="position/position.html">General</a></li>
View
79 tests/visual/menubar/menubar.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Menu Visual Test: Default</title>
+ <link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css">
+ <script src="../../../jquery-1.8.2.js"></script>
+ <script src="../../../ui/jquery.ui.core.js"></script>
+ <script src="../../../ui/jquery.ui.widget.js"></script>
+ <script src="../../../ui/jquery.ui.position.js"></script>
+ <script src="../../../ui/jquery.ui.menu.js"></script>
+ <script src="../../../ui/jquery.ui.menubar.js"></script>
+ <style>
+ body { font-size:62.5%; }
+ .ui-menu { width: 200px; margin-bottom: 2em; }
+ .menu4 { height: 200px; overflow-y: auto; overflow-x: hidden; }
+ .address-item { border-bottom: 1px solid #999; }
+ .address-header { display: block; margin-bottom: .2em; font-weight: bold; }
+ .address-content { display: block; margin-bottom: .2em; padding-left: 10px; }
+ </style>
+ <script type="text/javascript" charset="utf-8">
+ $(document).ready( function() {
+ $(".menubar").menubar();
+ } );
+ </script>
+</head>
+<body>
+
+<h2>Default menubar</h2>
+
+<ul class="menubar">
+ <li><a href="#About">About</a></li>
+ <li>
+ <a href="#File">File</a>
+ <ul>
+ <li><a href="#Open...">Open...</a></li>
+ <li class="ui-state-disabled"><a href="#">Open recent...</a></li>
+ <li><a href="#Save">Save</a></li>
+ <li><a href="#Save%20as...">Save as...</a></li>
+ <li><a href="#Close">Close</a></li>
+ <li><a href="#Quit">Quit</a></li>
+ </ul>
+ </li>
+ <li><a href="#Stubby">Stubby</a></li>
+ <li>
+ <a href="#Edit">Edit</a>
+ <ul>
+ <li><a href="#Copy">Copy</a></li>
+ <li><a href="#Cut">Cut</a></li>
+ <li class="ui-state-disabled"><a href="#">Paste</a></li>
+ </ul>
+ </li>
+ <li>
+ <a href="#View">View</a>
+ <ul>
+ <li><a href="#Fullscreen">Fullscreen</a></li>
+ <li><a href="#Fit%20into%20view">Fit into view</a></li>
+ <li>
+ <a href="#Encoding">Encoding</a>
+ <ul>
+ <li><a href="#Auto-detect">Auto-detect</a></li>
+ <li><a href="#UTF-8">UTF-8</a></li>
+ <li>
+ <a href="#UTF-16">UTF-16</a>
+ <ul>
+ <li><a href="#Option%201">Option 1</a></li>
+ <li><a href="#Option%202">Option 2</a></li>
+ <li><a href="#Option%203">Option 3</a></li>
+ <li><a href="#Option%204">Option 4</a></li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li><a href="#Customize...">Customize...</a></li>
+ </ul>
+ </li>
+</ul>
+</body>
+</html>
View
555 ui/jquery.ui.menubar.js
@@ -30,224 +30,367 @@ $.widget( "ui.menubar", {
at: "left bottom"
}
},
+
_create: function() {
- var that = this;
+ // Top-level elements containing the submenu-triggering elem
this.menuItems = this.element.children( this.options.items );
- this.items = this.menuItems.children( "button, a" );
+ // Links or buttons in menuItems, triggers of the submenus
+ this.items = [];
- this.menuItems
- .addClass( "ui-menubar-item" )
- .attr( "role", "presentation" );
- // let only the first item receive focus
- this.items.slice(1).attr( "tabIndex", -1 );
+ this._initializeMenubarsBoundElement();
+ this._initializeWidget();
+ this._initializeMenuItems();
+ // Keep track of open submenus
+ this.openSubmenus = 0;
+ },
+
+ _initializeMenubarsBoundElement: function() {
this.element
- .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+ .addClass("ui-menubar ui-widget-header ui-helper-clearfix")
.attr( "role", "menubar" );
- this._focusable( this.items );
- this._hoverable( this.items );
- this.items.siblings( this.options.menuElement )
+ },
+
+ _initializeWidget: function() {
+ var menubar = this;
+
+ this._on( {
+ 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 );
+ $( event.target ).blur().mouseleave();
+ active.prev().focus();
+ }
+ },
+ focusin: function( event ) {
+ clearTimeout( menubar.closeTimer );
+ },
+ focusout: function( event ) {
+ menubar.closeTimer = setTimeout (function() {
+ menubar._close( event );
+ }, 150 );
+ },
+ "mouseleave .ui-menubar-item": function( event ) {
+ if ( menubar.options.autoExpand ) {
+ menubar.closeTimer = setTimeout( function() {
+ menubar._close( event );
+ }, 150 );
+ }
+ },
+ "mouseenter .ui-menubar-item": function( event ) {
+ clearTimeout( menubar.closeTimer );
+ }
+ });
+ },
+
+ _initializeMenuItems: function() {
+ var $item,
+ menubar = this;
+
+ this.menuItems
+ .addClass("ui-menubar-item")
+ .attr( "role", "presentation" );
+
+ $.each( this.menuItems, function( index, menuItem ){
+ menubar._initializeMenuItem( $( menuItem ), menubar );
+ menubar._identifyMenuItemsNeighbors( $( menuItem ), menubar, index );
+ } );
+ },
+
+ _identifyMenuItemsNeighbors: function( $menuItem, menubar, index ) {
+ var collectionLength = this.menuItems.toArray().length,
+ isFirstElement = ( index === 0 ),
+ isLastElement = ( index === ( collectionLength - 1 ) );
+
+ if ( isFirstElement ) {
+ $menuItem.data( "prevMenuItem", $( this.menuItems[collectionLength - 1]) );
+ $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
+ } else if ( isLastElement ) {
+ $menuItem.data( "nextMenuItem", $( this.menuItems[0]) );
+ $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
+ } else {
+ $menuItem.data( "nextMenuItem", $( this.menuItems[index+1]) );
+ $menuItem.data( "prevMenuItem", $( this.menuItems[index-1]) );
+ }
+ },
+
+ _initializeMenuItem: function( $menuItem, menubar ) {
+ var $item = $menuItem.children("button, a");
+
+ menubar._determineSubmenuStatus( $menuItem, menubar );
+ menubar._styleMenuItem( $menuItem, menubar );
+
+ if ( $menuItem.data("hasSubMenu") ) {
+ menubar._initializeSubMenu( $menuItem, menubar );
+ }
+
+ $item.data( "parentMenuItem", $menuItem );
+ menubar.items.push( $item );
+ menubar._initializeItem( $item, menubar );
+ },
+
+ _determineSubmenuStatus: function ( $menuItem, menubar ) {
+ var subMenus = $menuItem.children( menubar.options.menuElement ),
+ hasSubMenu = subMenus.length > 0;
+ $menuItem.data( "hasSubMenu", hasSubMenu );
+ },
+
+ _styleMenuItem: function( $menuItem, menubar ) {
+ $menuItem.css({
+ "border-width" : "1px",
+ "border-style" : "hidden"
+ });
+ },
+
+ _initializeSubMenu: function( $menuItem, menubar ){
+ var subMenus = $menuItem.children( menubar.options.menuElement );
+
+ subMenus
.menu({
position: {
within: this.options.position.within
},
select: function( event, ui ) {
- ui.item.parents( "ul.ui-menu:last" ).hide();
- that._close();
+ 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();
- that._trigger( "select", event, ui );
+ $( event.target ).prev().focus();
+ menubar._trigger( "select", event, ui );
},
- menus: that.options.menuElement
+ menus: this.options.menuElement
})
.hide()
.attr({
"aria-hidden": "true",
"aria-expanded": "false"
- })
- // TODO use _on
- .bind( "keydown.menubar", function( event ) {
- var menu = $( this );
- if ( menu.is( ":hidden" ) ) {
+ });
+
+ this._on( subMenus, {
+ keydown: function( event ) {
+ var parentButton,
+ menu = $( this );
+ if ( menu.is(":hidden") ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.LEFT:
- that.previous( event );
+ parentButton = menubar.active.prev(".ui-button");
+
+ if ( parentButton.parent().prev().data('hasSubMenu') ) {
+ menubar.active.blur();
+ menubar._open( event, parentButton.parent().prev().find(".ui-menu") );
+ } else {
+ parentButton.parent().prev().find(".ui-button").focus();
+ menubar._close( event );
+ this.open = true;
+ }
+
event.preventDefault();
break;
case $.ui.keyCode.RIGHT:
- that.next( event );
+ this.next( event );
event.preventDefault();
break;
}
- });
- this.items.each(function() {
- var input = $(this),
- // TODO menu var is only used on two places, doesn't quite justify the .each
- menu = input.next( that.options.menuElement );
-
- // might be a non-menu button
- if ( menu.length ) {
- // TODO use _on
- input.bind( "click.menubar focus.menubar mouseenter.menubar", function( event ) {
- // ignore triggered focus event
- if ( event.type === "focus" && !event.originalEvent ) {
- return;
- }
- event.preventDefault();
- // TODO can we simplify or extractthis check? especially the last two expressions
- // there's a similar active[0] == menu[0] check in _open
- if ( event.type === "click" && menu.is( ":visible" ) && that.active && that.active[0] === menu[0] ) {
- that._close();
- return;
- }
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" || that.options.autoExpand ) {
- if( that.options.autoExpand ) {
- clearTimeout( that.closeTimer );
- }
+ },
+ focusout: function( event ) {
+ event.stopImmediatePropagation();
+ }
+ });
+ },
- that._open( event, menu );
- }
- })
- // TODO use _on
- .bind( "keydown", function( event ) {
- switch ( event.keyCode ) {
- case $.ui.keyCode.SPACE:
- case $.ui.keyCode.UP:
- case $.ui.keyCode.DOWN:
- that._open( event, $( this ).next() );
- event.preventDefault();
- break;
- case $.ui.keyCode.LEFT:
- that.previous( event );
- event.preventDefault();
- break;
- case $.ui.keyCode.RIGHT:
- that.next( event );
- event.preventDefault();
- break;
- }
- })
- .attr( "aria-haspopup", "true" );
+ _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 );
+ this._applyDOMPropertiesOnItem( $anItem, menubar);
+
+ this.__applyMouseAndKeyboardBehaviorForMenuItem ( $anItem, menubar );
+
+ if ( menuItemHasSubMenu ) {
+ this.__applyMouseBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
+ this.__applyKeyboardBehaviorForSubmenuHavingMenuItem( $anItem, menubar );
+
+ $anItem.attr( "aria-haspopup", "true" );
+ if ( menubar.options.menuIcon ) {
+ $anItem.addClass("ui-state-default").append("<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>");
+ $anItem.removeClass("ui-button-text-only").addClass("ui-button-text-icon-secondary");
+ }
+ } else {
+ this.__applyMouseBehaviorForSubmenulessMenuItem( $anItem, menubar );
+ this.__applyKeyboardBehaviorForSubmenulessMenuItem( $anItem, menubar );
+ }
+ },
+
+ __applyMouseAndKeyboardBehaviorForMenuItem: function( $anItem, menubar ) {
+ menubar._on( $anItem, {
+ focus: function( event ){
+ $anItem.addClass("ui-state-focus");
+ },
+ focusout: function( event ){
+ $anItem.removeClass("ui-state-focus");
+ }
+ } );
+ },
+
+ _applyDOMPropertiesOnItem: function( $item, menubar) {
+ $item
+ .addClass("ui-button ui-widget ui-button-text-only ui-menubar-link")
+ .attr( "role", "menuitem" )
+ .wrapInner("<span class='ui-button-text'></span>");
+
+ if ( menubar.options.buttons ) {
+ $item.removeClass("ui-menubar-link").addClass("ui-state-default");
+ }
+ },
- // TODO review if these options (menuIcon and buttons) are a good choice, maybe they can be merged
- if ( that.options.menuIcon ) {
- input.addClass( "ui-state-default" ).append( "<span class='ui-button-icon-secondary ui-icon ui-icon-triangle-1-s'></span>" );
- input.removeClass( "ui-button-text-only" ).addClass( "ui-button-text-icon-secondary" );
+ __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;
}
- } else {
- // TODO use _on
- input.bind( "click.menubar mouseenter.menubar", function( event ) {
- if ( ( that.open && event.type === "mouseenter" ) || event.type === "click" ) {
- that._close();
+ 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.stashedOpenMenu = undefined;
+ }
+ if ( ( this.open && event.type === "mouseenter" ) || event.type === "click" || this.options.autoExpand ) {
+ if ( this.options.autoExpand ) {
+ clearTimeout( this.closeTimer );
+ }
+ this._open( event, menu );
+ }
+ };
- input
- .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
- .attr( "role", "menuitem" )
- .wrapInner( "<span class='ui-button-text'></span>" );
+ menubar._on( input, {
+ click: mouseBehaviorCallback,
+ focus: mouseBehaviorCallback,
+ mouseenter: mouseBehaviorCallback
+ });
+ },
- if ( that.options.buttons ) {
- input.removeClass( "ui-menubar-link" ).addClass( "ui-state-default" );
+ __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;
}
+ };
+
+ menubar._on( input, {
+ keydown: keyboardBehaviorCallback
});
- that._on( {
- keydown: function( event ) {
- if ( event.keyCode === $.ui.keyCode.ESCAPE && that.active && that.active.menu( "collapse", event ) !== true ) {
- var active = that.active;
- that.active.blur();
- that._close( event );
- active.prev().focus();
+ },
+
+ __applyMouseBehaviorForSubmenulessMenuItem: function( $anItem, menubar ) {
+ menubar._off( $anItem, "click mouseenter" );
+ menubar._hoverable( $anItem );
+ menubar._on( $anItem, {
+ click: function( event ) {
+ if ( this.active ) {
+ this._close();
+ } else {
+ this.open = true;
+ this.active = $( $anItem ).parent();
}
},
- focusin: function( event ) {
- clearTimeout( that.closeTimer );
- },
- focusout: function( event ) {
- that.closeTimer = setTimeout( function() {
- that._close( event );
- }, 150);
- },
- "mouseleave .ui-menubar-item": function( event ) {
- if ( that.options.autoExpand ) {
- that.closeTimer = setTimeout( function() {
- that._close( event );
- }, 150);
+ mouseenter: function( event ) {
+ if ( this.open ) {
+ this.stashedOpenMenu = this.active;
+ this._close();
}
- },
- "mouseenter .ui-menubar-item": function( event ) {
- clearTimeout( that.closeTimer );
}
});
-
- // Keep track of open submenus
- this.openSubmenus = 0;
+ },
+ __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();
+ }
+ };
+ menubar._on( $anItem, {
+ keydown: behavior
+ });
},
_destroy : function() {
this.menuItems
- .removeClass( "ui-menubar-item" )
- .removeAttr( "role" );
+ .removeClass("ui-menubar-item")
+ .removeAttr("role");
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" )
+ .unbind(".menubar")
+ .removeClass("ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default")
+ .removeAttr("role")
+ .removeAttr("aria-haspopup")
// TODO unwrap?
- .children( "span.ui-button-text" ).each(function( i, e ) {
+ .children("span.ui-button-text").each(function( i, e ) {
var item = $( this );
item.parent().html( item.html() );
})
.end()
- .children( ".ui-icon" ).remove();
+ .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");
},
_close: function() {
if ( !this.active || !this.active.length ) {
return;
}
- this.active
- .menu( "collapseAll" )
- .hide()
- .attr({
- "aria-hidden": "true",
- "aria-expanded": "false"
- });
- this.active
- .prev()
- .removeClass( "ui-state-active" )
- .removeAttr( "tabIndex" );
- this.active = null;
- this.open = false;
- this.openSubmenus = 0;
- },
- _open: function( event, menu ) {
- // on a single-button menubar, ignore reopening the same menu
- if ( this.active && this.active[0] === menu[0] ) {
- return;
- }
- // TODO refactor, almost the same as _close above, but don't remove tabIndex
- if ( this.active ) {
+ if ( this.active.closest( this.options.items ).data("hasSubMenu") ) {
this.active
- .menu( "collapseAll" )
+ .menu("collapseAll")
.hide()
.attr({
"aria-hidden": "true",
@@ -255,32 +398,72 @@ $.widget( "ui.menubar", {
});
this.active
.prev()
- .removeClass( "ui-state-active" );
+ .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.active = null;
+ this.open = false;
+ this.openSubmenus = 0;
+ },
+
+ _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");
+ }
}
+
// set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
- var button = menu.prev().addClass( "ui-state-active" ).attr( "tabIndex", -1 );
+ button = menuItem.addClass("ui-state-active").attr( "tabIndex", -1 );
+
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() )
+ .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();
+
this.open = true;
},
next: function( event ) {
- if ( this.open && this.active.data( "menu" ).active.has( ".ui-menu" ).length ) {
+ if ( this.open && this.active &&
+ this.active.closest( this.options.items ).data("hasSubMenu") &&
+ this.active.data("menu").active &&
+ this.active.data("menu").active.has(".ui-menu").length ) {
// Track number of open submenus and prevent moving to next menubar item
this.openSubmenus++;
return;
}
this.openSubmenus = 0;
this._move( "next", "first", event );
+
},
previous: function( event ) {
@@ -296,32 +479,48 @@ $.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 = nextMenuItem.find("a, button");
+
if ( this.open ) {
- next = this.active.closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).first().children( ".ui-menu" ).eq( 0 );
- wrapItem = this.menuItems[ filter ]().children( ".ui-menu" ).eq( 0 );
- } else {
- if ( event ) {
- next = $( event.target ).closest( ".ui-menubar-item" )[ direction + "All" ]( this.options.items ).children( ".ui-menubar-link" ).eq( 0 );
- wrapItem = this.menuItems[ filter ]().children( ".ui-menubar-link" ).eq( 0 );
+ if ( nextMenuItem.data("hasSubMenu") ) {
+ this._open( event, nextMenuItem.children(".ui-menu") );
} else {
- next = wrapItem = this.menuItems.children( "a" ).eq( 0 );
+ this._submenuless_open( event, nextMenuItem );
}
}
- if ( next.length ) {
- if ( this.open ) {
- this._open( event, next );
- } else {
- next.removeAttr( "tabIndex")[0].focus();
- }
- } else {
- if ( this.open ) {
- this._open( event, wrapItem );
- } else {
- wrapItem.removeAttr( "tabIndex")[0].focus();
+ focusableTarget.focus();
+ },
+
+ _submenuless_open: function( event, next ) {
+ var button,
+ menuItem = next.closest(".ui-menubar-item");
+
+ if ( this.active && this.active.length ) {
+ // TODO refactor, almost the same as _close above, but don't remove tabIndex
+ if ( this.active.closest( this.options.items ) ) {
+ this.active
+ .menu("collapseAll")
+ .hide()
+ .attr({
+ "aria-hidden": "true",
+ "aria-expanded": "false"
+ });
}
+ this.active.closest(this.options.items)
+ .removeClass("ui-state-active");
}
+
+ // set tabIndex -1 to have the button skipped on shift-tab when menu is open (it gets focus)
+ button = menuItem.attr( "tabIndex", -1 );
+
+ this.open = true;
+ this.active = menuItem;
}
+
});
}( jQuery ));
Something went wrong with that request. Please try again.