/** * AccessibilityAnnouncer.tsx * * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT license. * * Implements the behavior for announcing text to screen readers, using aria-live regions. */ import * as React from 'react'; import { SubscriptionToken } from 'subscribableevent'; import { Types } from '../common/Interfaces'; import Timers from '../common/utils/Timers'; import Accessibility from './Accessibility'; import AccessibilityUtil from './AccessibilityUtil'; import Styles from './Styles'; export interface AccessibilityAnnouncerState { // Screen Reader text to be announced. announcementText: string; // Render announcementText in a nested div to work around browser quirks for windows. // Nested divs break mac. announcementTextInNestedDiv: boolean; } const _isMac = (typeof navigator !== 'undefined') && (typeof navigator.platform === 'string') && (navigator.platform.indexOf('Mac') >= 0); const _styles = { liveRegionContainer: Styles.combine({ position: 'absolute', overflow: 'hidden', opacity: 0, top: -30, bottom: 0, left: 0, right: 0, height: 30, whiteSpace: 'pre' }) }; export class AccessibilityAnnouncer extends React.Component<{}, AccessibilityAnnouncerState> { private _clearAnnouncementTimer: number | undefined; private _newAnnouncementEventChangedSubscription: SubscriptionToken | undefined; constructor(props: {}) { super(props); // Update announcement text. this._newAnnouncementEventChangedSubscription = Accessibility.newAnnouncementReadyEvent.subscribe(announcement => { if (this.state.announcementText === announcement) { // If the previous announcement is the same as the current announcement // we will append a ' ' to it. This ensures that the text in DOM of aria-live region changes // and will be read by screen Reader announcement += ' '; } if (_isMac) { // annnouncementText should never be in nested div for mac. // Voice over ignores reading nested divs in aria-live container. this.setState({ announcementText: announcement }); } else { // Additionally, alternate between announcement text directly under the aria-live element and // nested in a div to work around issues with some readers. NVDA on Windows is known to // not announce aria-live reliably without this, for example. this.setState({ announcementText: announcement, announcementTextInNestedDiv: !this.state.announcementTextInNestedDiv }); } }); this.state = this._getInitialState(); } private _getInitialState(): AccessibilityAnnouncerState { return { announcementText: '', announcementTextInNestedDiv: false }; } componentDidUpdate(prevProps: {}, prevState: AccessibilityAnnouncerState) { // When a new announcement text is set in the live region, start a timer to clear the text from the div so that it can't be focused // using a screen reader. if (prevState.announcementText !== this.state.announcementText && this.state.announcementText) { this._startClearAnnouncementTimer(); } } componentWillUnmount() { if (this._newAnnouncementEventChangedSubscription) { this._newAnnouncementEventChangedSubscription.unsubscribe(); this._newAnnouncementEventChangedSubscription = undefined; } } render() { const announcement: any = this.state.announcementTextInNestedDiv ? (