Menu: Add classes option

Ref #7053
Ref gh-1411
This commit is contained in:
Alexander Schmitz 2014-12-03 11:24:44 -05:00
parent f58277a521
commit 2ebef69efe
3 changed files with 88 additions and 69 deletions

View File

@ -9,6 +9,7 @@
<script src="../../../external/qunit/qunit.js"></script> <script src="../../../external/qunit/qunit.js"></script>
<script src="../../../external/jquery-simulate/jquery.simulate.js"></script> <script src="../../../external/jquery-simulate/jquery.simulate.js"></script>
<script src="../testsuite.js"></script> <script src="../testsuite.js"></script>
<script src="../../../external/qunit-assert-classes/qunit-assert-classes.js"></script>
<script> <script>
TestHelpers.loadResources({ TestHelpers.loadResources({
css: [ "core", "menu" ], css: [ "core", "menu" ],
@ -52,7 +53,7 @@
<div id="qunit-fixture"> <div id="qunit-fixture">
<ul class="foo" id="menu1"> <ul class="foo" id="menu1">
<li class="foo"><div>Aberdeen</div></li> <li class="foo"><div>Aberdeen</div>
<li class="foo"><div>Ada</div></li> <li class="foo"><div>Ada</div></li>
<li class="foo"><div>Adamsville</div></li> <li class="foo"><div>Adamsville</div></li>
<li class="foo"><div id="testID1">Addyston</div></li> <li class="foo"><div id="testID1">Addyston</div></li>
@ -332,6 +333,18 @@
<li class="foo"><div>-Saarland</div></li> <li class="foo"><div>-Saarland</div></li>
</ul> </ul>
<ul class="foo" id="menu9">
<li class="foo">
<div>Aberdeen</div>
<ul>
<li class="foo"><div>Ada</div></li>
</ul>
</li>
<li class="foo"><div>Ada</div></li>
<li class="foo"><div>Adamsville</div></li>
<li class="foo"><div>Addyston</div></li>
<li class="foo"><div>Adelphi</div></li>
</ul>
</div> </div>
</body> </body>
</html> </html>

View File

@ -2,13 +2,23 @@
module( "menu: core" ); module( "menu: core" );
test( "markup structure", function() { test( "markup structure", function( assert ) {
expect( 6 ); expect( 11 );
var element = $( "#menu1" ).menu(); var element = $( "#menu9" ).menu(),
ok( element.hasClass( "ui-menu" ), "main element is .ui-menu" ); items = element.children(),
element.children().each(function( index ) { firstItemChildren = items.eq( 0 ).children();
ok( $( this ).hasClass( "ui-menu-item" ), "child " + index + " is .ui-menu-item" );
}); assert.hasClasses( element, "ui-menu ui-widget ui-widget-content" );
assert.hasClasses( items[ 0 ], "ui-menu-item" );
equal( items.eq( 0 ).children().length, 2, "Item has exactly 2 children when it has a sub menu" );
assert.hasClasses( firstItemChildren[ 0 ], "ui-menu-item-wrapper" );
assert.hasClasses( firstItemChildren[ 1 ], "ui-menu ui-widget ui-widget-content" );
assert.hasClasses( firstItemChildren.eq( 1 ).children()[ 0 ], "ui-menu-item" );
assert.hasClasses( firstItemChildren.eq( 1 ).children().eq( 0 ).children(), "ui-menu-item-wrapper" );
assert.hasClasses( items[ 1 ], "ui-menu-item" );
equal( items.eq( 1 ).children().length, 1, "Item has exactly 1 child when it does not have a sub menu" );
assert.hasClasses( items[ 2 ], "ui-menu-item" );
equal( items.eq( 2 ).children().length, 1, "Item has exactly 1 child when it does not have a sub menu" );
}); });
test( "accessibility", function () { test( "accessibility", function () {

View File

@ -38,6 +38,7 @@ return $.widget( "ui.menu", {
defaultElement: "<ul>", defaultElement: "<ul>",
delay: 300, delay: 300,
options: { options: {
classes: {},
icons: { icons: {
submenu: "ui-icon-caret-1-e" submenu: "ui-icon-caret-1-e"
}, },
@ -63,20 +64,19 @@ return $.widget( "ui.menu", {
this.mouseHandled = false; this.mouseHandled = false;
this.element this.element
.uniqueId() .uniqueId()
.addClass( "ui-menu ui-widget ui-widget-content" )
.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
.attr({ .attr({
role: this.options.role, role: this.options.role,
tabIndex: 0 tabIndex: 0
}); });
if ( this.options.disabled ) { if ( this.options.disabled ) {
this.element this._addClass( null, "ui-state-disabled" );
.addClass( "ui-state-disabled" ) this.element.attr( "aria-disabled", "true" );
.attr( "aria-disabled", "true" );
} }
this._addClass( "ui-menu", "ui-widget ui-widget-content" );
this._on({ this._on({
// Prevent focus from sticking to links inside menu after clicking // Prevent focus from sticking to links inside menu after clicking
// them (focus should always stay on UL during navigation). // them (focus should always stay on UL during navigation).
"mousedown .ui-menu-item": function( event ) { "mousedown .ui-menu-item": function( event ) {
@ -118,8 +118,8 @@ return $.widget( "ui.menu", {
var target = $( event.currentTarget ); var target = $( event.currentTarget );
// Remove ui-state-active class from siblings of the newly focused menu item // Remove ui-state-active class from siblings of the newly focused menu item
// to avoid a jump caused by adjacent elements both having a class with a border // to avoid a jump caused by adjacent elements both having a class with a border
target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" ); this._removeClass( target.siblings().children( ".ui-state-active" ),
null, "ui-state-active" );
this.focus( event, target ); this.focus( event, target );
}, },
mouseleave: "collapseAll", mouseleave: "collapseAll",
@ -159,11 +159,19 @@ return $.widget( "ui.menu", {
}, },
_destroy: function() { _destroy: function() {
var items = this.element.find( ".ui-menu-item" )
.removeAttr( "role" )
.removeAttr( "aria-disabled" ),
submenus = items.children( ".ui-menu-item-wrapper" )
.removeUniqueId()
.removeAttr( "tabIndex" )
.removeAttr( "role" )
.removeAttr( "aria-haspopup" );
// Destroy (sub)menus // Destroy (sub)menus
this.element this.element
.removeAttr( "aria-activedescendant" ) .removeAttr( "aria-activedescendant" )
.find( ".ui-menu" ).addBack() .find( ".ui-menu" ).addBack()
.removeClass( "ui-menu ui-widget ui-widget-content ui-menu-icons ui-front" )
.removeAttr( "role" ) .removeAttr( "role" )
.removeAttr( "tabIndex" ) .removeAttr( "tabIndex" )
.removeAttr( "aria-labelledby" ) .removeAttr( "aria-labelledby" )
@ -173,26 +181,12 @@ return $.widget( "ui.menu", {
.removeUniqueId() .removeUniqueId()
.show(); .show();
// Destroy menu items submenus.children().each(function() {
this.element.find( ".ui-menu-item" ) var elem = $( this );
.removeClass( "ui-menu-item" ) if ( elem.data( "ui-menu-submenu-caret" ) ) {
.removeAttr( "role" ) elem.remove();
.removeAttr( "aria-disabled" ) }
.children( ".ui-menu-item-wrapper" ) });
.removeUniqueId()
.removeClass( "ui-menu-item-wrapper ui-state-hover" )
.removeAttr( "tabIndex" )
.removeAttr( "role" )
.removeAttr( "aria-haspopup" )
.children().each(function() {
var elem = $( this );
if ( elem.data( "ui-menu-submenu-caret" ) ) {
elem.remove();
}
});
// Destroy menu dividers
this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
}, },
_keydown: function( event ) { _keydown: function( event ) {
@ -286,16 +280,15 @@ return $.widget( "ui.menu", {
}, },
refresh: function() { refresh: function() {
var menus, items, var menus, items, newSubmenus, newItems, newWrappers,
that = this, that = this,
icon = this.options.icons.submenu, icon = this.options.icons.submenu,
submenus = this.element.find( this.options.menus ); submenus = this.element.find( this.options.menus );
this.element.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length ); this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length );
// Initialize nested menus // Initialize nested menus
submenus.filter( ":not(.ui-menu)" ) newSubmenus = submenus.filter( ":not(.ui-menu)" )
.addClass( "ui-menu ui-widget ui-widget-content ui-front" )
.hide() .hide()
.attr({ .attr({
role: this.options.role, role: this.options.role,
@ -305,16 +298,17 @@ return $.widget( "ui.menu", {
.each(function() { .each(function() {
var menu = $( this ), var menu = $( this ),
item = menu.prev(), item = menu.prev(),
submenuCaret = $( "<span>" ) submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true );
.addClass( "ui-menu-icon ui-icon " + icon )
.data( "ui-menu-submenu-caret", true );
that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon );
item item
.attr( "aria-haspopup", "true" ) .attr( "aria-haspopup", "true" )
.prepend( submenuCaret ); .prepend( submenuCaret );
menu.attr( "aria-labelledby", item.attr( "id" ) ); menu.attr( "aria-labelledby", item.attr( "id" ) );
}); });
this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" );
menus = submenus.add( this.element ); menus = submenus.add( this.element );
items = menus.find( this.options.items ); items = menus.find( this.options.items );
@ -322,21 +316,21 @@ return $.widget( "ui.menu", {
items.not( ".ui-menu-item" ).each(function() { items.not( ".ui-menu-item" ).each(function() {
var item = $( this ); var item = $( this );
if ( that._isDivider( item ) ) { if ( that._isDivider( item ) ) {
item.addClass( "ui-widget-content ui-menu-divider" ); that._addClass( item, "ui-menu-divider", "ui-widget-content" );
} }
}); });
// Don't refresh list items that are already adapted // Don't refresh list items that are already adapted
items.not( ".ui-menu-item, .ui-menu-divider" ) newItems = items.not( ".ui-menu-item, .ui-menu-divider" );
.addClass( "ui-menu-item" ) newWrappers = newItems.children()
.children() .not( ".ui-menu" )
.not( ".ui-menu" ) .uniqueId()
.addClass( "ui-menu-item-wrapper" ) .attr({
.uniqueId() tabIndex: -1,
.attr({ role: this._itemRole()
tabIndex: -1, });
role: this._itemRole() this._addClass( newItems, "ui-menu-item" )
}); ._addClass( newWrappers, "ui-menu-item-wrapper" );
// Add aria-disabled attribute to any disabled menu item // Add aria-disabled attribute to any disabled menu item
items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
@ -356,26 +350,27 @@ return $.widget( "ui.menu", {
_setOption: function( key, value ) { _setOption: function( key, value ) {
if ( key === "icons" ) { if ( key === "icons" ) {
this.element.find( ".ui-menu-icon" ) var icons = this.element.find( ".ui-menu-icon" );
.removeClass( this.options.icons.submenu ) this._removeClass( icons, null, this.options.icons.submenu )
.addClass( value.submenu ); ._addClass( icons, null, value.submenu );
} }
if ( key === "disabled" ) { if ( key === "disabled" ) {
this.element this.element.attr( "aria-disabled", value );
.toggleClass( "ui-state-disabled", !!value ) this._toggleClass( null, "ui-state-disabled", !!value );
.attr( "aria-disabled", value );
} }
this._super( key, value ); this._super( key, value );
}, },
focus: function( event, item ) { focus: function( event, item ) {
var nested, focused; var nested, focused, activeParent;
this.blur( event, event && event.type === "focus" ); this.blur( event, event && event.type === "focus" );
this._scrollIntoView( item ); this._scrollIntoView( item );
this.active = item.first(); this.active = item.first();
focused = this.active.children( ".ui-menu-item-wrapper" ).addClass( "ui-state-active" );
focused = this.active.children( ".ui-menu-item-wrapper" );
this._addClass( focused, null, "ui-state-active" );
// Only update aria-activedescendant if there's a role // Only update aria-activedescendant if there's a role
// otherwise we assume focus is managed elsewhere // otherwise we assume focus is managed elsewhere
@ -384,11 +379,11 @@ return $.widget( "ui.menu", {
} }
// Highlight active parent menu item, if any // Highlight active parent menu item, if any
this.active activeParent = this.active
.parent() .parent()
.closest( ".ui-menu-item" ) .closest( ".ui-menu-item" )
.children( ".ui-menu-item-wrapper" ) .children( ".ui-menu-item-wrapper" );
.addClass( "ui-state-active" ); this._addClass( activeParent, null, "ui-state-active" );
if ( event && event.type === "keydown" ) { if ( event && event.type === "keydown" ) {
this._close(); this._close();
@ -434,7 +429,8 @@ return $.widget( "ui.menu", {
return; return;
} }
this.active.children( ".ui-menu-item-wrapper" ).removeClass( "ui-state-active" ); this._removeClass( this.active.children( ".ui-menu-item-wrapper" ),
null, "ui-state-active" );
this.active = null; this.active = null;
this._trigger( "blur", event, { item: this.active } ); this._trigger( "blur", event, { item: this.active } );
@ -498,14 +494,14 @@ return $.widget( "ui.menu", {
startMenu = this.active ? this.active.parent() : this.element; startMenu = this.active ? this.active.parent() : this.element;
} }
startMenu var active = startMenu
.find( ".ui-menu" ) .find( ".ui-menu" )
.hide() .hide()
.attr( "aria-hidden", "true" ) .attr( "aria-hidden", "true" )
.attr( "aria-expanded", "false" ) .attr( "aria-expanded", "false" )
.end() .end()
.find( ".ui-state-active" ).not( ".ui-menu-item-wrapper" ) .find( ".ui-state-active" ).not( ".ui-menu-item-wrapper" );
.removeClass( "ui-state-active" ); this._removeClass( active, null, "ui-state-active" );
}, },
_closeOnDocumentClick: function( event ) { _closeOnDocumentClick: function( event ) {