// Copyright 2007 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Announcer that allows messages to be spoken by assistive * technologies. */ goog.provide('goog.a11y.aria.Announcer'); goog.require('goog.Disposable'); goog.require('goog.Timer'); goog.require('goog.a11y.aria'); goog.require('goog.a11y.aria.LivePriority'); goog.require('goog.a11y.aria.State'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.object'); /** * Class that allows messages to be spoken by assistive technologies that the * user may have active. * * @param {goog.dom.DomHelper=} opt_domHelper DOM helper. * @constructor * @extends {goog.Disposable} * @final */ goog.a11y.aria.Announcer = function(opt_domHelper) { goog.a11y.aria.Announcer.base(this, 'constructor'); /** * @type {goog.dom.DomHelper} * @private */ this.domHelper_ = opt_domHelper || goog.dom.getDomHelper(); /** * Map of priority to live region elements to use for communicating updates. * Elements are created on demand. * @type {Object} * @private */ this.liveRegions_ = {}; }; goog.inherits(goog.a11y.aria.Announcer, goog.Disposable); /** @override */ goog.a11y.aria.Announcer.prototype.disposeInternal = function() { goog.object.forEach( this.liveRegions_, this.domHelper_.removeNode, this.domHelper_); this.liveRegions_ = null; this.domHelper_ = null; goog.a11y.aria.Announcer.base(this, 'disposeInternal'); }; /** * Announce a message to be read by any assistive technologies the user may * have active. * @param {string} message The message to announce to screen readers. * @param {goog.a11y.aria.LivePriority=} opt_priority The priority of the * message. Defaults to POLITE. */ goog.a11y.aria.Announcer.prototype.say = function(message, opt_priority) { var priority = opt_priority || goog.a11y.aria.LivePriority.POLITE; var liveRegion = this.getLiveRegion_(priority); // Resets text content to force a DOM mutation (so that the setTextContent // post-timeout function will be noticed by the screen reader). This is to // avoid the problem of when the same message is "said" twice, which doesn't // trigger a DOM mutation. goog.dom.setTextContent(liveRegion, ''); // Uses non-zero timer to make VoiceOver and NVDA work goog.Timer.callOnce(function() { goog.dom.setTextContent(liveRegion, message); }, 1); }; /** * Returns an aria-live region that can be used to communicate announcements. * @param {!goog.a11y.aria.LivePriority} priority The required priority. * @return {!Element} A live region of the requested priority. * @private */ goog.a11y.aria.Announcer.prototype.getLiveRegion_ = function(priority) { var liveRegion = this.liveRegions_[priority]; if (liveRegion) { // Make sure the live region is not aria-hidden. goog.a11y.aria.removeState(liveRegion, goog.a11y.aria.State.HIDDEN); return liveRegion; } liveRegion = this.domHelper_.createElement(goog.dom.TagName.DIV); // Note that IE has a habit of declaring things that aren't display:none as // invisible to third-party tools like JAWs, so we can't just use height:0. liveRegion.style.position = 'absolute'; liveRegion.style.top = '-1000px'; liveRegion.style.height = '1px'; liveRegion.style.overflow = 'hidden'; goog.a11y.aria.setState(liveRegion, goog.a11y.aria.State.LIVE, priority); goog.a11y.aria.setState(liveRegion, goog.a11y.aria.State.ATOMIC, 'true'); this.domHelper_.getDocument().body.appendChild(liveRegion); this.liveRegions_[priority] = liveRegion; return liveRegion; };