From b9e438d07c370ac2d4b198048feb6b6922469f70 Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sat, 26 Oct 2013 11:31:07 -0400 Subject: [PATCH] Tooltip: Improve accessibility by adding content to an aria-live div Fixes #9610 Closes gh-1118 --- tests/unit/tooltip/tooltip_core.js | 28 +++++++++++++++++++++++---- tests/unit/tooltip/tooltip_options.js | 9 ++++++--- ui/jquery.ui.tooltip.js | 25 +++++++++++++++++++++++- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/tests/unit/tooltip/tooltip_core.js b/tests/unit/tooltip/tooltip_core.js index c3568bffc..710444b44 100644 --- a/tests/unit/tooltip/tooltip_core.js +++ b/tests/unit/tooltip/tooltip_core.js @@ -21,27 +21,47 @@ test( "markup structure", function() { }); test( "accessibility", function() { - expect( 5 ); + expect( 15 ); - var tooltipId, - tooltip, - element = $( "#multiple-describedby" ).tooltip(); + var tooltipId, tooltip, + element = $( "#multiple-describedby" ).tooltip(), + liveRegion = element.tooltip( "instance" ).liveRegion; + equal( liveRegion.find( ">div" ).length, 0 ); + equal( liveRegion.attr( "role" ), "log" ); + equal( liveRegion.attr( "aria-live" ), "assertive" ); + equal( liveRegion.attr( "aria-relevant" ), "additions" ); element.tooltip( "open" ); tooltipId = element.data( "ui-tooltip-id" ); tooltip = $( "#" + tooltipId ); equal( tooltip.attr( "role" ), "tooltip", "role" ); equal( element.attr( "aria-describedby" ), "fixture-span " + tooltipId, "multiple describedby when open" ); + // strictEqual to distinguish between .removeAttr( "title" ) and .attr( "title", "" ) // support: jQuery <1.6.2 // support: IE <8 // We should use strictEqual( ..., undefined ) when dropping jQuery 1.6.1 support (or IE6/7) ok( !element.attr( "title" ), "no title when open" ); + equal( liveRegion.children().length, 1 ); + equal( liveRegion.children().last().html(), "..." ); element.tooltip( "close" ); equal( element.attr( "aria-describedby" ), "fixture-span", "correct describedby when closed" ); equal( element.attr( "title" ), "...", "title restored when closed" ); + + element.tooltip( "open" ); + equal( liveRegion.children().length, 2, + "After the second tooltip show, there should be two children" ); + equal( liveRegion.children().filter( ":visible" ).length, 1, + "Only one of the children should be visible" ); + ok( liveRegion.children().last().is( ":visible" ), + "Only the last child should be visible" ); + element.tooltip( "close" ); + + element.tooltip( "destroy" ); + equal( liveRegion.parent().length, 0, + "Tooltip liveregion element should be removed" ); }); test( "delegated removal", function() { diff --git a/tests/unit/tooltip/tooltip_options.js b/tests/unit/tooltip/tooltip_options.js index 01ac25040..bced8e860 100644 --- a/tests/unit/tooltip/tooltip_options.js +++ b/tests/unit/tooltip/tooltip_options.js @@ -41,13 +41,16 @@ test( "content: return string", function() { }); test( "content: return jQuery", function() { - expect( 1 ); + expect( 2 ); var element = $( "#tooltipped1" ).tooltip({ content: function() { - return $( "
" ).html( "customstring" ); + return $( "
" ).html( "customstring" ); } - }).tooltip( "open" ); + }).tooltip( "open" ), + liveRegion = element.tooltip( "instance" ).liveRegion; deepEqual( $( "#" + element.data( "ui-tooltip-id" ) ).text(), "customstring" ); + equal( liveRegion.children().last().html(), "
customstring
", + "The accessibility live region will strip the ids but keep the structure" ); }); asyncTest( "content: sync + async callback", function() { diff --git a/ui/jquery.ui.tooltip.js b/ui/jquery.ui.tooltip.js index 9fde036a8..1ebe1a958 100644 --- a/ui/jquery.ui.tooltip.js +++ b/ui/jquery.ui.tooltip.js @@ -82,6 +82,16 @@ $.widget( "ui.tooltip", { if ( this.options.disabled ) { this._disable(); } + + // Append the aria-live region so tooltips announce correctly + this.liveRegion = $( "
" ) + .attr({ + role: "log", + "aria-live": "assertive", + "aria-relevant": "additions" + }) + .addClass( "ui-helper-hidden-accessible" ) + .appendTo( this.document[ 0 ].body ); }, _setOption: function( key, value ) { @@ -211,7 +221,7 @@ $.widget( "ui.tooltip", { }, _open: function( event, target, content ) { - var tooltip, events, delayedShow, + var tooltip, events, delayedShow, a11yContent, positionOption = $.extend( {}, this.options.position ); if ( !content ) { @@ -245,6 +255,18 @@ $.widget( "ui.tooltip", { this._addDescribedBy( target, tooltip.attr( "id" ) ); tooltip.find( ".ui-tooltip-content" ).html( content ); + // Support: Voiceover on OS X, JAWS on IE <= 9 + // JAWS announces deletions even when aria-relevant="additions" + // Voiceover will sometimes re-read the entire log region's contents from the beginning + this.liveRegion.children().hide(); + if ( content.clone ) { + a11yContent = content.clone(); + a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" ); + } else { + a11yContent = content; + } + $( "
" ).html( a11yContent ).appendTo( this.liveRegion ); + function position( event ) { positionOption.of = event; if ( tooltip.is( ":hidden" ) ) { @@ -394,6 +416,7 @@ $.widget( "ui.tooltip", { element.removeData( "ui-tooltip-title" ); } }); + this.liveRegion.remove(); } }); -- 2.39.5