From 07f1db4050673770a7012c2d175f63980af94cdc Mon Sep 17 00:00:00 2001 From: Dylan Barrell Date: Sun, 3 Nov 2013 08:14:39 -0500 Subject: [PATCH] Datepicker: Announce dates using aria-live so screen reader users can use the keyboard shortcuts. Fixes #9623. Does not announce to AT with keyboard shortcuts --- tests/unit/datepicker/datepicker_core.js | 49 +++++++++++++++++++++++- ui/jquery.ui.datepicker.js | 23 ++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/unit/datepicker/datepicker_core.js b/tests/unit/datepicker/datepicker_core.js index 0bc5723ec8c..d6416e52f01 100644 --- a/tests/unit/datepicker/datepicker_core.js +++ b/tests/unit/datepicker/datepicker_core.js @@ -303,9 +303,14 @@ asyncTest( "customStructure", function() { setTimeout( step1 ); }); +var announcedDate = null; +function callback ( date, inst ) { + announcedDate = $.datepicker._formatDate( inst, inst.selectedDay, inst.selectedMonth, inst.selectedYear ); +} + test("keystrokes", function() { - expect( 26 ); - var inp = TestHelpers.datepicker.init("#inp"), + expect( 41 ); + var inp = TestHelpers.datepicker.init( "#inp", { onSelect : callback } ), date = new Date(); inp.val("").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); @@ -334,67 +339,105 @@ test("keystrokes", function() { TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2008, 2 - 1, 4), "Keystroke esc - abandoned"); // Moving by day or week + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.LEFT}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() - 1); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke ctrl+left"); + equal( $.datepicker.liveRegion.find( "p" ).length, 2, "Should have made two announcements" ); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+LEFT was announced" ); + equal( $.datepicker.liveRegion.find( "p" ).last().text(), announcedDate, "SELECT was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.LEFT}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() + 1); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke left"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "LEFT was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.RIGHT}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() + 1); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke ctrl+right"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+RIGHT was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.RIGHT}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() - 1); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke right"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "RIGHT was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.UP}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() - 7); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke ctrl+up"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+UP was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.UP}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() + 7); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke up"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "UP was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.DOWN}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() + 7); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke ctrl+down"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+DOWN was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.DOWN}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); date.setDate(date.getDate() - 7); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), date, "Keystroke down"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "DOWN was announced" ); + // Moving by month or year + $.datepicker.liveRegion.empty(); inp.val("02/04/2008").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.PAGE_UP}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2008, 1 - 1, 4), "Keystroke pgup"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "PAGE_UP was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("02/04/2008").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.PAGE_DOWN}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2008, 3 - 1, 4), "Keystroke pgdn"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "PAGE_DOWN was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("02/04/2008").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.PAGE_UP}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2007, 2 - 1, 4), "Keystroke ctrl+pgup"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+PAGE_UP was announced" ); + + $.datepicker.liveRegion.empty(); inp.val("02/04/2008").datepicker("show"). simulate("keydown", {ctrlKey: true, keyCode: $.ui.keyCode.PAGE_DOWN}). simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2009, 2 - 1, 4), "Keystroke ctrl+pgdn"); + equal( $.datepicker.liveRegion.find( "p" ).first().text(), announcedDate, "CTRL+PAGE_DOWN was announced" ); + // Check for moving to short months inp.val("03/31/2008").datepicker("show"). simulate("keydown", {keyCode: $.ui.keyCode.PAGE_UP}). @@ -424,6 +467,8 @@ test("keystrokes", function() { simulate("keydown", {keyCode: $.ui.keyCode.ENTER}); TestHelpers.datepicker.equalsDate(inp.datepicker("getDate"), new Date(2008, 2 - 1, 4), "Keystroke ctrl+home"); + equal( $.datepicker.liveRegion.find( "p" ).last().prev("p").text(), announcedDate, "CTRL+HOME was announced" ); + // Change steps inp.datepicker("option", {stepMonths: 2, gotoCurrent: false}). datepicker("hide").val("02/04/2008").datepicker("show"). diff --git a/ui/jquery.ui.datepicker.js b/ui/jquery.ui.datepicker.js index e4961a1df60..dc0dc07dd10 100644 --- a/ui/jquery.ui.datepicker.js +++ b/ui/jquery.ui.datepicker.js @@ -130,6 +130,12 @@ function Datepicker() { }; $.extend(this._defaults, this.regional[""]); this.dpDiv = datepicker_bindHover($("
")); + this.liveRegion = $( "
" ) + .attr({ + "role": "log", + "aria-live": "assertive", + "aria-relevant": "additions" + }); } $.extend(Datepicker.prototype, { @@ -202,6 +208,7 @@ $.extend(Datepicker.prototype, { if( inst.settings.disabled ) { this._disableDatepicker( target ); } + input.attr( "role", "application" ); }, /* Make attachments based on settings. */ @@ -377,6 +384,7 @@ $.extend(Datepicker.prototype, { } else if (nodeName === "div" || nodeName === "span") { $target.removeClass(this.markerClassName).empty(); } + $target.removeAttr( "role" ); }, /* Enable the date picker to a jQuery selection. @@ -563,6 +571,13 @@ $.extend(Datepicker.prototype, { return (inst ? this._getDate(inst) : null); }, + /* Announce updates to assistive technology using aria-live */ + _announce: function( inst ) { + var announcement = this._formatDate( inst, inst.selectedDay, inst.selectedMonth, inst.selectedYear ); + this.liveRegion.find( "p" ).hide(); + $( "

" ).text( announcement ).appendTo( this.liveRegion ); + }, + /* Handle keystrokes. */ _doKeyDown: function(event) { var onSelect, dateStr, sel, @@ -591,7 +606,7 @@ $.extend(Datepicker.prototype, { } else { $.datepicker._hideDatepicker(); } - + $.datepicker._announce( inst ); return false; // don't submit the form case 27: $.datepicker._hideDatepicker(); break; // hide on escape @@ -656,6 +671,7 @@ $.extend(Datepicker.prototype, { } if (handled) { + $.datepicker._announce( inst ); event.preventDefault(); event.stopPropagation(); } @@ -2029,6 +2045,11 @@ $.fn.datepicker = function(options){ $.datepicker.initialized = true; } + /* Append the live region if it does not exist */ + if ( $( "#" + $.datepicker._mainDivId + "-liveregion" ).length === 0 ) { + $( "body" ).append( $.datepicker.liveRegion ); + } + /* Append datepicker main container to body if not exist. */ if ($("#"+$.datepicker._mainDivId).length === 0) { $("body").append($.datepicker.dpDiv);