diff options
22 files changed, 1908 insertions, 1 deletions
diff --git a/Gruntfile.js b/Gruntfile.js index ad5e6f925..8c0a940bc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -35,6 +35,7 @@ var "progressbar", "resizable", "selectable", + "selectmenu", "slider", "spinner", "tabs", diff --git a/build/tasks/testswarm.js b/build/tasks/testswarm.js index 2bce06f26..86a06a69d 100644 --- a/build/tasks/testswarm.js +++ b/build/tasks/testswarm.js @@ -26,6 +26,7 @@ var versions = { "Progressbar": "progressbar/progressbar.html", "Resizable": "resizable/resizable.html", "Selectable": "selectable/selectable.html", + "Selectmenu": "selectmenu/selectmenu.html", "Slider": "slider/slider.html", "Sortable": "sortable/sortable.html", "Spinner": "spinner/spinner.html", diff --git a/demos/index.html b/demos/index.html index 4739d76cd..338da841a 100644 --- a/demos/index.html +++ b/demos/index.html @@ -24,6 +24,7 @@ <li><a href="removeClass/">removeClass</a></li> <li><a href="resizable/">resizable</a></li> <li><a href="selectable/">selectable</a></li> + <li><a href="selectmenu/">selectmenu</a></li> <li><a href="show/">show</a></li> <li><a href="slider/">slider</a></li> <li><a href="sortable/">sortable</a></li> diff --git a/demos/selectmenu/custom_render.html b/demos/selectmenu/custom_render.html new file mode 100644 index 000000000..7d33eee8c --- /dev/null +++ b/demos/selectmenu/custom_render.html @@ -0,0 +1,150 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Selectmenu - Default functionality</title> + <link rel="stylesheet" href="../../themes/base/jquery.ui.all.css"> + <script src="../../jquery-1.10.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.selectmenu.js"></script> + <link rel="stylesheet" href="../demos.css"> + <script> + $(function() { + $.widget( "custom.iconselectmenu", $.ui.selectmenu, { + _renderItem: function( ul, item ) { + var a, span, + li = $( "<li>" ); + + if ( item.disabled ) { + li.addClass( "ui-state-disabled" ).text( item.label ); + } else { + a = $( "<a>", { + text: item.label, + href: "#" + }).appendTo( li ); + span = $( "<span>", { + style: item.element.attr( "style" ), + "class": "ui-icon " + item.element.attr( "class" ) + }).appendTo( a ); + } + + return li.appendTo( ul ); + } + }); + + $( "#filesA" ) + .iconselectmenu() + .iconselectmenu( "menuWidget" ) + .addClass( "ui-menu-icons" ); + + $( "#filesB" ) + .iconselectmenu() + .iconselectmenu( "menuWidget" ) + .addClass( "ui-menu-icons customicons" ); + + $( "#people" ) + .iconselectmenu() + .iconselectmenu( "menuWidget") + .addClass( "ui-menu-icons avatar" ); + }); + </script> + <style> + h2 { + margin: 30px 0 0 0 + } + fieldset { + border: 0; + } + label { + display: block; + } + select { + width: 200px; + } + + .ui-selectmenu-menu .ui-menu .ui-icon { + top: 0.4em; + } + .ui-selectmenu-menu .ui-menu .ui-menu-item a { + padding-left: 2em; + } + + /* select with custom icons */ + .ui-selectmenu-menu .ui-menu.customicons .ui-menu-item a { + padding: 0.5em 0 0.5em 3em; + } + .ui-selectmenu-menu .ui-menu.customicons .ui-menu-item a .ui-icon { + height: 24px; + width: 24px; + top: 0.2em; + } + .ui-icon.video { + background: url(images/24-video-square.png) 0 0 no-repeat; + } + .ui-icon.podcast { + background: url(images/24-podcast-square.png) 0 0 no-repeat; + } + .ui-icon.rss { + background: url(images/24-rss-square.png) 0 0 no-repeat; + } + + /* select with CSS avatar icons */ + option.avatar { + background-repeat: no-repeat !important; + padding-left: 20px; + } + .avatar .ui-icon { + background-position: left top; + } + </style> +</head> +<body> + +<div class="demo"> + +<form action="#"> + + <h2>Selectmenu with framework icons</h2> + <fieldset> + <label for="filesA">Select a File:</label> + <select name="filesA" id="filesA"> + <option value="jquery" class="ui-icon-script">jQuery.js</option> + <option value="jquerylogo" class="ui-icon-image">jQuery Logo</option> + <option value="jqueryui" class="ui-icon-script">ui.jQuery.js</option> + <option value="jqueryuilogo" selected="selected" class="ui-icon-image">jQuery UI Logo</option> + <option value="somefile">Some unknown file</option> + </select> + </fieldset> + + <h2>Selectmenu with custom icon images</h2> + <fieldset> + <label for="filesB">Select a podcast:</label> + <select name="filesB" id="filesB"> + <option value="mypodcast" class="podcast">John Resig Podcast</option> + <option value="myvideo" class="video">Scott Gonzales Video</option> + <option value="myrss" class="rss">jQuery RSS XML</option> + </select> + </fieldset> + + <h2>Selectmenu with custom avatar 16x16 images as CSS background</h2> + <fieldset> + <label for="people">Select a Person:</label> + <select name="people" id="people"> + <option value="1" class="avatar" style="background-image: url(http://www.gravatar.com/avatar/b3e04a46e85ad3e165d66f5d927eb609?d=monsterid&r=g&s=16);">John Resig</option> + <option value="2" class="avatar" style="background-image: url(http://www.gravatar.com/avatar/e42b1e5c7cfd2be0933e696e292a4d5f?d=monsterid&r=g&s=16);">Tauren Mills</option> + <option value="3" class="avatar" style="background-image: url(http://www.gravatar.com/avatar/bdeaec11dd663f26fa58ced0eb7facc8?d=monsterid&r=g&s=16);">Jane Doe</option> + </select> + </fieldset> + +</form> + +</div> + +<div class="demo-description"> +<p>The whole rendering process is extendable to make custom styling as easy as possible.</p> +</div> +</body> +</html> diff --git a/demos/selectmenu/default.html b/demos/selectmenu/default.html new file mode 100644 index 000000000..2e35599bc --- /dev/null +++ b/demos/selectmenu/default.html @@ -0,0 +1,103 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Selectmenu - Default functionality</title> + <link rel="stylesheet" href="../../themes/base/jquery.ui.all.css"> + <script src="../../jquery-1.10.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.selectmenu.js"></script> + <link rel="stylesheet" href="../demos.css"> + <script> + $(function() { + $( "#speed" ).selectmenu(); + + $( "#files" ).selectmenu(); + + $( "#number" ) + .selectmenu() + .selectmenu( "menuWidget" ) + .addClass( "overflow" ); + }); + </script> + <style> + fieldset { + border: 0; + } + label { + display: block; + margin: 30px 0 0 0; + } + select { + width: 200px; + } + .overflow { + height: 200px; + } + </style> +</head> +<body> + +<div class="demo"> + +<form action="#"> + + <fieldset> + <label for="speed">Select a speed</label> + <select name="speed" id="speed"> + <option value="Slower">Slower</option> + <option value="Slow">Slow</option> + <option value="Medium" selected="selected">Medium</option> + <option value="Fast">Fast</option> + <option value="Faster">Faster</option> + </select> + + <label for="files">Select a file</label> + <select name="files" id="files"> + <optgroup label="Scripts"> + <option value="jquery">jQuery.js</option> + <option value="jqueryui">ui.jQuery.js</option> + </optgroup> + <optgroup label="Other files"> + <option value="somefile">Some unknown file</option> + <option value="someotherfile">Some other file with a very long option text</option> + </optgroup> + </select> + + <label for="number">Select a number</label> + <select name="number" id="number"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + <option value="11">11</option> + <option value="12">12</option> + <option value="13">13</option> + <option value="14">14</option> + <option value="15">15</option> + <option value="16">16</option> + <option value="17">17</option> + <option value="18">18</option> + <option value="19">19</option> + </select> + </fieldset> + +</form> + +</div> + +<div class="demo-description"> +<p>The Selectmenu widgets provides a styleable select element replacement. It will act as a proxy back to the original select element, controlling its state for form submission or serialization </p> +<p>The datasource is a native select element. Supports optgroups.</p> +</div> +</body> +</html> diff --git a/demos/selectmenu/images/24-podcast-square.png b/demos/selectmenu/images/24-podcast-square.png Binary files differnew file mode 100644 index 000000000..3c3e38f3f --- /dev/null +++ b/demos/selectmenu/images/24-podcast-square.png diff --git a/demos/selectmenu/images/24-rss-square.png b/demos/selectmenu/images/24-rss-square.png Binary files differnew file mode 100644 index 000000000..f59b69ed3 --- /dev/null +++ b/demos/selectmenu/images/24-rss-square.png diff --git a/demos/selectmenu/images/24-video-square.png b/demos/selectmenu/images/24-video-square.png Binary files differnew file mode 100644 index 000000000..ce50ccfde --- /dev/null +++ b/demos/selectmenu/images/24-video-square.png diff --git a/demos/selectmenu/index.html b/demos/selectmenu/index.html new file mode 100644 index 000000000..ef7162c4d --- /dev/null +++ b/demos/selectmenu/index.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Selectmenu Demos</title> +</head> +<body> + +<ul> + <li><a href="default.html">Default functionality</a></li> + <li><a href="custom_render.html">Custom item rendering functionality</a></li> +</ul> + +</body> +</html> diff --git a/tests/unit/selectmenu/all.html b/tests/unit/selectmenu/all.html new file mode 100644 index 000000000..c3089ae66 --- /dev/null +++ b/tests/unit/selectmenu/all.html @@ -0,0 +1,30 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Selectmenu Test Suite</title> + + <script src="../../../jquery-1.9.1.js"></script> + + <link rel="stylesheet" href="../../../external/qunit.css"> + <link rel="stylesheet" href="../qunit-composite.css"> + <script src="../../../external/qunit.js"></script> + <script src="../qunit-composite.js"></script> + <script src="../subsuite.js"></script> + + <script> + testAllVersions( "selectmenu" ); + </script> +</head> +<body> + +<h1 id="qunit-header">jQuery UI Selectmenu Test Suite</h1> +<h2 id="qunit-banner"></h2> +<div id="qunit-testrunner-toolbar"></div> +<h2 id="qunit-userAgent"></h2> +<ol id="qunit-tests"></ol> +<div id="qunit-fixture"> + +</div> +</body> +</html> diff --git a/tests/unit/selectmenu/selectmenu.html b/tests/unit/selectmenu/selectmenu.html new file mode 100644 index 000000000..584a47b53 --- /dev/null +++ b/tests/unit/selectmenu/selectmenu.html @@ -0,0 +1,91 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Selectmenu Test Suite</title> + + <script src="../../jquery.js"></script> + <link rel="stylesheet" href="../../../external/qunit.css"> + <script src="../../../external/qunit.js"></script> + <script src="../../jquery.simulate.js"></script> + <script src="../testsuite.js"></script> + <script> + TestHelpers.loadResources({ + css: [ "ui.core", "ui.menu" , "ui.selectmenu" ], + js: [ + "ui/jquery.ui.core.js", + "ui/jquery.ui.widget.js", + "ui/jquery.ui.position.js", + "ui/jquery.ui.menu.js", + "ui/jquery.ui.selectmenu.js" + ] + }); + </script> + + <script src="selectmenu_common.js"></script> + <script src="selectmenu_core.js"></script> + <script src="selectmenu_events.js"></script> + <script src="selectmenu_methods.js"></script> + <script src="selectmenu_options.js"></script> + + <script src="../swarminject.js"></script> +</head> +<body> + +<h1 id="qunit-header">jQuery UI Selectmenu Test Suite</h1> +<h2 id="qunit-banner"></h2> +<div id="qunit-testrunner-toolbar"></div> +<h2 id="qunit-userAgent"></h2> +<ol id="qunit-tests"></ol> +<div id="qunit-fixture"> + <div id="selectmenu-wrap1" class="selectmenu-wrap"></div> + + <div id="selectmenu-wrap2" class="selectmenu-wrap"> + <label for="speed">Select a speed:</label> + <select name="speed" id="speed"> + <option value="Slower">Slower</option> + <option value="Slow">Slow</option> + <option value="Medium" selected="selected">Medium</option> + <option value="Fast">Fast</option> + <option value="Faster">Faster</option> + </select> + </div> + + <label for="number">Select a number:</label> + <select name="number" id="number"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + <option value="11">11</option> + <option value="12">12</option> + <option value="13">13</option> + <option value="14">14</option> + <option value="15">15</option> + <option value="16">16</option> + <option value="17">17</option> + <option value="18">18</option> + <option value="19">19</option> + </select> + + <label for="files">Select a file:</label> + <select name="files" id="files"> + <optgroup label="Scripts"> + <option value="jquery">jQuery.js</option> + <option value="jqueryui">ui.jQuery.js</option> + </optgroup> + <optgroup label="Other files"> + <option value="somefile">Some unknown file</option> + <option value="someotherfile">Some other file</option> + </optgroup> + </select> + +</div> +</body> +</html> diff --git a/tests/unit/selectmenu/selectmenu_common.js b/tests/unit/selectmenu/selectmenu_common.js new file mode 100644 index 000000000..d5addff9a --- /dev/null +++ b/tests/unit/selectmenu/selectmenu_common.js @@ -0,0 +1,24 @@ +TestHelpers.commonWidgetTests( "selectmenu", { + defaults: { + appendTo: null, + disabled: false, + icons: { + button: "ui-icon-triangle-1-s" + }, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + widthButton: null, + widthMenu: null, + + // callbacks + create: null, + change: null, + close: null, + focus: null, + open: null, + select: null + } +}); diff --git a/tests/unit/selectmenu/selectmenu_core.js b/tests/unit/selectmenu/selectmenu_core.js new file mode 100644 index 000000000..d8b449828 --- /dev/null +++ b/tests/unit/selectmenu/selectmenu_core.js @@ -0,0 +1,235 @@ +(function( $ ) { + +module( "selectmenu: core" ); + +asyncTest( "accessibility", function() { + var links, + element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + button.simulate( "focus" ); + links = menu.find( "li.ui-menu-item a" ); + + expect( 12 + links.length * 2 ); + + setTimeout(function() { + equal( button.attr( "role" ), "combobox", "button link role" ); + equal( button.attr( "aria-haspopup" ), "true", "button link aria-haspopup" ); + equal( button.attr( "aria-expanded" ), "false", "button link aria-expanded" ); + equal( button.attr( "aria-autocomplete" ), "list", "button link aria-autocomplete" ); + equal( button.attr( "aria-owns" ), menu.attr("id"), "button link aria-owns" ); + equal( + button.attr( "aria-labelledby" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "button link aria-labelledby" + ); + equal( button.attr( "tabindex" ), 0, "button link tabindex" ); + + equal( menu.attr( "role" ), "listbox", "menu role" ); + equal( menu.attr( "aria-labelledby" ), button.attr( "id" ), "menu aria-labelledby" ); + equal( menu.attr( "aria-hidden" ), "true", "menu aria-hidden" ); + equal( menu.attr( "tabindex" ), 0, "menu tabindex" ); + equal( + menu.attr( "aria-activedescendant" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "menu aria-activedescendant" + ); + $.each( links, function( index ){ + equal( $( this ).attr( "role" ), "option", "menu link #" + index +" role" ); + equal( $( this ).attr( "tabindex" ), -1, "menu link #" + index +" tabindex" ); + }); + start(); + }); +}); + + +$.each([ + { + type: "default", + selector: "#speed" + }, + { + type: "optgroups", + selector: "#files" + } +], function( i, settings ) { + asyncTest( "state synchronization - after keydown on button - " + settings.type, function () { + expect( 4 ); + + var links, + element = $( settings.selector ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ), + selected = element.find( "option:selected" ); + + button.simulate( "focus" ); + setTimeout(function() { + links = menu.find("li.ui-menu-item a"); + + button.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); + equal( + menu.attr( "aria-activedescendant" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "menu aria-activedescendant" + ); + equal( + button.attr( "aria-activedescendant" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "button aria-activedescendant" + ); + equal( + element.find( "option:selected" ).val(), + selected.next( "option" ).val() , + "original select state" + ); + equal( button.text(), selected.next( "option" ).text(), "button text" ); + start(); + }, 1 ); + }); + + asyncTest( "state synchronization - after click on item - " + settings.type, function () { + expect( 4 ); + + var links, + element = $( settings.selector ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + button.simulate( "focus" ); + setTimeout(function() { + links = menu.find("li.ui-menu-item a"); + + button.trigger( "click" ); + menu.find( "a" ).last().simulate( "mouseover" ).trigger( "click" ); + equal( + menu.attr( "aria-activedescendant" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "menu aria-activedescendant" + ); + equal( + button.attr( "aria-activedescendant" ), + links.eq( element[ 0 ].selectedIndex ).attr( "id" ), + "button aria-activedescendant" + ); + equal( + element.find( "option:selected" ).val(), + element.find( "option" ).last().val(), + "original select state" + ); + equal( button.text(), element.find( "option" ).last().text(), "button text" ); + start(); + }, 1 ); + }); + + asyncTest( "state synchronization - after focus item and keydown on button - " + settings.type, function () { + expect( 4 ); + + var links, + element = $( settings.selector ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ), + options = element.find( "option" ); + + // init menu + button.simulate( "focus" ); + + setTimeout(function() { + links = menu.find( "li.ui-menu-item a" ); + // open menu and click first item + button.trigger( "click" ); + links.first().simulate( "mouseover" ).trigger( "click" ); + // open menu again and hover item + button.trigger( "click" ); + links.eq( 3 ).simulate( "mouseover" ); + // close and use keyboard control on button + button.simulate( "keydown", { keyCode: $.ui.keyCode.ESCAPE } ); + button.simulate( "focus" ); + setTimeout(function() { + button.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); + + equal( menu.attr( "aria-activedescendant" ), links.eq( 1 ).attr( "id" ), "menu aria-activedescendant" ); + equal( button.attr( "aria-activedescendant" ), links.eq( 1 ).attr( "id" ), "button aria-activedescendant" ); + equal( element.find( "option:selected" ).val(), options.eq( 1 ).val() , "original select state" ); + equal( button.text(), options.eq( 1 ).text(), "button text" ); + start(); + }, 1 ); + }, 1 ); + }); + + asyncTest( "item looping - " + settings.type, function () { + expect( 2 ); + + var links, + element = $( settings.selector ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + // init menu + button.simulate( "focus" ); + + setTimeout(function() { + links = menu.find( "li.ui-menu-item a" ); + + button.trigger( "click" ); + links.first().simulate( "mouseover" ).trigger( "click" ); + button.simulate( "keydown", { keyCode: $.ui.keyCode.UP } ); + equal( element[ 0 ].selectedIndex, 0, "No looping beyond first item" ); + + button.trigger( "click" ); + links.last().simulate( "mouseover" ).trigger( "click" ); + button.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); + equal( element[ 0 ].selectedIndex + 1, links.length, "No looping behind last item" ); + start(); + }, 1 ); + }); + + asyncTest( "item focus and active state - " + settings.type, function () { + expect( 8 ); + + var element = $( settings.selector ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ), + links, focusedItem, activeItem; + + // init menu + button.simulate( "focus" ); + + setTimeout(function() { + links = menu.find( "li.ui-menu-item a" ); + + button.trigger( "click" ); + setTimeout(function() { + checkItemClasses(); + + links.eq( 3 ).simulate( "mouseover" ).trigger( "click" ); + + button.trigger( "click" ); + links.eq( 2 ).simulate( "mouseover" ); + $( document ).trigger( "click" ); + + button.trigger( "click" ); + links.eq( 1 ).simulate( "mouseover" ); + $( document ).trigger( "click" ); + + button.trigger( "click" ); + setTimeout(function() { + checkItemClasses(); + start(); + }, 350 ); + }, 350 ); + }, 1 ); + + function checkItemClasses() { + focusedItem = menu.find( "li.ui-menu-item a.ui-state-focus" ); + equal( focusedItem.length, 1, "only one item has ui-state-focus class" ); + equal( focusedItem.attr( "id" ), links.eq( element[ 0 ].selectedIndex ).attr( "id" ), "selected item has ui-state-focus class" ); + + activeItem = menu.find( "li.ui-menu-item a.ui-state-active" ); + equal( activeItem.length, 1, "only one item has ui-state-active class" ); + equal( activeItem.attr( "id" ), links.eq( element[ 0 ].selectedIndex ).attr( "id" ), "selected item has ui-state-active class" ); + } + }); +}); + +})( jQuery ); diff --git a/tests/unit/selectmenu/selectmenu_events.js b/tests/unit/selectmenu/selectmenu_events.js new file mode 100644 index 000000000..353780ee5 --- /dev/null +++ b/tests/unit/selectmenu/selectmenu_events.js @@ -0,0 +1,135 @@ +(function ( $ ) { + +module( "selectmenu: events", { + setup: function () { + this.element = $( "#speed" ); + } +}); + +asyncTest( "change", function () { + expect( 5 ); + + var optionIndex = 1, + button, menu, options; + + this.element.selectmenu({ + change: function ( event, ui ) { + ok( event, "change event fired on change" ); + equal( event.type, "selectmenuchange", "event type set to selectmenuchange" ); + equal( ui.item.index, optionIndex, "ui.item.index contains correct option index" ); + equal( ui.item.element[ 0 ], options.eq( optionIndex )[ 0 ], "ui.item.element contains original option element" ); + equal( ui.item.value, options.eq( optionIndex ).text(), "ui.item.value property updated correctly" ); + } + }); + + button = this.element.selectmenu( "widget" ); + menu = this.element.selectmenu( "menuWidget" ).parent(); + options = this.element.find( "option" ); + + button.simulate( "focus" ); + + setTimeout(function() { + button.trigger( "click" ); + menu.find( "a" ).eq( optionIndex ).simulate( "mouseover" ).trigger( "click" ); + start(); + }, 1 ); +}); + + +test( "close", function () { + expect( 4 ); + + this.element.selectmenu({ + close: function ( event ) { + ok( event, "close event fired on close" ); + equal( event.type, "selectmenuclose", "event type set to selectmenuclose" ); + } + }); + + this.element.selectmenu( "open" ).selectmenu( "close" ); + + this.element.selectmenu( "open" ); + $( "body" ).trigger( "click" ); +}); + + +asyncTest( "focus", function () { + expect( 12 ); + + var that = this, + optionIndex = this.element[ 0 ].selectedIndex + 1, + options = this.element.find( "option" ), + button, menu, links; + + this.element.selectmenu({ + focus: function ( event, ui ) { + ok( event, "focus event fired on element #" + optionIndex + " mouseover" ); + equal( event.type, "selectmenufocus", "event type set to selectmenufocus" ); + equal( ui.item.index, optionIndex, "ui.item.index contains correct option index" ); + equal( ui.item.element[ 0 ], options.eq( optionIndex )[ 0 ], "ui.item.element contains original option element" ); + } + }); + + button = this.element.selectmenu( "widget" ); + menu = this.element.selectmenu( "menuWidget" ); + + button.simulate( "focus" ); + + setTimeout(function() { + button.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); + + button.trigger( "click" ); + links = menu.find( "li.ui-menu-item a" ); + optionIndex = 0; + links.eq( optionIndex ).simulate( "mouseover" ); + optionIndex += 1; + links.eq( optionIndex ).simulate( "mouseover" ); + + // this tests for unwanted, additional focus event on close + that.element.selectmenu( "close" ); + start(); + }, 1 ); +}); + + +test( "open", function () { + expect( 2 ); + + this.element.selectmenu({ + open: function ( event ) { + ok( event, "open event fired on open" ); + equal( event.type, "selectmenuopen", "event type set to selectmenuopen" ); + } + }); + + this.element.selectmenu( "open" ); +}); + + +asyncTest( "select", function () { + expect( 4 ); + + this.element.selectmenu({ + select: function ( event, ui ) { + ok( event, "select event fired on item select" ); + equal( event.type, "selectmenuselect", "event type set to selectmenuselect" ); + equal( ui.item.index, optionIndex, "ui.item.index contains correct option index" ); + equal( ui.item.element[ 0 ], options.eq( optionIndex )[ 0 ], "ui.item.element contains original option element" ); + } + }); + + var button = this.element.selectmenu( "widget" ), + menu = this.element.selectmenu( "menuWidget" ).parent(), + options = this.element.find( "option" ), + optionIndex = 1; + + button.simulate( "focus" ); + + setTimeout(function() { + button.trigger( "click" ); + menu.find( "a" ).eq( optionIndex ).simulate( "mouseover" ).trigger( "click" ); + start(); + }, 1 ); +}); + +})(jQuery); diff --git a/tests/unit/selectmenu/selectmenu_methods.js b/tests/unit/selectmenu/selectmenu_methods.js new file mode 100644 index 000000000..f2b56e835 --- /dev/null +++ b/tests/unit/selectmenu/selectmenu_methods.js @@ -0,0 +1,169 @@ +(function( $ ) { + +module( "selectmenu: methods" ); + +test( "destroy", function() { + expect( 1 ); + domEqual( "#speed", function() { + $( "#speed" ).selectmenu().selectmenu( "destroy" ); + }); +}); + + +test( "open / close", function() { + expect( 4 ); + + var element = $( "#speed" ).selectmenu(), + menu = element.selectmenu( "menuWidget" ); + + element.selectmenu( "open" ); + ok( menu.is( ":visible" ), "open: menu visible" ); + equal( menu.attr( "aria-hidden" ), "false", "open: menu aria-disabled" ); + + element.selectmenu( "close" ); + ok( menu.is( ":hidden" ), "close: menu hidden" ); + equal( menu.attr( "aria-hidden" ), "true", "close: menu aria-disabled" ); +}); + + +test( "enable / disable", function () { + expect(10); + + var element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + element.selectmenu( "disable" ); + ok( element.selectmenu( "option", "disabled" ), "disable: widget option" ); + equal( element.attr( "disabled" ), "disabled", "disable: native select disabled" ); + equal( button.attr( "aria-disabled" ), "true", "disable: button wrapper ARIA" ); + equal( button.attr( "tabindex" ), -1, "disable: button tabindex" ); + equal( menu.attr( "aria-disabled" ), "true", "disable: menu wrapper ARIA" ); + + element.selectmenu( "enable" ); + ok( !element.selectmenu( "option", "disabled" ), "enable: widget option" ); + equal( element.attr( "disabled" ), undefined, "enable: native select disabled" ); + equal( button.attr( "aria-disabled" ), "false", "enable: button wrapper ARIA" ); + equal( button.attr( "tabindex" ), 0, "enable: button tabindex" ); + equal( menu.attr( "aria-disabled" ), "false", "enable: menu wrapper ARIA" ); +}); + + +test( "refresh - structure", function () { + expect( 3 ); + + var element = $( "#speed" ).selectmenu(), + menu = element.selectmenu( "menuWidget" ).parent(); + + element.find( "option" ).eq( 2 ).remove(); + element.find( "option" ).eq( 3 ).remove(); + element.append( "<option value=\"added_option\">Added option</option>" ); + element.find( "option" ).first() + .attr( "value", "changed_value" ) + .text( "Changed value" ); + element.selectmenu( "refresh" ); + + equal( element.find( "option" ).length, menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).length, "menu item length" ); + equal( element.find( "option" ).last().text(), menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).last().text(), "added item" ); + equal( element.find( "option" ).first().text(), menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).first().text(), "changed item" ); +}); + +asyncTest( "refresh - change selected option", function () { + expect( 4 ); + + var element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ); + + equal( element.find( "option:selected" ).text(), button.text(), "button text after init" ); + + button.simulate( "focus" ); + + setTimeout(function() { + equal( element.find( "option:selected" ).text(), button.text(), "button text after focus" ); + + element[ 0 ].selectedIndex = 0; + element.selectmenu( "refresh" ); + equal( element.find( "option:selected" ).text(), button.text(), "button text after changing selected option" ); + + element.find( "option" ).removeAttr( "selected" ); + element.prepend( "<option selected value=\"selected_option\">Selected option</option>" ); + element.selectmenu( "refresh" ); + equal( element.find( "option:selected" ).text(), button.text(), "button text after adding selected option" ); + + start(); + }, 1 ); +}); + + +test( "refresh - disabled select", function () { + expect( 4 ); + + var element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + element.attr( "disabled", "disabled" ); + element.selectmenu( "refresh" ); + + ok( element.selectmenu( "option", "disabled" ), "widget option" ); + equal( button.attr( "aria-disabled" ), "true", "button wrapper ARIA" ); + equal( button.attr( "tabindex" ), -1, "button tabindex" ); + equal( menu.attr( "aria-disabled" ), "true", "menu wrapper ARIA" ); +}); + + +test( "refresh - disabled option", function () { + expect(1); + + var disabledItem, + element = $( "#speed" ).selectmenu(), + menu = element.selectmenu( "menuWidget" ).parent(); + + element.attr( "disabled", "disabled" ); + element.find( "option" ).eq( 2 ).attr( "disabled", "disabled" ); + element.selectmenu( "refresh" ); + + disabledItem = menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).eq(2); + ok( disabledItem.hasClass( "ui-state-disabled" ), "class" ); +}); + + +test( "refresh - disabled optgroup", function () { + + var i, item, + element = $( "#files" ).selectmenu(), + menu = element.selectmenu( "menuWidget" ).parent(), + originalDisabledOptgroup = element.find( "optgroup" ).first(), + originalDisabledOptions = originalDisabledOptgroup.find( "option" ); + + expect( 2 + originalDisabledOptions.length ); + + originalDisabledOptgroup.attr( "disabled", "disabled" ); + element.selectmenu( "refresh" ); + + item = menu.find( "li.ui-selectmenu-optgroup" ).first(); + ok( item.hasClass( "ui-state-disabled" ), "class" ); + + equal( menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).filter( ".ui-state-disabled" ).length, originalDisabledOptions.length, "disabled options" ); + for ( i = 0; i < originalDisabledOptions.length; i++ ) { + item = item.next( "li" ); + ok( item.hasClass( "ui-state-disabled" ), "item #" + i + ": class" ); + } +}); + +test( "widget and menuWidget", function() { + expect( 4 ); + var element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + element.selectmenu( "refresh" ); + + equal( button.length, 1, "button: one element" ); + ok( button.is( ".ui-selectmenu-button" ), "button: class" ); + + equal( menu.length, 1, "Menu Widget: one element" ); + ok( menu.is( "ul.ui-menu" ), "Menu Widget: element and class" ); +}); + +})( jQuery ); diff --git a/tests/unit/selectmenu/selectmenu_options.js b/tests/unit/selectmenu/selectmenu_options.js new file mode 100644 index 000000000..82ea6a8b4 --- /dev/null +++ b/tests/unit/selectmenu/selectmenu_options.js @@ -0,0 +1,60 @@ +(function ( $ ) { + +module( "selectmenu: options" ); + +test( "appendTo another element", function () { + expect( 8 ); + + var detached = $( "<div>" ), + element = $( "#speed" ).selectmenu(); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], document.body, "defaults to body" ); + element.selectmenu( "destroy" ); + + element.selectmenu({ + appendTo: ".selectmenu-wrap" + }); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], $( "#selectmenu-wrap1" )[ 0 ], "first found element" ); + equal( $( "#selectmenu-wrap2 .ui-selectmenu" ).length, 0, "only appends to one element" ); + element.selectmenu( "destroy" ); + + $( "#selectmenu-wrap2" ).addClass( "ui-front" ); + element.selectmenu(); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], $( "#selectmenu-wrap2" )[ 0 ], "null, inside .ui-front" ); + element.selectmenu( "destroy" ); + $( "#selectmenu-wrap2" ).removeClass( "ui-front" ); + + element.selectmenu().selectmenu( "option", "appendTo", "#selectmenu-wrap1" ); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], $( "#selectmenu-wrap1" )[ 0 ], "modified after init" ); + element.selectmenu( "destroy" ); + + element.selectmenu({ + appendTo: detached + }); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], detached[ 0 ], "detached jQuery object" ); + element.selectmenu( "destroy" ); + + element.selectmenu({ + appendTo: detached[ 0 ] + }); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], detached[ 0 ], "detached DOM element" ); + element.selectmenu( "destroy" ); + + element.selectmenu().selectmenu( "option", "appendTo", detached ); + equal( element.selectmenu( "menuWidget" ).parent().parent()[ 0 ], detached[ 0 ], "detached DOM element via option()" ); + element.selectmenu( "destroy" ); +}); + + +test( "CSS styles", function () { + expect( 2 ); + + var element = $( "#speed" ).selectmenu(), + button = element.selectmenu( "widget" ), + menu = element.selectmenu( "menuWidget" ); + + element.selectmenu( "open" ); + ok( button.hasClass( "ui-corner-top" ) && !button.hasClass( "ui-corner-all" ) && button.find( "span.ui-icon" ).hasClass( "ui-icon-triangle-1-s" ), "button styles dropdown" ); + ok( menu.hasClass( "ui-corner-bottom" ) && !menu.hasClass( "ui-corner-all" ), "menu styles dropdown" ); +}); + +})( jQuery ); diff --git a/tests/visual/compound/dialog_widgets.html b/tests/visual/compound/dialog_widgets.html index f5b0be95e..25bbd123f 100644 --- a/tests/visual/compound/dialog_widgets.html +++ b/tests/visual/compound/dialog_widgets.html @@ -22,8 +22,10 @@ <script src="../../../ui/jquery.ui.slider.js"></script> <script src="../../../ui/jquery.ui.tabs.js"></script> <script src="../../../ui/jquery.ui.tooltip.js"></script> + <script src="../../../ui/jquery.ui.selectmenu.js"></script> <script> $(function() { + $( "#dialog" ).dialog(); $( "[title]" ).tooltip(); $( "#accordion" ).accordion(); $( "#autocomplete" ).autocomplete({ @@ -45,7 +47,7 @@ } }); $( "#tabs" ).tabs(); - $( "#dialog" ).dialog(); + $( '#select' ).selectmenu(); $( "#dialog2" ).dialog({ autoOpen: false, width: 100, @@ -92,6 +94,13 @@ <div id="tabs-2">Phasellus mattis tincidunt nibh.</div> <div id="tabs-3">Nam dui erat, auctor a, dignissim quis, sollicitudin eu, felis. Pellentesque nisi urna, interdum eget, sagittis et, consequat vestibulum, lacus. Mauris porttitor ullamcorper augue.</div> </div> + <select id="select"> + <option>Slower</option> + <option>Slow</option> + <option>Medium</option> + <option>Fast</option> + <option>Faster</option> + </select> </div> <div id="dialog2"> Yay, another dialog. diff --git a/tests/visual/compound/tabs_selectmenu.html b/tests/visual/compound/tabs_selectmenu.html new file mode 100644 index 000000000..9554562c5 --- /dev/null +++ b/tests/visual/compound/tabs_selectmenu.html @@ -0,0 +1,53 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Compound Visual Test : Selectmenu in Tabs</title> + <link rel="stylesheet" href="../visual.css"> + <link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css"> + <script src="../../../jquery-1.10.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.selectmenu.js"></script> + <script src="../../../ui/jquery.ui.tabs.js"></script> + <script> + $(function() { + $( "#tabs" ).tabs(); + $( "select" ).selectmenu(); + }); + </script> + <style> + select { width: 200px; } + </style> +</head> +<body> + +<div id="tabs"> + <ul> + <li><a href="#tabs-1">First</a></li> + <li><a href="#tabs-2">Second</a></li> + </ul> + <div id="tabs-1"> + <select> + <option>Slower</option> + <option>Slow</option> + <option>Medium</option> + <option>Fast</option> + <option>Faster</option> + </select> + </div> + <div id="tabs-2"> + <select> + <option>Slower</option> + <option>Slow</option> + <option>Medium</option> + <option>Fast</option> + <option>Faster</option> + </select> + </div> +</div> + +</body> +</html> diff --git a/tests/visual/selectmenu/selectmenu.html b/tests/visual/selectmenu/selectmenu.html new file mode 100644 index 000000000..fab7c0a7c --- /dev/null +++ b/tests/visual/selectmenu/selectmenu.html @@ -0,0 +1,248 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Selectmenu Visual Test: Default</title> + <link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css"> + <script src="../../../jquery-1.10.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.selectmenu.js"></script> + <script> + $(function() { + var log = $("#log"), + index = 0, + text; + + function logger( event, ui ) { + text = "#" + index++ + " " + event.type.replace("selectmenu",""); + if ( ui.item ) { + text += ": " + ui.item.index + " => " + ui.item.label; + } + $( "<p>" ).text( text ).prependTo( "#log" ); + } + + /* events */ + var eventsSelectmenu = $('#control select').selectmenu({ + open: logger, + close: logger, + focus : logger, + select: logger, + change: logger + }); + eventsSelectmenu.show(); + + $("#destroy").click( function() { + eventsSelectmenu.selectmenu("destroy"); + return false; + }); + + $("#refresh_add").click( function() { + eventsSelectmenu.append('<option value="fastsound">Speed of light</option>'); + eventsSelectmenu.selectmenu("refresh"); + return false; + }); + + $("#refresh_selected").click( function() { + eventsSelectmenu[0].selectedIndex = 0; + eventsSelectmenu.selectmenu("refresh"); + return false; + }); + + $("#refresh").click( function() { + eventsSelectmenu.selectmenu("refresh"); + return false; + }); + + $("#open").click( function() { + eventsSelectmenu.selectmenu("open"); + return false; + }); + + $("#close").click( function() { + eventsSelectmenu.selectmenu("close"); + return false; + }); + + /* disabled */ + $('#disabled1, #disabled2, #disabled3').selectmenu(); + var disabled4 = $('#disabled4').selectmenu(); + + $("#disable_select").on("click", function() { + if (disable_select) { + disable_select = false; + disabled4.selectmenu("disable"); + } else { + disable_select = true; + disabled4.removeAttr("disabled"); + } + disabled4.selectmenu("refresh"); + return false; + }); + + $("#disable_option").on("click", function() { + if (disable_option) { + disable_option = false; + disabled4.find("option:eq(0)").attr("disabled", "disabled"); + } else { + disable_option = true; + disabled4.find("option:eq(0)").removeAttr("disabled"); + } + disabled4.selectmenu("refresh"); + return false; + }); + + $("#disable_optgroup").on("click", function() { + if (disable_optgroup) { + disable_optgroup = false; + disabled4.find("optgroup:eq(0)").attr("disabled", "disabled"); + } else { + disable_optgroup = true; + disabled4.find("optgroup:eq(0)").removeAttr("disabled"); + } + disabled4.selectmenu("refresh"); + return false; + }); + + /* empty */ + $('.empty select').selectmenu(); + }); + </script> + <style> + body { font-size:62.5%; } + fieldset { border: 0; } + label { display: block; } + select { width: 200px; } + + .ui-selectmenu-button { display: block; margin-bottom: 1em;} + </style> +</head> +<body> + +<div id="control"> + <h2>Event logging tests</h2> + <form action="#"> + <button id="open">Open</button> + <button id="close">Close</button> + <button id="refresh_add">Add item</button> + <button id="refresh_selected">Change to first item</button> + <button id="refresh">Refresh</button> + <button id="destroy">Destroy</button> + <fieldset> + <select> + <option value="Slower">Slower</option> + <option value="Slow">Slow</option> + <option value="Medium" selected="selected">Medium</option> + <option value="Fast">Fast</option> + <option value="Faster">Faster</option> + </select> + </fieldset> + </form> +</div> + +<form action="#"> + <h2>Disabled tests</h2> + <fieldset> + <label for="disabled1">Disabled select</label> + <select disabled="disabled" name="disabled1" id="disabled1"> + <option value="Slower">Slower</option> + <option value="Slow">Slow</option> + <option value="Medium" selected="selected">Medium</option> + <option value="Fast">Fast</option> + <option value="Faster">Faster</option> + </select> + + <label for="disabled2">Disabled options</label> + <select name="disabled2" id="disabled2"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option disabled="disabled" value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option disabled="disabled" value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + <option disabled="disabled" value="11">11</option> + <option value="12">12</option> + <option value="13">13</option> + <option disabled="disabled" value="14">14</option> + <option disabled="disabled" value="15">15</option> + <option value="16">16</option> + <option value="17">17</option> + <option value="18">18</option> + <option value="19">19</option> + </select> + + <label for="disabled3">Disabled optgroup</label> + <select name="disabled3" id="disabled3"> + <optgroup disabled="disabled" label="Scripts"> + <option value="jquery">jQuery.js</option> + <option value="jqueryui">ui.jQuery.js</option> + </optgroup> + <optgroup label="Other files"> + <option value="somefile">Some unknown file</option> + <option value="someotherfile">Some other file</option> + </optgroup> + </select> + + <label for="disabled4">Disable specific element and refresh selectmenu on button click</label> + <select name="disabled4" id="disabled4"> + <optgroup label="Scripts"> + <option value="jquery">jQuery.js</option> + <option value="jqueryui">ui.jQuery.js</option> + </optgroup> + <optgroup label="Other files"> + <option value="somefile">Some unknown file</option> + <option value="someotherfile">Some other file</option> + </optgroup> + </select> + <button id="disable_select">Toggle disable select</button> + <button id="disable_option">Toggle disable option</button> + <button id="disable_optgroup">Toggle disable optgroup</button> + </fieldset> + + <h2>Empty tests</h2> + <fieldset class="empty"> + <label for="empty1">Select with no option elements</label> + <select name="empty1" id="empty1"></select> + + <label for="empty2">Select with one empty option element</label> + <select name="empty2" id="empty2"> + <option value=""></option> + </select> + + <label for="empty3">Select with some empty option elements</label> + <select name="empty3" id="empty3"> + <option value="1">1</option> + <option value="2" selected="selected">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value=""></option> + <option value="6">6</option> + <option value="7">7</option> + <option value=""></option> + <option value="9">9</option> + </select> + + <label for="empty4">Select with empty optgroup</label> + <select id="empty4" name="empty4" class="empty4"> + <optgroup label="Scripts"></optgroup> + <optgroup label="Other files"> + <option value="somefile">Some unknown file</option> + <option value="someotherfile">Some other file</option> + </optgroup> + </select> + </fieldset> +</form> + +<div style="position: absolute; top: 1em; right: 1em;"> + Log: + <div id="log" style="height: 400px; width: 300px; overflow: auto; border: 1px solid #000;"></div> +</div> + +</body> +</html> diff --git a/themes/base/jquery.ui.base.css b/themes/base/jquery.ui.base.css index 4f642b8d1..aefa81de4 100644 --- a/themes/base/jquery.ui.base.css +++ b/themes/base/jquery.ui.base.css @@ -19,6 +19,7 @@ @import url("jquery.ui.progressbar.css"); @import url("jquery.ui.resizable.css"); @import url("jquery.ui.selectable.css"); +@import url("jquery.ui.selectmenu.css"); @import url("jquery.ui.slider.css"); @import url("jquery.ui.spinner.css"); @import url("jquery.ui.tabs.css"); diff --git a/themes/base/jquery.ui.selectmenu.css b/themes/base/jquery.ui.selectmenu.css new file mode 100644 index 000000000..c941ef0b1 --- /dev/null +++ b/themes/base/jquery.ui.selectmenu.css @@ -0,0 +1,61 @@ +/*! + * jQuery UI Selectmenu @VERSION + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/selectmenu/#theming + */ +.ui-selectmenu-menu { + padding: 0; + margin: 0; + position: absolute; + top: 0; + left: 0; + display: none; +} +.ui-selectmenu-menu .ui-menu { + overflow: auto; + /* Support: IE7 */ + overflow-x: hidden; +} +.ui-selectmenu-menu .ui-menu-item a { + padding: 0.3em 1em; +} +.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { + font-size: 1em; + font-weight: bold; + line-height: 1.5; + padding: 2px 0.4em; + margin: 0.5em 0 0 0; + height: auto; + border: 0; +} +.ui-selectmenu-open { + display: block; +} +.ui-selectmenu-button { + display: inline-block; + overflow: hidden; + position: relative; + text-decoration: none; + cursor: pointer; +} +.ui-selectmenu-button span.ui-icon { + right: 0.5em; + left: auto; + margin-top: -8px; + position: absolute; + top: 50%; +} +.ui-selectmenu-button span.ui-selectmenu-text { + text-align: left; + padding: 0.4em 2.1em 0.4em 1em; + display: block; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/ui/jquery.ui.selectmenu.js b/ui/jquery.ui.selectmenu.js new file mode 100644 index 000000000..7d7c3fb98 --- /dev/null +++ b/ui/jquery.ui.selectmenu.js @@ -0,0 +1,520 @@ +/*! + * jQuery UI Selectmenu @VERSION + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/selectmenu + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + * jquery.ui.menu.js + */ +(function( $, undefined ) { + +$.widget( "ui.selectmenu", { + version: "@VERSION", + defaultElement: "<select>", + options: { + appendTo: null, + icons: { + button: "ui-icon-triangle-1-s" + }, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + widthButton: null, + widthMenu: null, + + // callbacks + change: null, + close: null, + focus: null, + open: null, + select: null + }, + + _create: function() { + var selectmenuId = this.element.uniqueId().attr( "id" ); + this.ids = { + element: selectmenuId, + button: selectmenuId + "-button", + menu: selectmenuId + "-menu" + }; + + this._drawButton(); + this._drawMenu(); + + if ( this.options.disabled ) { + this.disable(); + } + }, + + _drawButton: function() { + var tabindex = this.element.attr( "tabindex" ); + + // Associate existing label with the new button + this.label = $( "label[for='" + this.ids.element + "']" ).attr( "for", this.ids.button ); + this._on( this.label, { + click: function( event ) { + this.button.focus(); + event.preventDefault(); + } + }); + + // Hide original select element + this.element.hide(); + + // Create button + this.button = $( "<span>", { + "class": "ui-selectmenu-button ui-widget ui-state-default ui-corner-all", + tabindex: tabindex || this.options.disabled ? -1 : 0, + id: this.ids.button, + role: "combobox", + "aria-expanded": "false", + "aria-autocomplete": "list", + "aria-owns": this.ids.menu, + "aria-haspopup": "true" + }) + .insertAfter( this.element ); + + $( "<span>", { + "class": "ui-icon " + this.options.icons.button + }).prependTo( this.button ); + + this.buttonText = $( "<span>", { + "class": "ui-selectmenu-text" + }) + .appendTo( this.button ); + + this._setText( this.buttonText, this.element.find( "option:selected" ).text() ); + this._setOption( "widthButton", this.options.widthButton ); + + this._on( this.button, this._buttonEvents ); + this._hoverable( this.button ); + this._focusable( this.button ); + }, + + _drawMenu: function() { + var that = this; + + // Create menu + this.menu = $( "<ul>", { + "aria-hidden": "true", + "aria-labelledby": this.ids.button, + id: this.ids.menu + }); + + // Wrap menu + this.menuWrap = $( "<div>", { + "class": "ui-selectmenu-menu ui-front" + }) + .append( this.menu ) + .appendTo( this._appendTo() ); + + // Initialize menu widget + this.menuInstance = this.menu.menu({ + role: "listbox", + select: function( event, ui ) { + event.preventDefault(); + that._select( ui.item.data( "ui-selectmenu-item" ), event ); + }, + focus: function( event, ui ) { + var item = ui.item.data( "ui-selectmenu-item" ); + + // Prevent inital focus from firing and checks if its a newly focused item + if ( that.focusIndex != null && item.index !== that.focusIndex ) { + that._trigger( "focus", event, { item: item } ); + if ( !that.isOpen ) { + that._select( item, event ); + } + } + that.focusIndex = item.index; + + that.button.attr( "aria-activedescendant", + that.menuItems.eq( item.index ).attr( "id" ) ); + } + }) + .menu( "instance" ); + + // Adjust menu styles to dropdown + this.menu.addClass( "ui-corner-bottom" ).removeClass( "ui-corner-all" ); + + // TODO: Can we make this cleaner? + // If not, at least update the comment to say what we're removing + // Unbind uneeded menu events + this.menuInstance._off( this.menu, "mouseleave" ); + + // Cancel the menu's collapseAll on document click + this.menuInstance._closeOnDocumentClick = function() { + return false; + }; + }, + + refresh: function() { + this._refreshMenu(); + this._setText( this.buttonText, this._getSelectedItem().text() ); + }, + + _refreshMenu: function() { + this.menu.empty(); + + var item, + options = this.element.find( "option" ); + + if ( !options.length ) { + return; + } + + this._readOptions( options ); + this._renderMenu( this.menu, this.items ); + + this.menuInstance.refresh(); + this.menuItems = this.menu.find( "li" ).not( ".ui-selectmenu-optgroup" ).find( "a" ); + + item = this._getSelectedItem(); + + // Update the menu to have the correct item focused + this.menuInstance.focus( null, item ); + this._setAria( item.data( "ui-selectmenu-item" ) ); + + // Set disabled state + this._setOption( "disabled", this.element.prop( "disabled" ) ); + }, + + open: function( event ) { + if ( this.options.disabled ) { + return; + } + + // If this is the first time the menu is being opened, render the items + if ( !this.menuItems ) { + this._refreshMenu(); + } else { + // TODO: Why is this necessary? + // Shouldn't the underlying menu always have accurate state? + this.menu.find( ".ui-state-focus" ).removeClass( "ui-state-focus" ); + this.menuInstance.focus( null, this._getSelectedItem() ); + } + + this.isOpen = true; + this._toggleAttr(); + this._resizeMenu(); + this._position(); + + this._on( this.document, this._documentClick ); + + this._trigger( "open", event ); + }, + + _position: function() { + this.menuWrap.position( $.extend( { of: this.button }, this.options.position ) ); + }, + + close: function( event ) { + if ( !this.isOpen ) { + return; + } + + this.isOpen = false; + this._toggleAttr(); + + // Check if we have an item to select + if ( this.menuItems ) { + this.menuInstance.active = this._getSelectedItem(); + } + + this._off( this.document ); + + this._trigger( "close", event ); + }, + + widget: function() { + return this.button; + }, + + menuWidget: function() { + return this.menu; + }, + + _renderMenu: function( ul, items ) { + var that = this, + currentOptgroup = ""; + + $.each( items, function( index, item ) { + if ( item.optgroup !== currentOptgroup ) { + $( "<li>", { + "class": "ui-selectmenu-optgroup" + + ( item.element.parent( "optgroup" ).attr( "disabled" ) ? + " ui-state-disabled" : + "" ), + text: item.optgroup + }) + .appendTo( ul ); + currentOptgroup = item.optgroup; + } + that._renderItemData( ul, item ); + }); + }, + + _renderItemData: function( ul, item ) { + return this._renderItem( ul, item ).data( "ui-selectmenu-item", item ); + }, + + _renderItem: function( ul, item ) { + var li = $( "<li>" ), + a = $( "<a>", { href: "#" }); + + if ( item.disabled ) { + li.addClass( "ui-state-disabled" ); + } + this._setText( a, item.label ); + + return li.append( a ).appendTo( ul ); + }, + + _setText: function( element, value ) { + if ( value ) { + element.text( value ); + } else { + element.html( " " ); + } + }, + + _move: function( direction, event ) { + if ( direction === "first" || direction === "last" ) { + // Set focus manually for first or last item + this.menu.menu( "focus", event, this.menuItems[ direction ]() ); + } else { + if ( direction === "previous" && this.menu.menu( "isFirstItem" ) || + direction === "next" && this.menu.menu( "isLastItem" ) ) { + return; + } + + // Move to and focus next or prev item + this.menu.menu( direction, event ); + } + }, + + _getSelectedItem: function() { + return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" ); + }, + + _toggle: function( event ) { + if ( this.isOpen ) { + this.close( event ); + } else { + this.open( event ); + } + }, + + _documentClick: { + click: function( event ) { + if ( this.isOpen && !$( event.target ).closest( "li.ui-state-disabled, li.ui-selectmenu-optgroup, #" + this.ids.button ).length ) { + this.close( event ); + } + } + }, + + _buttonEvents: { + focusin: function() { + // Delay rendering the menu items until the button receives focus + if ( !this.menuItems ) { + this._refreshMenu(); + } + this._off( this.button, "focusin" ); + }, + click: "_toggle", + keydown: function( event ) { + var preventDefault = true; + switch ( event.keyCode ) { + case $.ui.keyCode.TAB: + case $.ui.keyCode.ESCAPE: + this.close( event ); + preventDefault = false; + break; + case $.ui.keyCode.ENTER: + if ( this.isOpen ) { + this.menuInstance.select( event ); + } + break; + case $.ui.keyCode.UP: + if ( event.altKey ) { + this._toggle( event ); + } else { + this._move( "previous", event ); + } + break; + case $.ui.keyCode.DOWN: + if ( event.altKey ) { + this._toggle( event ); + } else { + this._move( "next", event ); + } + break; + case $.ui.keyCode.SPACE: + if ( this.isOpen ) { + this.menuInstance.select( event ); + } else { + this._toggle( event ); + } + break; + case $.ui.keyCode.LEFT: + this._move( "previous", event ); + break; + case $.ui.keyCode.RIGHT: + this._move( "next", event ); + break; + case $.ui.keyCode.HOME: + case $.ui.keyCode.PAGE_UP: + this._move( "first", event ); + break; + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_DOWN: + this._move( "last", event ); + break; + default: + this.menu.trigger( event ); + preventDefault = false; + } + + if ( preventDefault ) { + event.preventDefault(); + } + } + }, + + _select: function( item, event ) { + var oldIndex = this.element[ 0 ].selectedIndex; + + // Change native select element + this.element[ 0 ].selectedIndex = item.index; + this._setText( this.buttonText, item.label ); + this._setAria( item ); + this._trigger( "select", event, { item: item } ); + + if ( item.index !== oldIndex ) { + this._trigger( "change", event, { item: item } ); + } + + this.close( event ); + }, + + _setAria: function( item ) { + var link = this.menuItems.eq( item.index ), + id = link.attr( "id" ); + + this.button.attr({ + "aria-labelledby": id, + "aria-activedescendant": id + }); + this.menu.attr( "aria-activedescendant", id ); + }, + + _setOption: function( key, value ) { + if ( key === "icons" ) { + this.button.find( "span.ui-icon" ) + .removeClass( this.options.icons.button ) + .addClass( value.button ); + } + + this._super( key, value ); + + if ( key === "appendTo" ) { + this.menuWrap.appendTo( this._appendTo() ); + } + if ( key === "disabled" ) { + this.menuInstance.option( "disabled", value ); + this.button + .toggleClass( "ui-state-disabled", value ) + .attr( "aria-disabled", value ); + + this.element.prop( "disabled", value ); + if ( value ) { + this.button.attr( "tabindex", -1 ); + this.close(); + } else { + this.button.attr( "tabindex", 0 ); + } + } + if ( key === "widthButton" ) { + if ( !value ) { + value = this.element.outerWidth(); + } + this.button.outerWidth( value ); + } + }, + + _appendTo: function() { + var element = this.options.appendTo; + + if ( element ) { + element = element.jquery || element.nodeType ? + $( element ) : + this.document.find( element ).eq( 0 ); + } + + if ( !element ) { + element = this.element.closest( ".ui-front" ); + } + + if ( !element.length ) { + element = this.document[ 0 ].body; + } + + return element; + }, + + _toggleAttr: function(){ + this.button + .toggleClass( "ui-corner-top", this.isOpen ) + .toggleClass( "ui-corner-all", !this.isOpen ); + this.menuWrap.toggleClass( "ui-selectmenu-open", this.isOpen ); + this.menu.attr( "aria-hidden", !this.isOpen ); + this.button.attr( "aria-expanded", this.isOpen ); + }, + + _resizeMenu: function() { + this.menu.outerWidth( this.options.widthMenu || Math.max( + this.button.outerWidth(), + this.menu.width( "" ).outerWidth() + ) ); + }, + + _getCreateOptions: function() { + return { disabled: this.element.prop( "disabled" ) }; + }, + + _readOptions: function( options ) { + var data = []; + options.each( function( index, item ) { + var option = $( item ), + optgroup = option.parent( "optgroup" ); + data.push({ + element: option, + index: index, + value: option.attr( "value" ), + label: option.text(), + optgroup: optgroup.attr( "label" ) || "", + disabled: optgroup.attr( "disabled" ) || option.attr( "disabled" ) + }); + }); + this.items = data; + }, + + _destroy: function() { + this.menuWrap.remove(); + this.button.remove(); + this.element.show(); + this.element.removeUniqueId(); + this.label.attr( "for", this.ids.element ); + } +}); + +}( jQuery )); |