Skip to content

Commit f4b2d7a

Browse files
committed
Autocomplete: ARIA live region as extension, adding a messages option. Fixes #7840 - Autocomplete: popup results not read by screen-readers
1 parent c0f6b0c commit f4b2d7a

File tree

6 files changed

+68
-19
lines changed

6 files changed

+68
-19
lines changed

demos/autocomplete/multiple-remote.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
}
4848
},
4949
focus: function() {
50-
// prevent value inserted on focus
50+
// prevent value inserted on focus, update liveRegion instead
51+
$( this ).data( "autocomplete" ).liveRegion.text( ui.item.label );
5152
return false;
5253
},
5354
select: function( event, ui ) {

demos/autocomplete/multiple.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@
5959
response( $.ui.autocomplete.filter(
6060
availableTags, extractLast( request.term ) ) );
6161
},
62-
focus: function() {
63-
// prevent value inserted on focus
62+
focus: function( event, ui ) {
63+
// prevent value inserted on focus, update liveRegion instead
64+
$( this ).data( "autocomplete" ).liveRegion.text( ui.item.label );
6465
return false;
6566
},
6667
select: function( event, ui ) {

tests/unit/autocomplete/autocomplete_common.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ TestHelpers.commonWidgetTests( "autocomplete", {
44
autoFocus: false,
55
delay: 300,
66
disabled: false,
7+
messages: {
8+
noResults: "No search results.",
9+
results: $.ui.autocomplete.prototype.options.messages.results
10+
},
711
minLength: 1,
812
position: {
913
my: "left top",

tests/unit/menu/menu_common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ TestHelpers.commonWidgetTests( "menu", {
66
my: "left top",
77
at: "right top"
88
},
9+
role: "menu",
910

1011
// callbacks
1112
blur: null,

ui/jquery.ui.autocomplete.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,7 @@ $.widget( "ui.autocomplete", {
6060

6161
this.element
6262
.addClass( "ui-autocomplete-input" )
63-
.attr( "autocomplete", "off" )
64-
// TODO verify these actually work as intended
65-
.attr({
66-
role: "textbox",
67-
"aria-autocomplete": "list",
68-
"aria-haspopup": "true"
69-
});
63+
.attr( "autocomplete", "off" );
7064

7165
this._bind({
7266
keydown: function( event ) {
@@ -188,7 +182,9 @@ $.widget( "ui.autocomplete", {
188182
.appendTo( this.document.find( this.options.appendTo || "body" )[0] )
189183
.menu({
190184
// custom key handling for now
191-
input: $()
185+
input: $(),
186+
// disable ARIA support, the live region takes care of that
187+
role: null
192188
})
193189
.zIndex( this.element.zIndex() + 1 )
194190
.hide()
@@ -532,4 +528,40 @@ $.extend( $.ui.autocomplete, {
532528
}
533529
});
534530

531+
532+
// live region extension, adding a `messages` option
533+
$.widget( "ui.autocomplete", $.ui.autocomplete, {
534+
options: {
535+
messages: {
536+
noResults: "No search results.",
537+
results: function(amount) {
538+
return amount + ( amount > 1 ? " results are" : " result is" ) + " available, use up and down arrow keys to navigate.";
539+
}
540+
}
541+
},
542+
_create: function() {
543+
this._super();
544+
this.liveRegion = $( "<span>", {
545+
role: "status",
546+
"aria-live": "polite"
547+
})
548+
.addClass( "ui-helper-hidden-accessible" )
549+
.insertAfter( this.element );
550+
},
551+
__response: function( content ) {
552+
var message;
553+
this._superApply( arguments );
554+
if ( this.options.disabled || this.cancelSearch) {
555+
return;
556+
}
557+
if ( content && content.length ) {
558+
message = this.options.messages.results( content.length );
559+
} else {
560+
message = this.options.messages.noResults;
561+
}
562+
this.liveRegion.text( message );
563+
}
564+
});
565+
566+
535567
}( jQuery ));

ui/jquery.ui.menu.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ $.widget( "ui.menu", {
2626
my: "left top",
2727
at: "right top"
2828
},
29+
role: "menu",
2930

3031
// callbacks
3132
blur: null,
@@ -42,7 +43,7 @@ $.widget( "ui.menu", {
4243
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
4344
.attr({
4445
id: this.menuId,
45-
role: "menu",
46+
role: this.options.role,
4647
tabIndex: 0
4748
})
4849
// need to catch all clicks on disabled menu
@@ -267,7 +268,7 @@ $.widget( "ui.menu", {
267268
.addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
268269
.hide()
269270
.attr({
270-
role: "menu",
271+
role: this.options.role,
271272
"aria-hidden": "true",
272273
"aria-expanded": "false"
273274
});
@@ -281,7 +282,7 @@ $.widget( "ui.menu", {
281282
.children( "a" )
282283
.addClass( "ui-corner-all" )
283284
.attr( "tabIndex", -1 )
284-
.attr( "role", "menuitem" )
285+
.attr( "role", this._itemRole() )
285286
.attr( "id", function( i ) {
286287
return menuId + "-" + i;
287288
});
@@ -302,8 +303,15 @@ $.widget( "ui.menu", {
302303
});
303304
},
304305

306+
_itemRole: function() {
307+
return {
308+
menu: "menuitem",
309+
listbox: "option"
310+
}[ this.options.role ];
311+
},
312+
305313
focus: function( event, item ) {
306-
var nested, borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
314+
var nested, borderTop, paddingTop, offset, scroll, elementHeight, itemHeight, focused;
307315
this.blur( event, event && event.type === "focus" );
308316

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

324332
this.active = item.first();
325-
this.element.attr( "aria-activedescendant",
326-
this.active.children( "a" )
327-
.addClass( "ui-state-focus" )
328-
.attr( "id" ) );
333+
focused = this.active.children( "a" ).addClass( "ui-state-focus" );
334+
// only update aria-activedescendant if there's a role
335+
// otherwise we assume focus is managed elsewhere
336+
if ( this.options.role ) {
337+
this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
338+
}
329339

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

0 commit comments

Comments
 (0)