]> source.dussan.org Git - jquery-ui.git/commitdiff
Rewrite popup/menu interaction to make popup managed by menu (adds trigger option...
authorHans Hillen <hans.hillen@gmail.com>
Wed, 19 Oct 2011 09:41:33 +0000 (11:41 +0200)
committerJörn Zaefferer <joern.zaefferer@gmail.com>
Wed, 19 Oct 2011 10:05:37 +0000 (12:05 +0200)
demos/menu/contextmenu.html
demos/popup/popup-menu.html
ui/jquery.ui.menu.js
ui/jquery.ui.popup.js

index e031c2c4c8885234b9ff65e088f96614eccd32b1..961cdd450060e5735cfd329e76b4149967e271f1 100644 (file)
        <link href="../demos.css" rel="stylesheet" />
        <script>
        $(function() {
-               $(".demo button").button({
+               var btn = $(".demo button").button({
                        icons: {
                                primary: "ui-icon-home",
                                secondary: "ui-icon-triangle-1-s"
                        }
-               }).next().menu({
+               });
+               $("#cities").menu({
                        select: function(event, ui) {
-                               $(this).hide();
                                $("#log").append("<div>Selected " + ui.item.text() + "</div>");
-                       }
-               }).popup();
+                       },
+                       trigger : btn});
        });
        </script>
        <style>
@@ -36,7 +36,7 @@
 <div class="demo">
 
        <button>Select a city</button>
-       <ul>
+       <ul id="cities">
                <li><a href="#Amsterdam">Amsterdam</a></li>
                <li><a href="#Anaheim">Anaheim</a></li>
                <li><a href="#Cologne">Cologne</a></li>
index ddcef49574ae9f15d74543c1d729bf68d2f0187b..8041e2fa3aa2a7bb0207d1033f691ced4eead48d 100644 (file)
                function log( msg ) {
                        $( "<div/>" ).text( msg ).appendTo( "#log" );
                }
-               var selected = {
-                       select: function( event, ui ) {
-                               log( "Selected: " + ui.item.text() );
-                               $(this).popup("close");
-                       }
-               };
+               var selected = function( event, ui ) {
+                       log( "Selected: " + ui.item.text() );
+                       $(this).popup( "close" );
+               }
+
+               $("#button1").button().next().menu( {select: selected, trigger: $("#button1")} );
 
-               $("#button1").button()
-                       .next().menu(selected).popup();
-               
                $( "#rerun" )
                        .button()
                        .click(function() {
                                        }
                                })
                                .next()
-                                       .menu(selected)
-                                       .popup({
-                                               trigger: $("#select")
-                                       })
+                                       .menu( {select: selected, trigger: $("#select")} )
                                .parent()
                                        .buttonset({
                                                items: "button"
@@ -69,8 +63,8 @@
                <li><a href="#">Utrecht</a></li>
                <li><a href="#">Zurich</a></li>
        </ul>
-       
-       
+
+
        <div>
                <div>
                        <button id="rerun">Run last action</button>
index 944e47b7cd8b18b703c7a614cabee2d31908f449..f2f758d96e0939c6df72ae9f80a59f407c798b5a 100644 (file)
@@ -24,7 +24,8 @@ $.widget( "ui.menu", {
                position: {
                        my: "left top",
                        at: "right top"
-               }
+               },
+               trigger: null
        },
        _create: function() {
                this.activeMenu = this.element;
@@ -39,6 +40,16 @@ $.widget( "ui.menu", {
                                id: this.menuId,
                                role: "menu"
                        })
+                       // Prevent focus from sticking to links inside menu after clicking
+                       // them (focus should always stay on UL during navigation).
+                       // If the link is clicked, redirect focus to the menu.
+                       // TODO move to _bind below
+                       .bind( "mousedown.menu", function( event ) {
+                               if ( $( event.target).is( "a" ) ) {
+                                       event.preventDefault();
+                                       $( this ).focus( 1 );
+                               }
+                       })
                        // need to catch all clicks on disabled menu
                        // not possible through _bind
                        .bind( "click.menu", $.proxy( function( event ) {
@@ -203,10 +214,24 @@ $.widget( "ui.menu", {
                                }
                        }
                });
+
+               if ( this.options.trigger ) {
+                       this.element.popup({
+                               trigger: this.options.trigger,
+                               managed: true,
+                               focusPopup: $.proxy( function( event, ui ) {
+                                       this.focus( event, this.element.children( ".ui-menu-item" ).first() );
+                                       this.element.focus( 1 );
+                               }, this)
+                       });
+               }
        },
 
        _destroy: function() {
                //destroy (sub)menus
+               if ( this.options.trigger ) {
+                       this.element.popup( "destroy" );
+               }
                this.element
                        .removeAttr( "aria-activedescendant" )
                        .find( ".ui-menu" )
@@ -508,6 +533,10 @@ $.widget( "ui.menu", {
                        item: this.active
                };
                this.collapseAll( event, true );
+               if ( this.options.trigger ) {
+                       $( this.options.trigger ).focus( 1 );
+                       this.element.popup( "close" );
+               }
                this._trigger( "select", event, ui );
        }
 });
index 4726f3a61ba39f7d58abf53097bbf58390748445..8b8c4796c7bba2e7bff3ecd99713c030494d2f38 100644 (file)
@@ -14,7 +14,8 @@
  */
 (function($) {
 
-var idIncrement = 0;
+var idIncrement = 0,
+       suppressExpandOnFocus = false;
 
 $.widget( "ui.popup", {
        version: "@VERSION",
@@ -23,6 +24,8 @@ $.widget( "ui.popup", {
                        my: "left top",
                        at: "left bottom"
                },
+               managed: false,
+               expandOnFocus: false,
                show: {
                        effect: "slideDown",
                        duration: "fast"
@@ -43,9 +46,10 @@ $.widget( "ui.popup", {
                }
 
                if ( !this.element.attr( "role" ) ) {
-                       // TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
-                       this.element.attr( "role", "dialog" );
-                       this.generatedRole = true;
+                       if ( !this.options.managed  ) {
+                               this.element.attr( "role", "dialog" );
+                               this.generatedRole = true;
+                       }
                }
 
                this.options.trigger
@@ -59,37 +63,90 @@ $.widget( "ui.popup", {
 
                this._bind(this.options.trigger, {
                        keydown: function( event ) {
-                               // prevent space-to-open to scroll the page, only happens for anchor ui.button
-                               if ( $.ui.button && 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 ) {
-                                       // prevent scrolling
-                                       event.preventDefault();
-                                       this.options.trigger.trigger( "click", event );
+                               switch ( event.keyCode ) {
+                                       case $.ui.keyCode.TAB:
+                                               // Waiting for close() will make popup hide too late, which breaks tab key behavior
+                                               this.element.hide();
+                                               this.close( event );
+                                               break;
+                                       case $.ui.keyCode.ESCAPE:
+                                               if ( this.isOpen ) {
+                                                       this.close( event );
+                                               }
+                                               break;
+                                       case $.ui.keyCode.SPACE:
+                                               // prevent space-to-open to scroll the page, only happens for anchor ui.button
+                                               // TODO check for $.ui.button before using custom selector, once more below
+                                               if ( this.options.trigger.is( "a:ui-button" ) ) {
+                                                       event.preventDefault();
+                                               }
+
+                                               else if (this.options.trigger.is( "a:not(:ui-button)" ) ) {
+                                                       this.options.trigger.trigger( "click", event );
+                                               }
+                                               break;
+                                       case $.ui.keyCode.DOWN:
+                                       case $.ui.keyCode.UP:
+                                               // prevent scrolling
+                                               event.preventDefault();
+                                               var that = this;
+                                               clearTimeout( this.closeTimer );
+                                               setTimeout(function() {
+                                                       that.open( event );
+                                                       that.focusPopup( event );
+                                               }, 1);
+                                               break;
                                }
                        },
                        click: function( event ) {
+                               event.stopPropagation();
                                event.preventDefault();
+                       },
+                       mousedown: function( event ) {
+                               var noFocus = false;
+                               /* TODO: Determine in which cases focus should stay on the trigger after the popup opens
+                               (should apply for any trigger that has other interaction besides opening the popup, e.g. a text field) */
+                               if ( $( event.target ).is( "input" ) ) {
+                                       noFocus = true;
+                               }
                                if (this.isOpen) {
-                                       // let it propagate to close
+                                       suppressExpandOnFocus = true;
+                                       this.close();
                                        return;
                                }
+                               this.open( event );
+                               var that = this;
                                clearTimeout( this.closeTimer );
                                this._delay(function() {
-                                       this.open( event );
+                                       if ( !noFocus ) {
+                                               that.focusPopup();
+                                       }
                                }, 1);
                        }
                });
 
-               if ( !$.ui.menu || !this.element.is( ":ui-menu" ) ) {
-                       // default use case, wrap tab order in popup
+               if ( this.options.expandOnFocus ) {
+                       this._bind( this.options.trigger, {
+                               focus : function( event ) {
+                                       if ( !suppressExpandOnFocus ) {
+                                               var that = this;
+                                               setTimeout(function() {
+                                                       if ( !that.isOpen ) {
+                                                               that.open( event );
+                                                       }
+                                               }, 1);
+                                       }
+                                       setTimeout(function() {
+                                               suppressExpandOnFocus = false;
+                                       }, 100);
+                               },
+                               blur: function( event ) {
+                                       suppressExpandOnFocus = false;
+                               }
+                       });
+               }
+               if ( !this.options.managed ) {
+                       //default use case, wrap tab order in popup
                        this._bind({ keydown : function( event ) {
                                        if ( event.keyCode !== $.ui.keyCode.TAB ) {
                                                return;
@@ -109,21 +166,34 @@ $.widget( "ui.popup", {
                }
 
                this._bind({
-                       // TODO only triggered on element if it can receive focus
-                       // bind to document instead?
-                       // either element itself or a child should be focusable
+                       focusout: 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);
+                       },
+                       focusin: function( event ) {
+                               clearTimeout( this.closeTimer );
+                       },
+                       mouseup: function( event ) {
+                               clearTimeout( this.closeTimer );
+                       }
+               });
+
+               this._bind({
                        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.focusTrigger();
                                }
                        }
                });
 
                this._bind(document, {
                        click: function( event ) {
-                               if ( this.isOpen && !$(event.target).closest(".ui-popup").length ) {
+                               if ( this.isOpen && !$( event.target ).closest( this.element.add( this.options.trigger ) ).length ) {
                                        this.close( event );
                                }
                        }
@@ -161,11 +231,14 @@ $.widget( "ui.popup", {
                        .attr( "aria-expanded", "true" )
                        .position( position );
 
-               // can't use custom selector when menu isn't loaded
-               if ( $.ui.menu && this.element.is( ":ui-menu" ) ) {
-                       this.element.menu( "focus", event, this.element.children( "li" ).first() );
-                       this.element.focus();
-               } else {
+               // 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 );
+       },
+
+       focusPopup: function( event ) {
+               if ( !this.options.managed ) {
                        // set focus to the first tabbable element in the popup container
                        // if there are no tabbable elements, set focus on the popup itself
                        var tabbables = this.element.find( ":tabbable" );
@@ -179,11 +252,13 @@ $.widget( "ui.popup", {
                        }
                        tabbables.first().focus( 1 );
                }
+               this._trigger( "focusPopup", event );
+       },
 
-               // 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 );
+       focusTrigger: function( event ) {
+               suppressExpandOnFocus = true;
+               this.options.trigger.focus();
+               this._trigger( "focusTrigger", event );
        },
 
        close: function( event ) {