]> source.dussan.org Git - jquery-ui.git/commitdiff
Promote menubar and popup to their own top level files
authorJörn Zaefferer <joern.zaefferer@gmail.com>
Sat, 7 May 2011 14:22:04 +0000 (16:22 +0200)
committerJörn Zaefferer <joern.zaefferer@gmail.com>
Sat, 7 May 2011 14:22:04 +0000 (16:22 +0200)
demos/tooltip/video-player.html
tests/visual/menu/menubar.html
tests/visual/menu/menubar.js [deleted file]
tests/visual/menu/popup.html
tests/visual/menu/popup.js [deleted file]
ui/jquery.ui.menubar.js [new file with mode: 0644]
ui/jquery.ui.popup.js [new file with mode: 0644]

index 70a83b626c07890375781ad0a4ce8248d30738d1..56003ab5ad185de09813edb1415ec54bf828d535 100644 (file)
@@ -10,9 +10,9 @@
        <script type="text/javascript" src="../../ui/jquery.ui.tooltip.js"></script>
        <script type="text/javascript" src="../../ui/jquery.ui.button.js"></script>
        <script type="text/javascript" src="../../ui/jquery.ui.menu.js"></script>
+       <script type="text/javascript" src="../../ui/jquery.ui.popup.js"></script>
        <script type="text/javascript" src="../../ui/jquery.effects.core.js"></script>
        <script type="text/javascript" src="../../ui/jquery.effects.blind.js"></script>
-       <script type="text/javascript" src="../../tests/visual/menu/popup.js"></script>
        <link type="text/css" href="../demos.css" rel="stylesheet" />
        <script type="text/javascript">
        $(function() {
index d2185e97c12716e53ca5fbbdfa9a9f46e1f64e6f..3d7a3e59c92f52c12578f82f5d31407fbb9e0222 100644 (file)
@@ -10,7 +10,7 @@
        <script type="text/javascript" src="../../../ui/jquery.ui.position.js"></script>
        <script type="text/javascript" src="../../../ui/jquery.ui.button.js"></script>
        <script type="text/javascript" src="../../../ui/jquery.ui.menu.js"></script>
-       <script type="text/javascript" src="menubar.js"></script>
+       <script type="text/javascript" src="../../../ui/jquery.ui.menubar.js"></script>
        <!--
        <script type="text/javascript" src="http://jqueryui.com/themeroller/themeswitchertool/"></script>
        -->
diff --git a/tests/visual/menu/menubar.js b/tests/visual/menu/menubar.js
deleted file mode 100644 (file)
index b9abacb..0000000
+++ /dev/null
@@ -1,262 +0,0 @@
-/*
- * jQuery UI menubar
- * 
- * TODO move to jquery.ui.menubar.js
- */
-(function( $ ) {
-
-// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
-// there has to be just one item that has tabindex
-$.widget( "ui.menubar", {
-   options: {
-      buttons: false,
-      menuIcon: false
-   },
-       _create: function() {
-               var that = this;
-               var items = this.items = this.element.children( "li" )
-                       .addClass( "ui-menubar-item" )
-                       .attr( "role", "presentation" )
-                       .children( "button, a" );
-               // let only the first item receive focus
-               items.slice(1).attr( "tabIndex", -1 );
-
-               this.element
-                       .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
-                       .attr( "role", "menubar" );
-               this._focusable( items );
-               this._hoverable( items );
-               items.next( "ul" )
-                       .menu({
-                               select: function( event, ui ) {
-                                       ui.item.parents( "ul.ui-menu:last" ).hide();
-                                       that._trigger( "select", event, ui );
-                                       that._close();
-                                       // TODO what is this targetting? there's probably a better way to access it
-                                       $(event.target).prev().focus();
-                               }
-                       })
-                       .hide()
-                       .attr( "aria-hidden", "true" )
-                       .attr( "aria-expanded", "false" )
-                       .bind( "keydown.menubar", function( event ) {
-                               var menu = $( this );
-                               if ( menu.is( ":hidden" ) )
-                                       return;
-                               switch ( event.keyCode ) {
-                               case $.ui.keyCode.LEFT:
-                                       that._left( event );
-                                       event.preventDefault();
-                                       break;
-                               case $.ui.keyCode.RIGHT:
-                                       that._right( event );
-                                       event.preventDefault();
-                                       break;
-                               };
-                       });
-               items.each(function() {
-                       var input = $(this),
-                               // TODO menu var is only used on two places, doesn't quite justify the .each
-                               menu = input.next( "ul" );
-                       
-                       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._open( event, menu );
-                               }
-                       })
-                       .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._prev( event, $( this ) );
-                                       event.preventDefault();
-                                       break;
-                               case $.ui.keyCode.RIGHT:
-                                       that._next( event, $( this ) );
-                                       event.preventDefault();
-                                       break;
-                               }
-                       })
-                       .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
-                       .attr( "role", "menuitem" )
-                       .attr( "aria-haspopup", "true" )
-                       .wrapInner( "<span class='ui-button-text'></span>" );
-
-                       // TODO review if these options 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 ( !that.options.buttons ) {
-                               // TODO ui-menubar-link is added above, not needed here?
-                               input.addClass( "ui-menubar-link" ).removeClass( "ui-state-default" );
-                       };                      
-                       
-               });
-               that._bind( {
-                       keydown: function( event ) {
-                               if ( event.keyCode == $.ui.keyCode.ESCAPE && that.active && that.active.menu( "left", event ) !== true ) {
-                                       var active = that.active;
-                                       that.active.blur();
-                                       that._close( event );
-                                       active.prev().focus();
-                               }
-                       },
-                       focusin: function( event ) {
-                               clearTimeout( that.closeTimer );
-                       },
-                       focusout: function( event ) {
-                               that.closeTimer = setTimeout( function() {
-                                       that._close( event );
-                               }, 100);
-                       }
-               });
-       },
-       
-       _destroy : function() {
-               var items = this.element.children( "li" )
-                       .removeClass( "ui-menubar-item" )
-                       .removeAttr( "role", "presentation" )
-                       .children( "button, a" );
-               
-               this.element
-                       .removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
-                       .removeAttr( "role", "menubar" )
-                       .unbind( ".menubar" );
-               
-               items
-                       .unbind( ".menubar" )
-                       .removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
-                       .removeAttr( "role", "menuitem" )
-                       .removeAttr( "aria-haspopup", "true" )
-                       // TODO unwrap?
-                       .children( "span.ui-button-text" ).each(function( i, e ) {
-                               var item = $( this );
-                               item.parent().html( item.html() );
-                       })
-                       .end()
-                       .children( ".ui-icon" ).remove();
-
-               this.element.find( ":ui-menu" )
-                       .menu( "destroy" )
-                       .show()
-                       .removeAttr( "aria-hidden", "true" )
-                       .removeAttr( "aria-expanded", "false" )
-                       .removeAttr( "tabindex" )
-                       .unbind( ".menubar" );
-       },
-       
-       _close: function() {
-               if ( !this.active || !this.active.length )
-                       return;
-               this.active
-                       .menu( "closeAll" )
-                       .hide()
-                       .attr( "aria-hidden", "true" )
-                       .attr( "aria-expanded", "false" );
-               this.active
-                       .prev()
-                       .removeClass( "ui-state-active" )
-                       .removeAttr( "tabIndex" );
-               this.active = null;
-               this.open = false;
-       },
-       
-       _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 ) {
-                       this.active
-                               .menu( "closeAll" )
-                               .hide()
-                               .attr( "aria-hidden", "true" )
-                               .attr( "aria-expanded", "false" );
-                       this.active
-                               .prev()
-                               .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 );
-               this.active = menu
-                       .show()
-                       .position( {
-                               my: "left top",
-                               at: "left bottom",
-                               of: button
-                       })
-                       .removeAttr( "aria-hidden" )
-                       .attr( "aria-expanded", "true" )
-                       .menu("focus", event, menu.children( "li" ).first() )
-                       // TODO need a comment here why both events are triggered
-                       .focus()
-                       .focusin();
-               this.open = true;
-       },
-       
-       // TODO refactor this and the next three methods
-       _prev: function( event, button ) {
-               button.attr( "tabIndex", -1 );
-               var prev = button.parent().prevAll( "li" ).children( ".ui-button" ).eq( 0 );
-               if ( prev.length ) {
-                       prev.removeAttr( "tabIndex" )[0].focus();
-               } else {
-                       var lastItem = this.element.children( "li:last" ).children( ".ui-button:last" );
-                       lastItem.removeAttr( "tabIndex" )[0].focus();
-               }
-       },
-       
-       _next: function( event, button ) {
-               button.attr( "tabIndex", -1 );
-               var next = button.parent().nextAll( "li" ).children( ".ui-button" ).eq( 0 );
-               if ( next.length ) {
-                       next.removeAttr( "tabIndex")[0].focus();
-               } else {
-                       var firstItem = this.element.children( "li:first" ).children( ".ui-button:first" );
-                       firstItem.removeAttr( "tabIndex" )[0].focus();
-               }
-       },
-
-       // TODO rename to parent
-       _left: function( event ) {
-               var prev = this.active.parent().prevAll( "li:eq(0)" ).children( ".ui-menu" ).eq( 0 );
-               if ( prev.length ) {
-                       this._open( event, prev );
-               } else {
-                       var lastItem = this.element.children( "li:last" ).children( ".ui-menu:first" );
-                       this._open( event, lastItem );
-               }
-       },
-       
-       // TODO rename to child (or something like that)
-       _right: function( event ) {
-               var next = this.active.parent().nextAll( "li:eq(0)" ).children( ".ui-menu" ).eq( 0 );
-               if ( next.length ) {
-                       this._open( event, next );
-               } else {
-                       var firstItem = this.element.children( "li:first" ).children( ".ui-menu:first" );
-                       this._open( event, firstItem );
-               }
-       }
-});
-
-}( jQuery ));
index 2a712b00ab390a430893a5b017bc262c99f93c77..de27208b48e5b6f9de7840523f5703df3608eeb2 100644 (file)
@@ -10,7 +10,7 @@
        <script type="text/javascript" src="../../../ui/jquery.ui.position.js"></script>
        <script type="text/javascript" src="../../../ui/jquery.ui.button.js"></script>
        <script type="text/javascript" src="../../../ui/jquery.ui.menu.js"></script>
-       <script type="text/javascript" src="popup.js"></script>
+       <script type="text/javascript" src="../../../ui/jquery.ui.popup.js"></script>
        <!--
        <script type="text/javascript" src="http://jqueryui.com/themeroller/themeswitchertool/"></script>
        -->
diff --git a/tests/visual/menu/popup.js b/tests/visual/menu/popup.js
deleted file mode 100644 (file)
index 87a4807..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * jQuery UI popup utility
- */
-(function($) {
-       
-var idIncrement = 0;
-
-$.widget( "ui.popup", {
-       options: {
-               position: {
-                       my: "left top",
-                       at: "left bottom"
-               }
-       },
-       _create: function() {
-               if ( !this.options.trigger ) {
-                       this.options.trigger = this.element.prev();
-               }
-               
-               if ( !this.element.attr( "id" ) ) {
-                       this.element.attr( "id", "ui-popup-" + idIncrement++ );
-                       this.generatedId = true;
-               }
-               
-               if ( !this.element.attr( "role" ) ) {
-                       // TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
-                       this.element.attr( "role", "tooltip" );
-                       this.generatedRole = true;
-               }
-               
-               this.options.trigger
-                       .attr( "aria-haspopup", true )
-                       .attr( "aria-owns", this.element.attr( "id" ) );
-               
-               this.element
-                       .addClass("ui-popup")
-               this.close();
-
-               this._bind(this.options.trigger, {
-                       keydown: function( event ) {
-                               // prevent space-to-open to scroll the page, only hapens for anchor ui.button
-                               if ( this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE) {
-                                       event.preventDefault()
-                               }
-                               // TODO handle SPACE to open popup? only when not handled by ui.button
-                               if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is("a:not(:ui-button)") ) {
-                                       this.options.trigger.trigger( "click", event );
-                               }
-                               // translate keydown to click
-                               // opens popup and let's tooltip hide itself
-                               if ( event.keyCode == $.ui.keyCode.DOWN ) {
-                                       this.options.trigger.trigger( "click", event );
-                               }
-                       },
-                       click: function( event ) {
-                               event.preventDefault();
-                               if (this.isOpen) {
-                                       // let it propagate to close
-                                       return;
-                               }
-                               var that = this;
-                               clearTimeout( this.closeTimer );
-                               setTimeout(function() {
-                                       that.open( event );
-                               }, 1);
-                       }
-               });
-               
-               this._bind(this.element, {
-                       // TODO use focusout so that element itself doesn't need to be focussable
-                       blur: function( event ) {
-                               var that = this;
-                               // use a timer to allow click to clear it and letting that
-                               // handle the closing instead of opening again
-                               that.closeTimer = setTimeout( function() {
-                                       that.close( event );
-                               }, 100);
-                       }
-               });
-
-               this._bind({
-                       // TODO only triggerd on element if it can receive focus
-                       // bind to document instead?
-                       // either element itself or a child should be focusable
-                       keyup: function( event ) {
-                               if (event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" )) {
-                                       this.close( event );
-                                       // TODO move this to close()? would allow menu.select to call popup.close, and get focus back to trigger
-                                       this.options.trigger.focus();
-                               }
-                       }
-               });
-               
-               this._bind(document, {
-                       click: function( event ) {
-                               if (this.isOpen && !$(event.target).closest(".ui-popup").length) {
-                                       this.close( event );
-                               }
-                       }
-               })
-       },
-       
-       _destroy: function() {
-               this.element
-                       .show()
-                       .removeClass( "ui-popup" )
-                       .removeAttr( "aria-hidden" )
-                       .removeAttr( "aria-expanded" );
-
-               this.options.trigger
-                       .removeAttr( "aria-haspopup" )
-                       .removeAttr( "aria-owns" );
-                       
-               if ( this.generatedId ) {
-                       this.element.removeAttr( "id" );
-               }
-               if ( this.generatedRole ) {
-                       this.element.removeAttr( "role" );
-               }
-       },
-       
-       open: function( event ) {
-               var position = $.extend( {}, {
-                       of: this.options.trigger
-               }, this.options.position );
-
-               this.element
-                       .show()
-                       .attr( "aria-hidden", false )
-                       .attr( "aria-expanded", true )
-                       .position( position )
-                       // TODO find a focussable child, otherwise put focus on element, add tabIndex=0 if not focussable
-                       .focus();
-
-               if (this.element.is(":ui-menu")) {
-                       this.element.menu("focus", event, this.element.children( "li" ).first() );
-               }
-
-               // take trigger out of tab order to allow shift-tab to skip trigger
-               this.options.trigger.attr("tabindex", -1);
-
-               this.isOpen = true;
-               this._trigger( "open", event );
-       },
-
-       close: function( event ) {
-               this.element
-                       .hide()
-                       .attr( "aria-hidden", true )
-                       .attr( "aria-expanded", false );
-
-               this.options.trigger.attr("tabindex", 0);
-
-               this.isOpen = false;
-               this._trigger( "close", event );
-       }
-       
-       
-});
-
-}(jQuery));
diff --git a/ui/jquery.ui.menubar.js b/ui/jquery.ui.menubar.js
new file mode 100644 (file)
index 0000000..2879d07
--- /dev/null
@@ -0,0 +1,272 @@
+/*
+ * jQuery UI Menubar @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menubar
+ *
+ * Depends:
+ *     jquery.ui.core.js
+ *     jquery.ui.widget.js
+ *     jquery.ui.position.js
+ *     jquery.ui.menu.js
+ */
+(function( $ ) {
+
+// TODO when mixing clicking menus and keyboard navigation, focus handling is broken
+// there has to be just one item that has tabindex
+$.widget( "ui.menubar", {
+   options: {
+      buttons: false,
+      menuIcon: false
+   },
+       _create: function() {
+               var that = this;
+               var items = this.items = this.element.children( "li" )
+                       .addClass( "ui-menubar-item" )
+                       .attr( "role", "presentation" )
+                       .children( "button, a" );
+               // let only the first item receive focus
+               items.slice(1).attr( "tabIndex", -1 );
+
+               this.element
+                       .addClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+                       .attr( "role", "menubar" );
+               this._focusable( items );
+               this._hoverable( items );
+               items.next( "ul" )
+                       .menu({
+                               select: function( event, ui ) {
+                                       ui.item.parents( "ul.ui-menu:last" ).hide();
+                                       that._trigger( "select", event, ui );
+                                       that._close();
+                                       // TODO what is this targetting? there's probably a better way to access it
+                                       $(event.target).prev().focus();
+                               }
+                       })
+                       .hide()
+                       .attr( "aria-hidden", "true" )
+                       .attr( "aria-expanded", "false" )
+                       .bind( "keydown.menubar", function( event ) {
+                               var menu = $( this );
+                               if ( menu.is( ":hidden" ) )
+                                       return;
+                               switch ( event.keyCode ) {
+                               case $.ui.keyCode.LEFT:
+                                       that._left( event );
+                                       event.preventDefault();
+                                       break;
+                               case $.ui.keyCode.RIGHT:
+                                       that._right( event );
+                                       event.preventDefault();
+                                       break;
+                               };
+                       });
+               items.each(function() {
+                       var input = $(this),
+                               // TODO menu var is only used on two places, doesn't quite justify the .each
+                               menu = input.next( "ul" );
+                       
+                       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._open( event, menu );
+                               }
+                       })
+                       .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._prev( event, $( this ) );
+                                       event.preventDefault();
+                                       break;
+                               case $.ui.keyCode.RIGHT:
+                                       that._next( event, $( this ) );
+                                       event.preventDefault();
+                                       break;
+                               }
+                       })
+                       .addClass( "ui-button ui-widget ui-button-text-only ui-menubar-link" )
+                       .attr( "role", "menuitem" )
+                       .attr( "aria-haspopup", "true" )
+                       .wrapInner( "<span class='ui-button-text'></span>" );
+
+                       // TODO review if these options 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 ( !that.options.buttons ) {
+                               // TODO ui-menubar-link is added above, not needed here?
+                               input.addClass( "ui-menubar-link" ).removeClass( "ui-state-default" );
+                       };                      
+                       
+               });
+               that._bind( {
+                       keydown: function( event ) {
+                               if ( event.keyCode == $.ui.keyCode.ESCAPE && that.active && that.active.menu( "left", event ) !== true ) {
+                                       var active = that.active;
+                                       that.active.blur();
+                                       that._close( event );
+                                       active.prev().focus();
+                               }
+                       },
+                       focusin: function( event ) {
+                               clearTimeout( that.closeTimer );
+                       },
+                       focusout: function( event ) {
+                               that.closeTimer = setTimeout( function() {
+                                       that._close( event );
+                               }, 100);
+                       }
+               });
+       },
+       
+       _destroy : function() {
+               var items = this.element.children( "li" )
+                       .removeClass( "ui-menubar-item" )
+                       .removeAttr( "role", "presentation" )
+                       .children( "button, a" );
+               
+               this.element
+                       .removeClass( "ui-menubar ui-widget-header ui-helper-clearfix" )
+                       .removeAttr( "role", "menubar" )
+                       .unbind( ".menubar" );
+               
+               items
+                       .unbind( ".menubar" )
+                       .removeClass( "ui-button ui-widget ui-button-text-only ui-menubar-link ui-state-default" )
+                       .removeAttr( "role", "menuitem" )
+                       .removeAttr( "aria-haspopup", "true" )
+                       // TODO unwrap?
+                       .children( "span.ui-button-text" ).each(function( i, e ) {
+                               var item = $( this );
+                               item.parent().html( item.html() );
+                       })
+                       .end()
+                       .children( ".ui-icon" ).remove();
+
+               this.element.find( ":ui-menu" )
+                       .menu( "destroy" )
+                       .show()
+                       .removeAttr( "aria-hidden", "true" )
+                       .removeAttr( "aria-expanded", "false" )
+                       .removeAttr( "tabindex" )
+                       .unbind( ".menubar" );
+       },
+       
+       _close: function() {
+               if ( !this.active || !this.active.length )
+                       return;
+               this.active
+                       .menu( "closeAll" )
+                       .hide()
+                       .attr( "aria-hidden", "true" )
+                       .attr( "aria-expanded", "false" );
+               this.active
+                       .prev()
+                       .removeClass( "ui-state-active" )
+                       .removeAttr( "tabIndex" );
+               this.active = null;
+               this.open = false;
+       },
+       
+       _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 ) {
+                       this.active
+                               .menu( "closeAll" )
+                               .hide()
+                               .attr( "aria-hidden", "true" )
+                               .attr( "aria-expanded", "false" );
+                       this.active
+                               .prev()
+                               .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 );
+               this.active = menu
+                       .show()
+                       .position( {
+                               my: "left top",
+                               at: "left bottom",
+                               of: button
+                       })
+                       .removeAttr( "aria-hidden" )
+                       .attr( "aria-expanded", "true" )
+                       .menu("focus", event, menu.children( "li" ).first() )
+                       // TODO need a comment here why both events are triggered
+                       .focus()
+                       .focusin();
+               this.open = true;
+       },
+       
+       // TODO refactor this and the next three methods
+       _prev: function( event, button ) {
+               button.attr( "tabIndex", -1 );
+               var prev = button.parent().prevAll( "li" ).children( ".ui-button" ).eq( 0 );
+               if ( prev.length ) {
+                       prev.removeAttr( "tabIndex" )[0].focus();
+               } else {
+                       var lastItem = this.element.children( "li:last" ).children( ".ui-button:last" );
+                       lastItem.removeAttr( "tabIndex" )[0].focus();
+               }
+       },
+       
+       _next: function( event, button ) {
+               button.attr( "tabIndex", -1 );
+               var next = button.parent().nextAll( "li" ).children( ".ui-button" ).eq( 0 );
+               if ( next.length ) {
+                       next.removeAttr( "tabIndex")[0].focus();
+               } else {
+                       var firstItem = this.element.children( "li:first" ).children( ".ui-button:first" );
+                       firstItem.removeAttr( "tabIndex" )[0].focus();
+               }
+       },
+
+       // TODO rename to parent
+       _left: function( event ) {
+               var prev = this.active.parent().prevAll( "li:eq(0)" ).children( ".ui-menu" ).eq( 0 );
+               if ( prev.length ) {
+                       this._open( event, prev );
+               } else {
+                       var lastItem = this.element.children( "li:last" ).children( ".ui-menu:first" );
+                       this._open( event, lastItem );
+               }
+       },
+       
+       // TODO rename to child (or something like that)
+       _right: function( event ) {
+               var next = this.active.parent().nextAll( "li:eq(0)" ).children( ".ui-menu" ).eq( 0 );
+               if ( next.length ) {
+                       this._open( event, next );
+               } else {
+                       var firstItem = this.element.children( "li:first" ).children( ".ui-menu:first" );
+                       this._open( event, firstItem );
+               }
+       }
+});
+
+}( jQuery ));
diff --git a/ui/jquery.ui.popup.js b/ui/jquery.ui.popup.js
new file mode 100644 (file)
index 0000000..784e5c6
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * jQuery UI Popup @VERSION
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Popup
+ *
+ * Depends:
+ *     jquery.ui.core.js
+ *     jquery.ui.widget.js
+ *     jquery.ui.position.js
+ */
+(function($) {
+       
+var idIncrement = 0;
+
+$.widget( "ui.popup", {
+       options: {
+               position: {
+                       my: "left top",
+                       at: "left bottom"
+               }
+       },
+       _create: function() {
+               if ( !this.options.trigger ) {
+                       this.options.trigger = this.element.prev();
+               }
+               
+               if ( !this.element.attr( "id" ) ) {
+                       this.element.attr( "id", "ui-popup-" + idIncrement++ );
+                       this.generatedId = true;
+               }
+               
+               if ( !this.element.attr( "role" ) ) {
+                       // TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
+                       this.element.attr( "role", "tooltip" );
+                       this.generatedRole = true;
+               }
+               
+               this.options.trigger
+                       .attr( "aria-haspopup", true )
+                       .attr( "aria-owns", this.element.attr( "id" ) );
+               
+               this.element
+                       .addClass("ui-popup")
+               this.close();
+
+               this._bind(this.options.trigger, {
+                       keydown: function( event ) {
+                               // prevent space-to-open to scroll the page, only hapens for anchor ui.button
+                               if ( this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE) {
+                                       event.preventDefault()
+                               }
+                               // TODO handle SPACE to open popup? only when not handled by ui.button
+                               if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is("a:not(:ui-button)") ) {
+                                       this.options.trigger.trigger( "click", event );
+                               }
+                               // translate keydown to click
+                               // opens popup and let's tooltip hide itself
+                               if ( event.keyCode == $.ui.keyCode.DOWN ) {
+                                       this.options.trigger.trigger( "click", event );
+                               }
+                       },
+                       click: function( event ) {
+                               event.preventDefault();
+                               if (this.isOpen) {
+                                       // let it propagate to close
+                                       return;
+                               }
+                               var that = this;
+                               clearTimeout( this.closeTimer );
+                               setTimeout(function() {
+                                       that.open( event );
+                               }, 1);
+                       }
+               });
+               
+               this._bind(this.element, {
+                       // TODO use focusout so that element itself doesn't need to be focussable
+                       blur: function( event ) {
+                               var that = this;
+                               // use a timer to allow click to clear it and letting that
+                               // handle the closing instead of opening again
+                               that.closeTimer = setTimeout( function() {
+                                       that.close( event );
+                               }, 100);
+                       }
+               });
+
+               this._bind({
+                       // TODO only triggerd on element if it can receive focus
+                       // bind to document instead?
+                       // either element itself or a child should be focusable
+                       keyup: function( event ) {
+                               if (event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" )) {
+                                       this.close( event );
+                                       // TODO move this to close()? would allow menu.select to call popup.close, and get focus back to trigger
+                                       this.options.trigger.focus();
+                               }
+                       }
+               });
+               
+               this._bind(document, {
+                       click: function( event ) {
+                               if (this.isOpen && !$(event.target).closest(".ui-popup").length) {
+                                       this.close( event );
+                               }
+                       }
+               })
+       },
+       
+       _destroy: function() {
+               this.element
+                       .show()
+                       .removeClass( "ui-popup" )
+                       .removeAttr( "aria-hidden" )
+                       .removeAttr( "aria-expanded" );
+
+               this.options.trigger
+                       .removeAttr( "aria-haspopup" )
+                       .removeAttr( "aria-owns" );
+                       
+               if ( this.generatedId ) {
+                       this.element.removeAttr( "id" );
+               }
+               if ( this.generatedRole ) {
+                       this.element.removeAttr( "role" );
+               }
+       },
+       
+       open: function( event ) {
+               var position = $.extend( {}, {
+                       of: this.options.trigger
+               }, this.options.position );
+
+               this.element
+                       .show()
+                       .attr( "aria-hidden", false )
+                       .attr( "aria-expanded", true )
+                       .position( position )
+                       // TODO find a focussable child, otherwise put focus on element, add tabIndex=0 if not focussable
+                       .focus();
+
+               if (this.element.is(":ui-menu")) {
+                       this.element.menu("focus", event, this.element.children( "li" ).first() );
+               }
+
+               // take trigger out of tab order to allow shift-tab to skip trigger
+               this.options.trigger.attr("tabindex", -1);
+
+               this.isOpen = true;
+               this._trigger( "open", event );
+       },
+
+       close: function( event ) {
+               this.element
+                       .hide()
+                       .attr( "aria-hidden", true )
+                       .attr( "aria-expanded", false );
+
+               this.options.trigger.attr("tabindex", 0);
+
+               this.isOpen = false;
+               this._trigger( "close", event );
+       }
+       
+       
+});
+
+}(jQuery));