Skip to content

Commit 3c258bf

Browse files
hanshillenjzaefferer
authored andcommitted
Rewrite popup/menu interaction to make popup managed by menu (adds trigger option to menu). Makes popup agnostic of menu and allows datepicker to use popup (soon).
1 parent bd71f24 commit 3c258bf

File tree

4 files changed

+157
-59
lines changed

4 files changed

+157
-59
lines changed

demos/menu/contextmenu.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414
<link href="../demos.css" rel="stylesheet" />
1515
<script>
1616
$(function() {
17-
$(".demo button").button({
17+
var btn = $(".demo button").button({
1818
icons: {
1919
primary: "ui-icon-home",
2020
secondary: "ui-icon-triangle-1-s"
2121
}
22-
}).next().menu({
22+
});
23+
$("#cities").menu({
2324
select: function(event, ui) {
24-
$(this).hide();
2525
$("#log").append("<div>Selected " + ui.item.text() + "</div>");
26-
}
27-
}).popup();
26+
},
27+
trigger : btn});
2828
});
2929
</script>
3030
<style>
@@ -36,7 +36,7 @@
3636
<div class="demo">
3737

3838
<button>Select a city</button>
39-
<ul>
39+
<ul id="cities">
4040
<li><a href="#Amsterdam">Amsterdam</a></li>
4141
<li><a href="#Anaheim">Anaheim</a></li>
4242
<li><a href="#Cologne">Cologne</a></li>

demos/popup/popup-menu.html

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@
1616
function log( msg ) {
1717
$( "<div/>" ).text( msg ).appendTo( "#log" );
1818
}
19-
var selected = {
20-
select: function( event, ui ) {
21-
log( "Selected: " + ui.item.text() );
22-
$(this).popup("close");
23-
}
24-
};
19+
var selected = function( event, ui ) {
20+
log( "Selected: " + ui.item.text() );
21+
$(this).popup( "close" );
22+
}
23+
24+
$("#button1").button().next().menu( {select: selected, trigger: $("#button1")} );
2525

26-
$("#button1").button()
27-
.next().menu(selected).popup();
28-
2926
$( "#rerun" )
3027
.button()
3128
.click(function() {
@@ -39,10 +36,7 @@
3936
}
4037
})
4138
.next()
42-
.menu(selected)
43-
.popup({
44-
trigger: $("#select")
45-
})
39+
.menu( {select: selected, trigger: $("#select")} )
4640
.parent()
4741
.buttonset({
4842
items: "button"
@@ -69,8 +63,8 @@
6963
<li><a href="#">Utrecht</a></li>
7064
<li><a href="#">Zurich</a></li>
7165
</ul>
72-
73-
66+
67+
7468
<div>
7569
<div>
7670
<button id="rerun">Run last action</button>

ui/jquery.ui.menu.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ $.widget( "ui.menu", {
2424
position: {
2525
my: "left top",
2626
at: "right top"
27-
}
27+
},
28+
trigger: null
2829
},
2930
_create: function() {
3031
this.activeMenu = this.element;
@@ -39,6 +40,16 @@ $.widget( "ui.menu", {
3940
id: this.menuId,
4041
role: "menu"
4142
})
43+
// Prevent focus from sticking to links inside menu after clicking
44+
// them (focus should always stay on UL during navigation).
45+
// If the link is clicked, redirect focus to the menu.
46+
// TODO move to _bind below
47+
.bind( "mousedown.menu", function( event ) {
48+
if ( $( event.target).is( "a" ) ) {
49+
event.preventDefault();
50+
$( this ).focus( 1 );
51+
}
52+
})
4253
// need to catch all clicks on disabled menu
4354
// not possible through _bind
4455
.bind( "click.menu", $.proxy( function( event ) {
@@ -203,10 +214,24 @@ $.widget( "ui.menu", {
203214
}
204215
}
205216
});
217+
218+
if ( this.options.trigger ) {
219+
this.element.popup({
220+
trigger: this.options.trigger,
221+
managed: true,
222+
focusPopup: $.proxy( function( event, ui ) {
223+
this.focus( event, this.element.children( ".ui-menu-item" ).first() );
224+
this.element.focus( 1 );
225+
}, this)
226+
});
227+
}
206228
},
207229

208230
_destroy: function() {
209231
//destroy (sub)menus
232+
if ( this.options.trigger ) {
233+
this.element.popup( "destroy" );
234+
}
210235
this.element
211236
.removeAttr( "aria-activedescendant" )
212237
.find( ".ui-menu" )
@@ -508,6 +533,10 @@ $.widget( "ui.menu", {
508533
item: this.active
509534
};
510535
this.collapseAll( event, true );
536+
if ( this.options.trigger ) {
537+
$( this.options.trigger ).focus( 1 );
538+
this.element.popup( "close" );
539+
}
511540
this._trigger( "select", event, ui );
512541
}
513542
});

ui/jquery.ui.popup.js

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
*/
1515
(function($) {
1616

17-
var idIncrement = 0;
17+
var idIncrement = 0,
18+
suppressExpandOnFocus = false;
1819

1920
$.widget( "ui.popup", {
2021
version: "@VERSION",
@@ -23,6 +24,8 @@ $.widget( "ui.popup", {
2324
my: "left top",
2425
at: "left bottom"
2526
},
27+
managed: false,
28+
expandOnFocus: false,
2629
show: {
2730
effect: "slideDown",
2831
duration: "fast"
@@ -43,9 +46,10 @@ $.widget( "ui.popup", {
4346
}
4447

4548
if ( !this.element.attr( "role" ) ) {
46-
// TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
47-
this.element.attr( "role", "dialog" );
48-
this.generatedRole = true;
49+
if ( !this.options.managed ) {
50+
this.element.attr( "role", "dialog" );
51+
this.generatedRole = true;
52+
}
4953
}
5054

5155
this.options.trigger
@@ -59,37 +63,90 @@ $.widget( "ui.popup", {
5963

6064
this._bind(this.options.trigger, {
6165
keydown: function( event ) {
62-
// prevent space-to-open to scroll the page, only happens for anchor ui.button
63-
if ( $.ui.button && this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE ) {
64-
event.preventDefault();
65-
}
66-
// TODO handle SPACE to open popup? only when not handled by ui.button
67-
if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is( "a:not(:ui-button)" ) ) {
68-
this.options.trigger.trigger( "click", event );
69-
}
70-
// translate keydown to click
71-
// opens popup and let's tooltip hide itself
72-
if ( event.keyCode == $.ui.keyCode.DOWN ) {
73-
// prevent scrolling
74-
event.preventDefault();
75-
this.options.trigger.trigger( "click", event );
66+
switch ( event.keyCode ) {
67+
case $.ui.keyCode.TAB:
68+
// Waiting for close() will make popup hide too late, which breaks tab key behavior
69+
this.element.hide();
70+
this.close( event );
71+
break;
72+
case $.ui.keyCode.ESCAPE:
73+
if ( this.isOpen ) {
74+
this.close( event );
75+
}
76+
break;
77+
case $.ui.keyCode.SPACE:
78+
// prevent space-to-open to scroll the page, only happens for anchor ui.button
79+
// TODO check for $.ui.button before using custom selector, once more below
80+
if ( this.options.trigger.is( "a:ui-button" ) ) {
81+
event.preventDefault();
82+
}
83+
84+
else if (this.options.trigger.is( "a:not(:ui-button)" ) ) {
85+
this.options.trigger.trigger( "click", event );
86+
}
87+
break;
88+
case $.ui.keyCode.DOWN:
89+
case $.ui.keyCode.UP:
90+
// prevent scrolling
91+
event.preventDefault();
92+
var that = this;
93+
clearTimeout( this.closeTimer );
94+
setTimeout(function() {
95+
that.open( event );
96+
that.focusPopup( event );
97+
}, 1);
98+
break;
7699
}
77100
},
78101
click: function( event ) {
102+
event.stopPropagation();
79103
event.preventDefault();
104+
},
105+
mousedown: function( event ) {
106+
var noFocus = false;
107+
/* TODO: Determine in which cases focus should stay on the trigger after the popup opens
108+
(should apply for any trigger that has other interaction besides opening the popup, e.g. a text field) */
109+
if ( $( event.target ).is( "input" ) ) {
110+
noFocus = true;
111+
}
80112
if (this.isOpen) {
81-
// let it propagate to close
113+
suppressExpandOnFocus = true;
114+
this.close();
82115
return;
83116
}
117+
this.open( event );
118+
var that = this;
84119
clearTimeout( this.closeTimer );
85120
this._delay(function() {
86-
this.open( event );
121+
if ( !noFocus ) {
122+
that.focusPopup();
123+
}
87124
}, 1);
88125
}
89126
});
90127

91-
if ( !$.ui.menu || !this.element.is( ":ui-menu" ) ) {
92-
// default use case, wrap tab order in popup
128+
if ( this.options.expandOnFocus ) {
129+
this._bind( this.options.trigger, {
130+
focus : function( event ) {
131+
if ( !suppressExpandOnFocus ) {
132+
var that = this;
133+
setTimeout(function() {
134+
if ( !that.isOpen ) {
135+
that.open( event );
136+
}
137+
}, 1);
138+
}
139+
setTimeout(function() {
140+
suppressExpandOnFocus = false;
141+
}, 100);
142+
},
143+
blur: function( event ) {
144+
suppressExpandOnFocus = false;
145+
}
146+
});
147+
}
148+
if ( !this.options.managed ) {
149+
//default use case, wrap tab order in popup
93150
this._bind({ keydown : function( event ) {
94151
if ( event.keyCode !== $.ui.keyCode.TAB ) {
95152
return;
@@ -109,21 +166,34 @@ $.widget( "ui.popup", {
109166
}
110167

111168
this._bind({
112-
// TODO only triggered on element if it can receive focus
113-
// bind to document instead?
114-
// either element itself or a child should be focusable
169+
focusout: function( event ) {
170+
var that = this;
171+
// use a timer to allow click to clear it and letting that
172+
// handle the closing instead of opening again
173+
that.closeTimer = setTimeout( function() {
174+
that.close( event );
175+
}, 100);
176+
},
177+
focusin: function( event ) {
178+
clearTimeout( this.closeTimer );
179+
},
180+
mouseup: function( event ) {
181+
clearTimeout( this.closeTimer );
182+
}
183+
});
184+
185+
this._bind({
115186
keyup: function( event ) {
116187
if ( event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" ) ) {
117188
this.close( event );
118-
// TODO move this to close()? would allow menu.select to call popup.close, and get focus back to trigger
119-
this.options.trigger.focus();
189+
this.focusTrigger();
120190
}
121191
}
122192
});
123193

124194
this._bind(document, {
125195
click: function( event ) {
126-
if ( this.isOpen && !$(event.target).closest(".ui-popup").length ) {
196+
if ( this.isOpen && !$( event.target ).closest( this.element.add( this.options.trigger ) ).length ) {
127197
this.close( event );
128198
}
129199
}
@@ -161,11 +231,14 @@ $.widget( "ui.popup", {
161231
.attr( "aria-expanded", "true" )
162232
.position( position );
163233

164-
// can't use custom selector when menu isn't loaded
165-
if ( $.ui.menu && this.element.is( ":ui-menu" ) ) {
166-
this.element.menu( "focus", event, this.element.children( "li" ).first() );
167-
this.element.focus();
168-
} else {
234+
// take trigger out of tab order to allow shift-tab to skip trigger
235+
this.options.trigger.attr( "tabindex", -1 );
236+
this.isOpen = true;
237+
this._trigger( "open", event );
238+
},
239+
240+
focusPopup: function( event ) {
241+
if ( !this.options.managed ) {
169242
// set focus to the first tabbable element in the popup container
170243
// if there are no tabbable elements, set focus on the popup itself
171244
var tabbables = this.element.find( ":tabbable" );
@@ -179,11 +252,13 @@ $.widget( "ui.popup", {
179252
}
180253
tabbables.first().focus( 1 );
181254
}
255+
this._trigger( "focusPopup", event );
256+
},
182257

183-
// take trigger out of tab order to allow shift-tab to skip trigger
184-
this.options.trigger.attr( "tabindex", -1 );
185-
this.isOpen = true;
186-
this._trigger( "open", event );
258+
focusTrigger: function( event ) {
259+
suppressExpandOnFocus = true;
260+
this.options.trigger.focus();
261+
this._trigger( "focusTrigger", event );
187262
},
188263

189264
close: function( event ) {

0 commit comments

Comments
 (0)