Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Popup hh #344

Merged
merged 4 commits into from almost 3 years ago

2 participants

Hans Hillen Jörn Zaefferer
Hans Hillen

(reopened) Default keyboard mechanism for popup content.

ui/jquery.ui.popup.js
((13 lines not shown))
149 151
 			this.element.menu("focus", event, this.element.children( "li" ).first() );
  152
+			this.element.focus();
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

why two focus() calls for menu?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((7 lines not shown))
85 84
 				var that = this;
86 85
 				// use a timer to allow click to clear it and letting that
87 86
 				// handle the closing instead of opening again
88 87
 				that.closeTimer = setTimeout( function() {
89 88
 					that.close( event );
90 89
 				}, 100);
91  
-			}
  90
+			},
  91
+			focusin: function( event ) {
  92
+				var that = this;
  93
+				clearTimeout( that.closeTimer );
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

don't really need 'that' here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
@@ -80,19 +80,22 @@ $.widget( "ui.popup", {
80 80
 		});
81 81
 		
82 82
 		this._bind(this.element, {
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

No need to pass this.element to _bind, that's the default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((15 lines not shown))
150 153
 		}
151  
-
  154
+		// TODO add other special use cases that differ from the default dialog style keyboard mechanism
  155
+		else {
  156
+			//default use case, popup could be anything (e.g. a form)
  157
+			this.element
  158
+				.bind( "keydown.ui-popup", function( event ) {
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

Should move this to _create - no need to bind every time when popup is opened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((15 lines not shown))
150 153
 		}
151  
-
  154
+		// TODO add other special use cases that differ from the default dialog style keyboard mechanism
  155
+		else {
  156
+			//default use case, popup could be anything (e.g. a form)
  157
+			this.element
  158
+				.bind( "keydown.ui-popup", function( event ) {
  159
+	                if ( event.keyCode !== $.ui.keyCode.TAB ) {
  160
+	                    return;
  161
+	                }
  162
+	                var tabbables = $( ":tabbable", this ),
  163
+	                    first = tabbables.filter( ":first" ),
  164
+	                    last  = tabbables.filter( ":last" );
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

Can use .first() and .last() here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((24 lines not shown))
  161
+	                }
  162
+	                var tabbables = $( ":tabbable", this ),
  163
+	                    first = tabbables.filter( ":first" ),
  164
+	                    last  = tabbables.filter( ":last" );
  165
+	                if ( event.target === last[0] && !event.shiftKey ) {
  166
+	                    first.focus( 1 );
  167
+	                    return false;
  168
+	                } else if ( event.target === first[0] && event.shiftKey ) {
  169
+	                    last.focus( 1 );
  170
+	                    return false;
  171
+	                }
  172
+	            });
  173
+			
  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" );
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

Maybe add this? .andSelf( ":tabbable" )
Should match the element if its already tabbable and avoid overriding the tabIndex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((29 lines not shown))
  166
+	                    first.focus( 1 );
  167
+	                    return false;
  168
+	                } else if ( event.target === first[0] && event.shiftKey ) {
  169
+	                    last.focus( 1 );
  170
+	                    return false;
  171
+	                }
  172
+	            });
  173
+			
  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
+	        if (!tabbables.length) {
  178
+	        	this.element.attr("tabindex", "0");
  179
+	        	tabbables.add(this.element);
  180
+	        }
  181
+	        tabbables.eq( 0 ).focus(1);
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

.first() == .eq( 0 )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
@@ -160,7 +192,8 @@ $.widget( "ui.popup", {
160 192
 		this.element
161 193
 			.hide()
162 194
 			.attr( "aria-hidden", true )
163  
-			.attr( "aria-expanded", false );
  195
+			.attr( "aria-expanded", false )
  196
+			.unbind( "keypress.ui-popup");
1
Jörn Zaefferer Owner
jzaefferer added a note May 26, 2011

see above, or is there a reason to rebind this every time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jörn Zaefferer
Owner

Works fine in Chrome now, but doesn't wrap in IE6 and 7.

Hans Hillen

Jörn, I've handled your comments in the previous commit. The IE6/7 issue was caused by http://bugs.jqueryui.com/ticket/7438 and is related to the :focusable selector matching unfocusable form nodes. Since the issue is not related to popup itself, I worked around it for now by removing the form element from the popup demo's html.

ui/jquery.ui.popup.js
@@ -78,52 +78,76 @@ $.widget( "ui.popup", {
78 78
 				}, 1);
79 79
 			}
80 80
 		});
81  
-		
82  
-		this._bind(this.element, {
83  
-			// TODO use focusout so that element itself doesn't need to be focussable
84  
-			blur: function( event ) {
  81
+
  82
+		if ( !this.element.is( ":ui-menu" ) ) {
  83
+			//default use case, wrap tab order in popup
  84
+			this.element.bind( "keydown.ui-popup", function( event ) {
1
Jörn Zaefferer Owner
jzaefferer added a note May 31, 2011

Should also use _bind here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
ui/jquery.ui.popup.js
((10 lines not shown))
  83
+			//default use case, wrap tab order in popup
  84
+			this.element.bind( "keydown.ui-popup", function( event ) {
  85
+				if ( event.keyCode !== $.ui.keyCode.TAB ) {
  86
+					return;
  87
+				}
  88
+
  89
+				var tabbables = $( ":tabbable", this ),
  90
+					first = tabbables.first(),
  91
+					last  = tabbables.last();
  92
+
  93
+				if ( event.target === last[ 0 ] && !event.shiftKey ) {
  94
+					first.focus( 1 );
  95
+					return false;
  96
+				} else if ( event.target === first[ 0 ] && event.shiftKey ) {
  97
+					last.focus( 1 );
  98
+					return false;
1
Jörn Zaefferer Owner
jzaefferer added a note May 31, 2011

We generally try to avoid using "return false" in event handlers now, to allow propagation. Should replace with event.preventDefault()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jörn Zaefferer
Owner

Good update. I'll ping Scott about the :focusable issue.

Hans Hillen hanshillen commented on the diff June 10, 2011
ui/jquery.ui.popup.js
((16 lines not shown))
39 39
 			this.generatedRole = true;
40 40
 		}
41  
-		
  41
+
42 42
 		this.options.trigger
43 43
 			.attr( "aria-haspopup", true )
44 44
 			.attr( "aria-owns", this.element.attr( "id" ) );
1
Hans Hillen
hanshillen added a note June 10, 2011

Using aria-owns here is causing a bug in JAWS:

JAWS will consider the dialog to be part of the trigger element, and because of that it will not announce the dialog itself before announcing the focused element inside of it when the popup expands. Only when moving focus away from both the trigger element and the popup and back into the popup JAWS will announce the dialog properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jörn Zaefferer jzaefferer merged commit 7ccb0e5 into from June 10, 2011
Jörn Zaefferer jzaefferer closed this June 10, 2011
Jörn Zaefferer
Owner

Merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
28  demos/popup/default.html
@@ -29,7 +29,7 @@
29 29
 	<style type="text/css">
30 30
 		.ui-popup { position: absolute; z-index: 5000; }
31 31
 		.ui-menu { width: 200px; }
32  
-		
  32
+
33 33
 		/*
34 34
 		table {
35 35
 			border-collapse: collapse;
@@ -55,20 +55,18 @@
55 55
 
56 56
 <div class="demo">
57 57
 	<a href="#login-form">Log In</a>
58  
-	<div id="login-form" class="ui-widget-content" tabIndex="0">
59  
-		<form>
60  
-			<div>
61  
-				<label>Username</label>
62  
-				<input type="username" />
63  
-			</div>
64  
-			<div>
65  
-				<label>Password</label>
66  
-				<input type="password" />
67  
-			</div>
68  
-			<div>
69  
-				<input type="submit" class="submit" value="Login" />
70  
-			</div>
71  
-		</form>
  58
+	<div class="ui-widget-content" id="login-form" aria-label="Login options">
  59
+		<div>
  60
+			<label for="un">Username</label>
  61
+			<input type="text" id="un" />
  62
+		</div>
  63
+		<div>
  64
+			<label for="pw">Password</label>
  65
+			<input type="password" id="pw" />
  66
+		</div>
  67
+		<div>
  68
+			<input type="submit" value="Login" class="submit" />
  69
+		</div>
72 70
 	</div>
73 71
 </div>
74 72
 
102  ui/jquery.ui.popup.js
@@ -13,7 +13,7 @@
13 13
  *	jquery.ui.position.js
14 14
  */
15 15
 (function($) {
16  
-	
  16
+
17 17
 var idIncrement = 0;
18 18
 
19 19
 $.widget( "ui.popup", {
@@ -27,34 +27,34 @@ $.widget( "ui.popup", {
27 27
 		if ( !this.options.trigger ) {
28 28
 			this.options.trigger = this.element.prev();
29 29
 		}
30  
-		
  30
+
31 31
 		if ( !this.element.attr( "id" ) ) {
32 32
 			this.element.attr( "id", "ui-popup-" + idIncrement++ );
33 33
 			this.generatedId = true;
34 34
 		}
35  
-		
  35
+
36 36
 		if ( !this.element.attr( "role" ) ) {
37 37
 			// TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
38  
-			this.element.attr( "role", "tooltip" );
  38
+			this.element.attr( "role", "dialog" );
39 39
 			this.generatedRole = true;
40 40
 		}
41  
-		
  41
+
42 42
 		this.options.trigger
43 43
 			.attr( "aria-haspopup", true )
44 44
 			.attr( "aria-owns", this.element.attr( "id" ) );
45  
-		
  45
+
46 46
 		this.element
47  
-			.addClass("ui-popup")
  47
+			.addClass( "ui-popup" )
48 48
 		this.close();
49 49
 
50 50
 		this._bind(this.options.trigger, {
51 51
 			keydown: function( event ) {
52  
-				// prevent space-to-open to scroll the page, only hapens for anchor ui.button
53  
-				if ( this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE) {
54  
-					event.preventDefault()
  52
+				// prevent space-to-open to scroll the page, only happens for anchor ui.button
  53
+				if ( this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE ) {
  54
+					event.preventDefault();
55 55
 				}
56 56
 				// TODO handle SPACE to open popup? only when not handled by ui.button
57  
-				if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is("a:not(:ui-button)") ) {
  57
+				if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is( "a:not(:ui-button)" ) ) {
58 58
 					this.options.trigger.trigger( "click", event );
59 59
 				}
60 60
 				// translate keydown to click
@@ -78,52 +78,75 @@ $.widget( "ui.popup", {
78 78
 				}, 1);
79 79
 			}
80 80
 		});
81  
-		
82  
-		this._bind(this.element, {
83  
-			// TODO use focusout so that element itself doesn't need to be focussable
84  
-			blur: function( event ) {
  81
+
  82
+		if ( !this.element.is( ":ui-menu" ) ) {
  83
+			//default use case, wrap tab order in popup
  84
+			this._bind({ keydown : function( event ) {
  85
+					if ( event.keyCode !== $.ui.keyCode.TAB ) {
  86
+						return;
  87
+					}
  88
+					var tabbables = $( ":tabbable", this.element ),
  89
+						first = tabbables.first(),
  90
+						last  = tabbables.last();
  91
+					if ( event.target === last[ 0 ] && !event.shiftKey ) {
  92
+						first.focus( 1 );
  93
+						event.preventDefault();
  94
+					} else if ( event.target === first[ 0 ] && event.shiftKey ) {
  95
+						last.focus( 1 );
  96
+						event.preventDefault();
  97
+					}
  98
+				}
  99
+			});
  100
+		}
  101
+
  102
+		this._bind({
  103
+			focusout: function( event ) {
85 104
 				var that = this;
86 105
 				// use a timer to allow click to clear it and letting that
87 106
 				// handle the closing instead of opening again
88 107
 				that.closeTimer = setTimeout( function() {
89 108
 					that.close( event );
90 109
 				}, 100);
  110
+			},
  111
+			focusin: function( event ) {
  112
+				clearTimeout( this.closeTimer );
91 113
 			}
92 114
 		});
93 115
 
94 116
 		this._bind({
95  
-			// TODO only triggerd on element if it can receive focus
  117
+			// TODO only triggered on element if it can receive focus
96 118
 			// bind to document instead?
97 119
 			// either element itself or a child should be focusable
98 120
 			keyup: function( event ) {
99  
-				if (event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" )) {
  121
+				if ( event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" ) ) {
100 122
 					this.close( event );
101 123
 					// TODO move this to close()? would allow menu.select to call popup.close, and get focus back to trigger
102 124
 					this.options.trigger.focus();
103 125
 				}
104 126
 			}
105 127
 		});
106  
-		
  128
+
107 129
 		this._bind(document, {
108 130
 			click: function( event ) {
109  
-				if (this.isOpen && !$(event.target).closest(".ui-popup").length) {
  131
+				if ( this.isOpen && !$(event.target).closest(".ui-popup").length ) {
110 132
 					this.close( event );
111 133
 				}
112 134
 			}
113 135
 		})
114 136
 	},
115  
-	
  137
+
116 138
 	_destroy: function() {
117 139
 		this.element
118 140
 			.show()
119 141
 			.removeClass( "ui-popup" )
120 142
 			.removeAttr( "aria-hidden" )
121  
-			.removeAttr( "aria-expanded" );
  143
+			.removeAttr( "aria-expanded" )
  144
+			.unbind( "keypress.ui-popup");
122 145
 
123 146
 		this.options.trigger
124 147
 			.removeAttr( "aria-haspopup" )
125 148
 			.removeAttr( "aria-owns" );
126  
-			
  149
+
127 150
 		if ( this.generatedId ) {
128 151
 			this.element.removeAttr( "id" );
129 152
 		}
@@ -131,7 +154,7 @@ $.widget( "ui.popup", {
131 154
 			this.element.removeAttr( "role" );
132 155
 		}
133 156
 	},
134  
-	
  157
+
135 158
 	open: function( event ) {
136 159
 		var position = $.extend( {}, {
137 160
 			of: this.options.trigger
@@ -141,17 +164,28 @@ $.widget( "ui.popup", {
141 164
 			.show()
142 165
 			.attr( "aria-hidden", false )
143 166
 			.attr( "aria-expanded", true )
144  
-			.position( position )
145  
-			// TODO find a focussable child, otherwise put focus on element, add tabIndex=0 if not focussable
146  
-			.focus();
  167
+			.position( position );
147 168
 
148  
-		if (this.element.is(":ui-menu")) {
149  
-			this.element.menu("focus", event, this.element.children( "li" ).first() );
  169
+		if (this.element.is( ":ui-menu" )) { //popup is a menu
  170
+			this.element.menu( "focus", event, this.element.children( "li" ).first() );
  171
+			this.element.focus();
  172
+		} else {
  173
+			// set focus to the first tabbable element in the popup container
  174
+			// if there are no tabbable elements, set focus on the popup itself
  175
+			var tabbables = this.element.find( ":tabbable" );
  176
+			this.removeTabIndex = false;
  177
+			if ( !tabbables.length ) {
  178
+				if ( !this.element.is(":tabbable") ) {
  179
+					this.element.attr("tabindex", "0");
  180
+					this.removeTabIndex = true;
  181
+				}
  182
+				tabbables = tabbables.add( this.element[ 0 ] );
  183
+			}
  184
+			tabbables.first().focus( 1 );
150 185
 		}
151 186
 
152 187
 		// take trigger out of tab order to allow shift-tab to skip trigger
153  
-		this.options.trigger.attr("tabindex", -1);
154  
-
  188
+		this.options.trigger.attr( "tabindex", -1 );
155 189
 		this.isOpen = true;
156 190
 		this._trigger( "open", event );
157 191
 	},
@@ -162,13 +196,13 @@ $.widget( "ui.popup", {
162 196
 			.attr( "aria-hidden", true )
163 197
 			.attr( "aria-expanded", false );
164 198
 
165  
-		this.options.trigger.attr("tabindex", 0);
166  
-
  199
+		this.options.trigger.attr( "tabindex" , 0 );
  200
+		if ( this.removeTabIndex ) {
  201
+			this.element.removeAttr( "tabindex" );
  202
+		}
167 203
 		this.isOpen = false;
168 204
 		this._trigger( "close", event );
169 205
 	}
170  
-	
171  
-	
172 206
 });
173 207
 
174 208
 }(jQuery));
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.