Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Popup hh #344

Merged
merged 4 commits into from

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))
this.element.menu("focus", event, this.element.children( "li" ).first() );
+ this.element.focus();
Jörn Zaefferer Owner

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))
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 ) {
+ var that = this;
+ clearTimeout( that.closeTimer );
Jörn Zaefferer Owner

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", {
});
this._bind(this.element, {
Jörn Zaefferer Owner

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))
}
-
+ // TODO add other special use cases that differ from the default dialog style keyboard mechanism
+ else {
+ //default use case, popup could be anything (e.g. a form)
+ this.element
+ .bind( "keydown.ui-popup", function( event ) {
Jörn Zaefferer Owner

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))
}
-
+ // TODO add other special use cases that differ from the default dialog style keyboard mechanism
+ else {
+ //default use case, popup could be anything (e.g. a form)
+ this.element
+ .bind( "keydown.ui-popup", function( event ) {
+ if ( event.keyCode !== $.ui.keyCode.TAB ) {
+ return;
+ }
+ var tabbables = $( ":tabbable", this ),
+ first = tabbables.filter( ":first" ),
+ last = tabbables.filter( ":last" );
Jörn Zaefferer Owner

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))
+ }
+ var tabbables = $( ":tabbable", this ),
+ first = tabbables.filter( ":first" ),
+ last = tabbables.filter( ":last" );
+ if ( event.target === last[0] && !event.shiftKey ) {
+ first.focus( 1 );
+ return false;
+ } else if ( event.target === first[0] && event.shiftKey ) {
+ last.focus( 1 );
+ return false;
+ }
+ });
+
+ // 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" );
Jörn Zaefferer Owner

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))
+ first.focus( 1 );
+ return false;
+ } else if ( event.target === first[0] && event.shiftKey ) {
+ last.focus( 1 );
+ return false;
+ }
+ });
+
+ // 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" );
+ if (!tabbables.length) {
+ this.element.attr("tabindex", "0");
+ tabbables.add(this.element);
+ }
+ tabbables.eq( 0 ).focus(1);
Jörn Zaefferer Owner

.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", {
this.element
.hide()
.attr( "aria-hidden", true )
- .attr( "aria-expanded", false );
+ .attr( "aria-expanded", false )
+ .unbind( "keypress.ui-popup");
Jörn Zaefferer Owner

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", {
}, 1);
}
});
-
- this._bind(this.element, {
- // TODO use focusout so that element itself doesn't need to be focussable
- blur: function( event ) {
+
+ if ( !this.element.is( ":ui-menu" ) ) {
+ //default use case, wrap tab order in popup
+ this.element.bind( "keydown.ui-popup", function( event ) {
Jörn Zaefferer Owner

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))
+ //default use case, wrap tab order in popup
+ this.element.bind( "keydown.ui-popup", function( event ) {
+ if ( event.keyCode !== $.ui.keyCode.TAB ) {
+ return;
+ }
+
+ var tabbables = $( ":tabbable", this ),
+ first = tabbables.first(),
+ last = tabbables.last();
+
+ if ( event.target === last[ 0 ] && !event.shiftKey ) {
+ first.focus( 1 );
+ return false;
+ } else if ( event.target === first[ 0 ] && event.shiftKey ) {
+ last.focus( 1 );
+ return false;
Jörn Zaefferer Owner

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
ui/jquery.ui.popup.js
((16 lines not shown))
this.generatedRole = true;
}
-
+
this.options.trigger
.attr( "aria-haspopup", true )
.attr( "aria-owns", this.element.attr( "id" ) );

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
Jörn Zaefferer
Owner

Merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 25, 2011
  1. Hans Hillen
  2. Hans Hillen

    Key event fix

    hanshillen authored
Commits on May 30, 2011
  1. Hans Hillen
Commits on Jun 10, 2011
  1. Hans Hillen
This page is out of date. Refresh to see the latest.
Showing with 81 additions and 49 deletions.
  1. +13 −15 demos/popup/default.html
  2. +68 −34 ui/jquery.ui.popup.js
28 demos/popup/default.html
View
@@ -29,7 +29,7 @@
<style type="text/css">
.ui-popup { position: absolute; z-index: 5000; }
.ui-menu { width: 200px; }
-
+
/*
table {
border-collapse: collapse;
@@ -55,20 +55,18 @@
<div class="demo">
<a href="#login-form">Log In</a>
- <div id="login-form" class="ui-widget-content" tabIndex="0">
- <form>
- <div>
- <label>Username</label>
- <input type="username" />
- </div>
- <div>
- <label>Password</label>
- <input type="password" />
- </div>
- <div>
- <input type="submit" class="submit" value="Login" />
- </div>
- </form>
+ <div class="ui-widget-content" id="login-form" aria-label="Login options">
+ <div>
+ <label for="un">Username</label>
+ <input type="text" id="un" />
+ </div>
+ <div>
+ <label for="pw">Password</label>
+ <input type="password" id="pw" />
+ </div>
+ <div>
+ <input type="submit" value="Login" class="submit" />
+ </div>
</div>
</div>
102 ui/jquery.ui.popup.js
View
@@ -13,7 +13,7 @@
* jquery.ui.position.js
*/
(function($) {
-
+
var idIncrement = 0;
$.widget( "ui.popup", {
@@ -27,34 +27,34 @@ $.widget( "ui.popup", {
if ( !this.options.trigger ) {
this.options.trigger = this.element.prev();
}
-
+
if ( !this.element.attr( "id" ) ) {
this.element.attr( "id", "ui-popup-" + idIncrement++ );
this.generatedId = true;
}
-
+
if ( !this.element.attr( "role" ) ) {
// TODO alternatives to tooltip are dialog and menu, all three aren't generic popups
- this.element.attr( "role", "tooltip" );
+ this.element.attr( "role", "dialog" );
this.generatedRole = true;
}
-
+
this.options.trigger
.attr( "aria-haspopup", true )
.attr( "aria-owns", this.element.attr( "id" ) );

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
-
+
this.element
- .addClass("ui-popup")
+ .addClass( "ui-popup" )
this.close();
this._bind(this.options.trigger, {
keydown: function( event ) {
- // prevent space-to-open to scroll the page, only hapens for anchor ui.button
- if ( this.options.trigger.is( "a:ui-button" ) && event.keyCode == $.ui.keyCode.SPACE) {
- event.preventDefault()
+ // prevent space-to-open to scroll the page, only happens for anchor ui.button
+ if ( 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)") ) {
+ if ( event.keyCode == $.ui.keyCode.SPACE && this.options.trigger.is( "a:not(:ui-button)" ) ) {
this.options.trigger.trigger( "click", event );
}
// translate keydown to click
@@ -78,52 +78,75 @@ $.widget( "ui.popup", {
}, 1);
}
});
-
- this._bind(this.element, {
- // TODO use focusout so that element itself doesn't need to be focussable
- blur: function( event ) {
+
+ if ( !this.element.is( ":ui-menu" ) ) {
+ //default use case, wrap tab order in popup
+ this._bind({ keydown : function( event ) {
+ if ( event.keyCode !== $.ui.keyCode.TAB ) {
+ return;
+ }
+ var tabbables = $( ":tabbable", this.element ),
+ first = tabbables.first(),
+ last = tabbables.last();
+ if ( event.target === last[ 0 ] && !event.shiftKey ) {
+ first.focus( 1 );
+ event.preventDefault();
+ } else if ( event.target === first[ 0 ] && event.shiftKey ) {
+ last.focus( 1 );
+ event.preventDefault();
+ }
+ }
+ });
+ }
+
+ this._bind({
+ 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 );
}
});
this._bind({
- // TODO only triggerd on element if it can receive focus
+ // TODO only triggered on element if it can receive focus
// bind to document instead?
// either element itself or a child should be focusable
keyup: function( event ) {
- if (event.keyCode == $.ui.keyCode.ESCAPE && this.element.is( ":visible" )) {
+ 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._bind(document, {
click: function( event ) {
- if (this.isOpen && !$(event.target).closest(".ui-popup").length) {
+ if ( this.isOpen && !$(event.target).closest(".ui-popup").length ) {
this.close( event );
}
}
})
},
-
+
_destroy: function() {
this.element
.show()
.removeClass( "ui-popup" )
.removeAttr( "aria-hidden" )
- .removeAttr( "aria-expanded" );
+ .removeAttr( "aria-expanded" )
+ .unbind( "keypress.ui-popup");
this.options.trigger
.removeAttr( "aria-haspopup" )
.removeAttr( "aria-owns" );
-
+
if ( this.generatedId ) {
this.element.removeAttr( "id" );
}
@@ -131,7 +154,7 @@ $.widget( "ui.popup", {
this.element.removeAttr( "role" );
}
},
-
+
open: function( event ) {
var position = $.extend( {}, {
of: this.options.trigger
@@ -141,17 +164,28 @@ $.widget( "ui.popup", {
.show()
.attr( "aria-hidden", false )
.attr( "aria-expanded", true )
- .position( position )
- // TODO find a focussable child, otherwise put focus on element, add tabIndex=0 if not focussable
- .focus();
+ .position( position );
- if (this.element.is(":ui-menu")) {
- this.element.menu("focus", event, this.element.children( "li" ).first() );
+ if (this.element.is( ":ui-menu" )) { //popup is a menu
+ this.element.menu( "focus", event, this.element.children( "li" ).first() );
+ this.element.focus();
+ } else {
+ // 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" );
+ this.removeTabIndex = false;
+ if ( !tabbables.length ) {
+ if ( !this.element.is(":tabbable") ) {
+ this.element.attr("tabindex", "0");
+ this.removeTabIndex = true;
+ }
+ tabbables = tabbables.add( this.element[ 0 ] );
+ }
+ tabbables.first().focus( 1 );
}
// take trigger out of tab order to allow shift-tab to skip trigger
- this.options.trigger.attr("tabindex", -1);
-
+ this.options.trigger.attr( "tabindex", -1 );
this.isOpen = true;
this._trigger( "open", event );
},
@@ -162,13 +196,13 @@ $.widget( "ui.popup", {
.attr( "aria-hidden", true )
.attr( "aria-expanded", false );
- this.options.trigger.attr("tabindex", 0);
-
+ this.options.trigger.attr( "tabindex" , 0 );
+ if ( this.removeTabIndex ) {
+ this.element.removeAttr( "tabindex" );
+ }
this.isOpen = false;
this._trigger( "close", event );
}
-
-
});
}(jQuery));
Something went wrong with that request. Please try again.