1313 * jquery.ui.position.js
1414 */
1515( function ( $ ) {
16-
16+
1717var idIncrement = 0 ;
1818
1919$ . widget ( "ui.popup" , {
@@ -28,34 +28,34 @@ $.widget( "ui.popup", {
2828 if ( ! this . options . trigger ) {
2929 this . options . trigger = this . element . prev ( ) ;
3030 }
31-
31+
3232 if ( ! this . element . attr ( "id" ) ) {
3333 this . element . attr ( "id" , "ui-popup-" + idIncrement ++ ) ;
3434 this . generatedId = true ;
3535 }
36-
36+
3737 if ( ! this . element . attr ( "role" ) ) {
3838 // TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
39- this . element . attr ( "role" , "tooltip " ) ;
39+ this . element . attr ( "role" , "dialog " ) ;
4040 this . generatedRole = true ;
4141 }
42-
42+
4343 this . options . trigger
4444 . attr ( "aria-haspopup" , true )
4545 . attr ( "aria-owns" , this . element . attr ( "id" ) ) ;
46-
46+
4747 this . element
48- . addClass ( "ui-popup" )
48+ . addClass ( "ui-popup" )
4949 this . close ( ) ;
5050
5151 this . _bind ( this . options . trigger , {
5252 keydown : function ( event ) {
53- // prevent space-to-open to scroll the page, only hapens for anchor ui.button
54- if ( this . options . trigger . is ( "a:ui-button" ) && event . keyCode == $ . ui . keyCode . SPACE ) {
55- event . preventDefault ( )
53+ // prevent space-to-open to scroll the page, only happens for anchor ui.button
54+ if ( this . options . trigger . is ( "a:ui-button" ) && event . keyCode == $ . ui . keyCode . SPACE ) {
55+ event . preventDefault ( ) ;
5656 }
5757 // TODO handle SPACE to open popup? only when not handled by ui.button
58- if ( event . keyCode == $ . ui . keyCode . SPACE && this . options . trigger . is ( "a:not(:ui-button)" ) ) {
58+ if ( event . keyCode == $ . ui . keyCode . SPACE && this . options . trigger . is ( "a:not(:ui-button)" ) ) {
5959 this . options . trigger . trigger ( "click" , event ) ;
6060 }
6161 // translate keydown to click
@@ -79,60 +79,83 @@ $.widget( "ui.popup", {
7979 } , 1 ) ;
8080 }
8181 } ) ;
82-
83- this . _bind ( this . element , {
84- // TODO use focusout so that element itself doesn't need to be focussable
85- blur : function ( event ) {
82+
83+ if ( ! this . element . is ( ":ui-menu" ) ) {
84+ //default use case, wrap tab order in popup
85+ this . _bind ( { keydown : function ( event ) {
86+ if ( event . keyCode !== $ . ui . keyCode . TAB ) {
87+ return ;
88+ }
89+ var tabbables = $ ( ":tabbable" , this . element ) ,
90+ first = tabbables . first ( ) ,
91+ last = tabbables . last ( ) ;
92+ if ( event . target === last [ 0 ] && ! event . shiftKey ) {
93+ first . focus ( 1 ) ;
94+ event . preventDefault ( ) ;
95+ } else if ( event . target === first [ 0 ] && event . shiftKey ) {
96+ last . focus ( 1 ) ;
97+ event . preventDefault ( ) ;
98+ }
99+ }
100+ } ) ;
101+ }
102+
103+ this . _bind ( {
104+ focusout : function ( event ) {
86105 var that = this ;
87106 // use a timer to allow click to clear it and letting that
88107 // handle the closing instead of opening again
89108 that . closeTimer = setTimeout ( function ( ) {
90109 that . close ( event ) ;
91110 } , 100 ) ;
111+ } ,
112+ focusin : function ( event ) {
113+ clearTimeout ( this . closeTimer ) ;
92114 }
93115 } ) ;
94116
95117 this . _bind ( {
96- // TODO only triggerd on element if it can receive focus
118+ // TODO only triggered on element if it can receive focus
97119 // bind to document instead?
98120 // either element itself or a child should be focusable
99121 keyup : function ( event ) {
100- if ( event . keyCode == $ . ui . keyCode . ESCAPE && this . element . is ( ":visible" ) ) {
122+ if ( event . keyCode == $ . ui . keyCode . ESCAPE && this . element . is ( ":visible" ) ) {
101123 this . close ( event ) ;
102124 // TODO move this to close()? would allow menu.select to call popup.close, and get focus back to trigger
103125 this . options . trigger . focus ( ) ;
104126 }
105127 }
106128 } ) ;
107-
129+
108130 this . _bind ( document , {
109131 click : function ( event ) {
110- if ( this . isOpen && ! $ ( event . target ) . closest ( ".ui-popup" ) . length ) {
132+ if ( this . isOpen && ! $ ( event . target ) . closest ( ".ui-popup" ) . length ) {
111133 this . close ( event ) ;
112134 }
113135 }
114136 } )
115137 } ,
116-
138+
117139 _destroy : function ( ) {
118140 this . element
119141 . show ( )
120142 . removeClass ( "ui-popup" )
121143 . removeAttr ( "aria-hidden" )
122- . removeAttr ( "aria-expanded" ) ;
144+ . removeAttr ( "aria-expanded" )
145+ . unbind ( "keypress.ui-popup" ) ;
123146
124147 this . options . trigger
125148 . removeAttr ( "aria-haspopup" )
126149 . removeAttr ( "aria-owns" ) ;
127-
150+
128151 if ( this . generatedId ) {
129152 this . element . removeAttr ( "id" ) ;
130153 }
131154 if ( this . generatedRole ) {
132155 this . element . removeAttr ( "role" ) ;
133156 }
134157 } ,
135-
158+
136159 open : function ( event ) {
137160 var position = $ . extend ( { } , {
138161 of : this . options . trigger
@@ -142,17 +165,28 @@ $.widget( "ui.popup", {
142165 . show ( )
143166 . attr ( "aria-hidden" , false )
144167 . attr ( "aria-expanded" , true )
145- . position ( position )
146- // TODO find a focussable child, otherwise put focus on element, add tabIndex=0 if not focussable
147- . focus ( ) ;
168+ . position ( position ) ;
148169
149- if ( this . element . is ( ":ui-menu" ) ) {
150- this . element . menu ( "focus" , event , this . element . children ( "li" ) . first ( ) ) ;
170+ if ( this . element . is ( ":ui-menu" ) ) { //popup is a menu
171+ this . element . menu ( "focus" , event , this . element . children ( "li" ) . first ( ) ) ;
172+ this . element . focus ( ) ;
173+ } else {
174+ // set focus to the first tabbable element in the popup container
175+ // if there are no tabbable elements, set focus on the popup itself
176+ var tabbables = this . element . find ( ":tabbable" ) ;
177+ this . removeTabIndex = false ;
178+ if ( ! tabbables . length ) {
179+ if ( ! this . element . is ( ":tabbable" ) ) {
180+ this . element . attr ( "tabindex" , "0" ) ;
181+ this . removeTabIndex = true ;
182+ }
183+ tabbables = tabbables . add ( this . element [ 0 ] ) ;
184+ }
185+ tabbables . first ( ) . focus ( 1 ) ;
151186 }
152187
153188 // take trigger out of tab order to allow shift-tab to skip trigger
154- this . options . trigger . attr ( "tabindex" , - 1 ) ;
155-
189+ this . options . trigger . attr ( "tabindex" , - 1 ) ;
156190 this . isOpen = true ;
157191 this . _trigger ( "open" , event ) ;
158192 } ,
@@ -163,13 +197,13 @@ $.widget( "ui.popup", {
163197 . attr ( "aria-hidden" , true )
164198 . attr ( "aria-expanded" , false ) ;
165199
166- this . options . trigger . attr ( "tabindex" , 0 ) ;
167-
200+ this . options . trigger . attr ( "tabindex" , 0 ) ;
201+ if ( this . removeTabIndex ) {
202+ this . element . removeAttr ( "tabindex" ) ;
203+ }
168204 this . isOpen = false ;
169205 this . _trigger ( "close" , event ) ;
170206 }
171-
172-
173207} ) ;
174208
175209} ( jQuery ) ) ;
0 commit comments