Autocomplete: Rewrite with a delay instead of appending the live region

This fixes the issue caused by https://bugs.jqueryui.com/ticket/9357.
We now empty the live region instead of appending to it, and we do so
after a brief timeout so the live region isn't updated on every mousemove
event or when quickly traversing through options.

Fixes gh-2002
Closes gh-2031
This commit is contained in:
Ben Mullins 2022-01-05 05:35:34 -05:00 committed by GitHub
parent e90096e9dd
commit 933ce5d779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 38 deletions

View File

@ -293,6 +293,7 @@ QUnit.test( "simultaneous searches (#9334)", function( assert ) {
} ); } );
QUnit.test( "ARIA", function( assert ) { QUnit.test( "ARIA", function( assert ) {
var ready = assert.async();
assert.expect( 13 ); assert.expect( 13 );
var element = $( "#autocomplete" ).autocomplete( { var element = $( "#autocomplete" ).autocomplete( {
source: [ "java", "javascript" ] source: [ "java", "javascript" ]
@ -308,43 +309,51 @@ QUnit.test( "ARIA", function( assert ) {
"Live region's role attribute must be status" ); "Live region's role attribute must be status" );
element.autocomplete( "search", "j" ); element.autocomplete( "search", "j" );
assert.equal( liveRegion.children().first().text(), setTimeout( function() {
"2 results are available, use up and down arrow keys to navigate.", assert.equal( liveRegion.children().first().text(),
"Live region for multiple values" ); "2 results are available, use up and down arrow keys to navigate.",
"Live region for multiple values" );
element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } );
assert.equal( liveRegion.children().filter( ":visible" ).text(), "java", setTimeout( function() {
"Live region changed on keydown to announce the highlighted value" ); assert.equal( liveRegion.children().filter( ":visible" ).text(), "java",
"Live region changed on keydown to announce the highlighted value" );
element.one( "autocompletefocus", function( event ) { element.one( "autocompletefocus", function( event ) {
event.preventDefault(); event.preventDefault();
} ); } );
element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } );
assert.equal( liveRegion.children().filter( ":visible" ).text(), "javascript", setTimeout( function() {
"Live region updated when default focus is prevented" ); assert.equal( liveRegion.children().filter( ":visible" ).text(), "javascript",
"Live region updated when default focus is prevented" );
element.autocomplete( "search", "javas" ); element.autocomplete( "search", "javas" );
assert.equal( liveRegion.children().filter( ":visible" ).text(), setTimeout( function() {
"1 result is available, use up and down arrow keys to navigate.", assert.equal( liveRegion.children().filter( ":visible" ).text(),
"Live region for one value" ); "1 result is available, use up and down arrow keys to navigate.",
"Live region for one value" );
element.autocomplete( "search", "z" ); element.autocomplete( "search", "z" );
assert.equal( liveRegion.children().filter( ":visible" ).text(), "No search results.", setTimeout( function() {
"Live region for no values" ); assert.equal( liveRegion.children().filter( ":visible" ).text(), "No search results.",
"Live region for no values" );
assert.equal( liveRegion.children().length, 5, assert.equal( liveRegion.children().length, 1,
"Should be five children in the live region after the above" ); "Should be one child in the live region after the above" );
assert.equal( liveRegion.children().filter( ":visible" ).length, 1, assert.equal( liveRegion.children().filter( ":visible" ).length, 1,
"Only one should be still visible" ); "Only one should be still visible" );
assert.ok( liveRegion.children().filter( ":visible" )[ 0 ] === liveRegion.children().last()[ 0 ], assert.ok( liveRegion.children().filter( ":visible" )[ 0 ] === liveRegion.children().last()[ 0 ],
"The last one should be the visible one" ); "The last one should be the visible one" );
element.autocomplete( "destroy" );
element.autocomplete( "destroy" ); assert.equal( liveRegion.parent().length, 0,
assert.equal( liveRegion.parent().length, 0, "The liveRegion should be detached after destroy" );
"The liveRegion should be detached after destroy" ); ready();
}, 110 );
}, 110 );
}, 110 );
}, 110 );
}, 110 );
} ); } );
QUnit.test( "ARIA, aria-label announcement", function( assert ) { QUnit.test( "ARIA, aria-label announcement", function( assert ) {
var ready = assert.async();
assert.expect( 1 ); assert.expect( 1 );
$.widget( "custom.catcomplete", $.ui.autocomplete, { $.widget( "custom.catcomplete", $.ui.autocomplete, {
_renderMenu: function( ul, items ) { _renderMenu: function( ul, items ) {
@ -361,8 +370,11 @@ QUnit.test( "ARIA, aria-label announcement", function( assert ) {
liveRegion = element.catcomplete( "instance" ).liveRegion; liveRegion = element.catcomplete( "instance" ).liveRegion;
element.catcomplete( "search", "a" ); element.catcomplete( "search", "a" );
element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } ); element.simulate( "keydown", { keyCode: $.ui.keyCode.DOWN } );
assert.equal( liveRegion.children().filter( ":visible" ).text(), "People : anders andersson", setTimeout( function() {
"Live region changed on keydown to announce the highlighted value's aria-label attribute" ); assert.equal( liveRegion.children().filter( ":visible" ).text(), "People : anders andersson",
"Live region changed on keydown to announce the highlighted value's aria-label attribute" );
ready();
}, 110 );
} ); } );
QUnit.test( "ARIA, init on detached input", function( assert ) { QUnit.test( "ARIA, init on detached input", function( assert ) {

View File

@ -66,6 +66,7 @@ $.widget( "ui.autocomplete", {
requestIndex: 0, requestIndex: 0,
pending: 0, pending: 0,
liveRegionTimer: null,
_create: function() { _create: function() {
@ -267,8 +268,10 @@ $.widget( "ui.autocomplete", {
// Announce the value in the liveRegion // Announce the value in the liveRegion
label = ui.item.attr( "aria-label" ) || item.value; label = ui.item.attr( "aria-label" ) || item.value;
if ( label && String.prototype.trim.call( label ).length ) { if ( label && String.prototype.trim.call( label ).length ) {
this.liveRegion.children().hide(); clearTimeout( this.liveRegionTimer );
$( "<div>" ).text( label ).appendTo( this.liveRegion ); this.liveRegionTimer = this._delay( function() {
this.liveRegion.html( $( "<div>" ).text( label ) );
}, 100 );
} }
}, },
menuselect: function( event, ui ) { menuselect: function( event, ui ) {
@ -663,8 +666,10 @@ $.widget( "ui.autocomplete", $.ui.autocomplete, {
} else { } else {
message = this.options.messages.noResults; message = this.options.messages.noResults;
} }
this.liveRegion.children().hide(); clearTimeout( this.liveRegionTimer );
$( "<div>" ).text( message ).appendTo( this.liveRegion ); this.liveRegionTimer = this._delay( function() {
this.liveRegion.html( $( "<div>" ).text( message ) );
}, 100 );
} }
} ); } );