Skip to content

Menubar #829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0f33062
Adds unit test: correct sub-menu-less menu item event behavior
Nov 19, 2012
82d0056
Convert from bind() to _on()
Nov 19, 2012
15bd67f
menubar: remove remaining bind() calls; code standards compliance.
Nov 20, 2012
d4ca89b
menubar: rm stray, empty conditional
Dec 1, 2012
f30c853
menubar: rm namespacing on events in _on
Dec 3, 2012
ec67b65
menubar: decompose _create
Dec 4, 2012
c57319c
Revert "menubar: decompose _create"
Dec 8, 2012
36b5135
menubar: spacing / style fix
Dec 8, 2012
1a9f136
menubar: add visual test
Dec 8, 2012
ab98a00
menubar: restore keyboard navigation
Dec 8, 2012
2fe73a1
menubar: keyboard focus / mouse interaction
Dec 8, 2012
baf9882
menubar: more spaces
Dec 8, 2012
0700982
menubar: visual test: add to index page
Dec 9, 2012
2500c96
menubar: kbd / mouse interaction
Dec 9, 2012
bb87639
menubar: spacing and formatting
Dec 10, 2012
b74695a
menubar: rm erroneously committed .orig file
Dec 10, 2012
706d882
menubar: spacing
Dec 10, 2012
8237fb2
menubar: massive refactor for readability
Dec 30, 2012
1af1f8c
meubar: formatting per JQuery style guide
Jan 17, 2013
908fea5
menubar: mark active menuItem with .ui-state-focus
Jan 18, 2013
bb8e2e0
menubar: re-open submenu when returning to it after hover on menu-les…
Jan 18, 2013
0efe66e
menubar: relocate focus event onto *item* v. menuItem
Jan 18, 2013
fdc200d
menubar: fix pixel-shifting visual error
Jan 18, 2013
aafdc17
menubar: apply drop-down glyph only on menus w/ subMenu
Jan 20, 2013
1e8089b
LEFT cursor in an expanded menu approximates ESCAPE
Mar 14, 2013
09d51e8
Remove unused variable: seenFirstItem
Mar 16, 2013
a4a95cc
Do not remove tabIndex varaible
Mar 16, 2013
b2a3814
rm stray debugger
Mar 16, 2013
bab98f2
Change selector for next .ui-menubar-link -> .ui-button
Mar 16, 2013
79b06b2
Repair LEFT cursor
Mar 22, 2013
480de7f
Refactor _move by menuItems knowing their neighbors
Mar 22, 2013
56f6469
Method rename
Mar 22, 2013
100f552
Correct submenus triggering bad focusout behavior
Mar 22, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions tests/unit/menubar/menubar_events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
124 changes: 70 additions & 54 deletions ui/jquery.ui.menubar.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $.widget( "ui.menubar", {
}
},
_create: function() {
var that = this;
var that = this, subMenus;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that in another request, then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uninitialized vars at the top, on their own line:

var subMenus,
    that = this;

this.menuItems = this.element.children( this.options.items );
this.items = this.menuItems.children( "button, a" );

Expand All @@ -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 )
.menu({
position: {
within: this.options.position.within
Expand All @@ -58,89 +58,105 @@ $.widget( "ui.menubar", {
$(event.target).prev().focus();
that._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 ) {
});
this._on( subMenus, {
"keydown.menubar": function(event) {
var menu = $( this );
if ( menu.is( ":hidden" ) ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.LEFT:
that.previous( event );
this.previous( event );
event.preventDefault();
break;
case $.ui.keyCode.RIGHT:
that.next( event );
this.next( event );
event.preventDefault();
break;
}
});
}
});
if ( this.items.length > 0 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this conditional empty?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

}
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uninitialized vars at the top.


// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractthis typo

// 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 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

clearTimeout( this.closeTimer );
}

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() );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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 );
}
break;
case $.ui.keyCode.LEFT:
this.previous( event );
event.preventDefault();
break;
case $.ui.keyCode.RIGHT:
this.next( event );
event.preventDefault();
break;
}
};

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" );
// might be a non-menu button
if ( menu.length ) {
that._on(input, {
"click.menubar": mouseBehaviorCallback,
"focus.menubar": mouseBehaviorCallback,
"mouseenter.menubar": 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" );
}
} 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, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

click: function(event) {
this._close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

},

mouseenter: function(event) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

if (this.open){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ( this.open ) {

this._close();
}
}
});
}
Expand Down