Skip to content

Commit

Permalink
Autocomplete: ARIA live region as extension, adding a messages option…
Browse files Browse the repository at this point in the history
…. Fixes #7840 - Autocomplete: popup results not read by screen-readers
  • Loading branch information
jzaefferer committed May 16, 2012
1 parent c0f6b0c commit f4b2d7a
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 19 deletions.
3 changes: 2 additions & 1 deletion demos/autocomplete/multiple-remote.html
Expand Up @@ -47,7 +47,8 @@
} }
}, },
focus: function() { focus: function() {
// prevent value inserted on focus // prevent value inserted on focus, update liveRegion instead
$( this ).data( "autocomplete" ).liveRegion.text( ui.item.label );
return false; return false;
}, },
select: function( event, ui ) { select: function( event, ui ) {
Expand Down
5 changes: 3 additions & 2 deletions demos/autocomplete/multiple.html
Expand Up @@ -59,8 +59,9 @@
response( $.ui.autocomplete.filter( response( $.ui.autocomplete.filter(
availableTags, extractLast( request.term ) ) ); availableTags, extractLast( request.term ) ) );
}, },
focus: function() { focus: function( event, ui ) {
// prevent value inserted on focus // prevent value inserted on focus, update liveRegion instead
$( this ).data( "autocomplete" ).liveRegion.text( ui.item.label );
return false; return false;
}, },
select: function( event, ui ) { select: function( event, ui ) {
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/autocomplete/autocomplete_common.js
Expand Up @@ -4,6 +4,10 @@ TestHelpers.commonWidgetTests( "autocomplete", {
autoFocus: false, autoFocus: false,
delay: 300, delay: 300,
disabled: false, disabled: false,
messages: {
noResults: "No search results.",
results: $.ui.autocomplete.prototype.options.messages.results
},
minLength: 1, minLength: 1,
position: { position: {
my: "left top", my: "left top",
Expand Down
1 change: 1 addition & 0 deletions tests/unit/menu/menu_common.js
Expand Up @@ -6,6 +6,7 @@ TestHelpers.commonWidgetTests( "menu", {
my: "left top", my: "left top",
at: "right top" at: "right top"
}, },
role: "menu",


// callbacks // callbacks
blur: null, blur: null,
Expand Down
48 changes: 40 additions & 8 deletions ui/jquery.ui.autocomplete.js
Expand Up @@ -60,13 +60,7 @@ $.widget( "ui.autocomplete", {


this.element this.element
.addClass( "ui-autocomplete-input" ) .addClass( "ui-autocomplete-input" )
.attr( "autocomplete", "off" ) .attr( "autocomplete", "off" );
// TODO verify these actually work as intended
.attr({
role: "textbox",
"aria-autocomplete": "list",
"aria-haspopup": "true"
});


this._bind({ this._bind({
keydown: function( event ) { keydown: function( event ) {
Expand Down Expand Up @@ -188,7 +182,9 @@ $.widget( "ui.autocomplete", {
.appendTo( this.document.find( this.options.appendTo || "body" )[0] ) .appendTo( this.document.find( this.options.appendTo || "body" )[0] )
.menu({ .menu({
// custom key handling for now // custom key handling for now
input: $() input: $(),
// disable ARIA support, the live region takes care of that
role: null
}) })
.zIndex( this.element.zIndex() + 1 ) .zIndex( this.element.zIndex() + 1 )
.hide() .hide()
Expand Down Expand Up @@ -532,4 +528,40 @@ $.extend( $.ui.autocomplete, {
} }
}); });



// live region extension, adding a `messages` option
$.widget( "ui.autocomplete", $.ui.autocomplete, {
options: {
messages: {
noResults: "No search results.",
results: function(amount) {
return amount + ( amount > 1 ? " results are" : " result is" ) + " available, use up and down arrow keys to navigate.";
}
}
},
_create: function() {
this._super();
this.liveRegion = $( "<span>", {
role: "status",
"aria-live": "polite"
})
.addClass( "ui-helper-hidden-accessible" )
.insertAfter( this.element );
},
__response: function( content ) {
var message;
this._superApply( arguments );
if ( this.options.disabled || this.cancelSearch) {
return;
}
if ( content && content.length ) {
message = this.options.messages.results( content.length );
} else {
message = this.options.messages.noResults;
}
this.liveRegion.text( message );
}
});


}( jQuery )); }( jQuery ));
26 changes: 18 additions & 8 deletions ui/jquery.ui.menu.js
Expand Up @@ -26,6 +26,7 @@ $.widget( "ui.menu", {
my: "left top", my: "left top",
at: "right top" at: "right top"
}, },
role: "menu",


// callbacks // callbacks
blur: null, blur: null,
Expand All @@ -42,7 +43,7 @@ $.widget( "ui.menu", {
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
.attr({ .attr({
id: this.menuId, id: this.menuId,
role: "menu", role: this.options.role,
tabIndex: 0 tabIndex: 0
}) })
// need to catch all clicks on disabled menu // need to catch all clicks on disabled menu
Expand Down Expand Up @@ -267,7 +268,7 @@ $.widget( "ui.menu", {
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
.hide() .hide()
.attr({ .attr({
role: "menu", role: this.options.role,
"aria-hidden": "true", "aria-hidden": "true",
"aria-expanded": "false" "aria-expanded": "false"
}); });
Expand All @@ -281,7 +282,7 @@ $.widget( "ui.menu", {
.children( "a" ) .children( "a" )
.addClass( "ui-corner-all" ) .addClass( "ui-corner-all" )
.attr( "tabIndex", -1 ) .attr( "tabIndex", -1 )
.attr( "role", "menuitem" ) .attr( "role", this._itemRole() )
.attr( "id", function( i ) { .attr( "id", function( i ) {
return menuId + "-" + i; return menuId + "-" + i;
}); });
Expand All @@ -302,8 +303,15 @@ $.widget( "ui.menu", {
}); });
}, },


_itemRole: function() {
return {
menu: "menuitem",
listbox: "option"
}[ this.options.role ];
},

focus: function( event, item ) { focus: function( event, item ) {
var nested, borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; var nested, borderTop, paddingTop, offset, scroll, elementHeight, itemHeight, focused;
this.blur( event, event && event.type === "focus" ); this.blur( event, event && event.type === "focus" );


if ( this._hasScroll() ) { if ( this._hasScroll() ) {
Expand All @@ -322,10 +330,12 @@ $.widget( "ui.menu", {
} }


this.active = item.first(); this.active = item.first();
this.element.attr( "aria-activedescendant", focused = this.active.children( "a" ).addClass( "ui-state-focus" );
this.active.children( "a" ) // only update aria-activedescendant if there's a role
.addClass( "ui-state-focus" ) // otherwise we assume focus is managed elsewhere
.attr( "id" ) ); if ( this.options.role ) {
this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
}


// highlight active parent menu item, if any // highlight active parent menu item, if any
this.active.parent().closest( ".ui-menu-item" ).children( "a:first" ).addClass( "ui-state-active" ); this.active.parent().closest( ".ui-menu-item" ).children( "a:first" ).addClass( "ui-state-active" );
Expand Down

0 comments on commit f4b2d7a

Please sign in to comment.