14
14
*/
15
15
( function ( $ ) {
16
16
17
- var idIncrement = 0 ;
17
+ var idIncrement = 0 ,
18
+ suppressExpandOnFocus = false ;
18
19
19
20
$ . widget ( "ui.popup" , {
20
21
version : "@VERSION" ,
@@ -23,6 +24,8 @@ $.widget( "ui.popup", {
23
24
my : "left top" ,
24
25
at : "left bottom"
25
26
} ,
27
+ managed : false ,
28
+ expandOnFocus : false ,
26
29
show : {
27
30
effect : "slideDown" ,
28
31
duration : "fast"
@@ -43,9 +46,10 @@ $.widget( "ui.popup", {
43
46
}
44
47
45
48
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
+ }
49
53
}
50
54
51
55
this . options . trigger
@@ -59,37 +63,90 @@ $.widget( "ui.popup", {
59
63
60
64
this . _bind ( this . options . trigger , {
61
65
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 ;
76
99
}
77
100
} ,
78
101
click : function ( event ) {
102
+ event . stopPropagation ( ) ;
79
103
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
+ }
80
112
if ( this . isOpen ) {
81
- // let it propagate to close
113
+ suppressExpandOnFocus = true ;
114
+ this . close ( ) ;
82
115
return ;
83
116
}
117
+ this . open ( event ) ;
118
+ var that = this ;
84
119
clearTimeout ( this . closeTimer ) ;
85
120
this . _delay ( function ( ) {
86
- this . open ( event ) ;
121
+ if ( ! noFocus ) {
122
+ that . focusPopup ( ) ;
123
+ }
87
124
} , 1 ) ;
88
125
}
89
126
} ) ;
90
127
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
93
150
this . _bind ( { keydown : function ( event ) {
94
151
if ( event . keyCode !== $ . ui . keyCode . TAB ) {
95
152
return ;
@@ -109,21 +166,34 @@ $.widget( "ui.popup", {
109
166
}
110
167
111
168
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 ( {
115
186
keyup : function ( event ) {
116
187
if ( event . keyCode == $ . ui . keyCode . ESCAPE && this . element . is ( ":visible" ) ) {
117
188
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 ( ) ;
120
190
}
121
191
}
122
192
} ) ;
123
193
124
194
this . _bind ( document , {
125
195
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 ) {
127
197
this . close ( event ) ;
128
198
}
129
199
}
@@ -161,11 +231,14 @@ $.widget( "ui.popup", {
161
231
. attr ( "aria-expanded" , "true" )
162
232
. position ( position ) ;
163
233
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 ) {
169
242
// set focus to the first tabbable element in the popup container
170
243
// if there are no tabbable elements, set focus on the popup itself
171
244
var tabbables = this . element . find ( ":tabbable" ) ;
@@ -179,11 +252,13 @@ $.widget( "ui.popup", {
179
252
}
180
253
tabbables . first ( ) . focus ( 1 ) ;
181
254
}
255
+ this . _trigger ( "focusPopup" , event ) ;
256
+ } ,
182
257
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 ) ;
187
262
} ,
188
263
189
264
close : function ( event ) {
0 commit comments