blob: bca6b47e404584411272af59e8d1e7d910ebb60e [file] [log] [blame]
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {css, html, LitElement} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
/**
* `gr-a11y-announcer` is a singleton element that allows screen reader
* announcements via dispatching a bubbling `iron-announce` event with a
* `text` detail payload.
*/
@customElement('gr-a11y-announcer')
export class GrA11yAnnouncer extends LitElement {
/**
* The `aria-live` mode: 'off', 'polite', or 'assertive'.
*/
@property({type: String}) mode: 'off' | 'polite' | 'assertive' = 'polite';
/**
* Time (ms) to wait before setting the text, to ensure screen readers re-announce.
*/
@property({type: Number}) timeout = 150;
@state()
private _text = '';
static instance: GrA11yAnnouncer | null = null;
static override styles = css`
:host {
display: inline-block;
position: fixed;
clip: rect(0, 0, 0, 0);
}
`;
override connectedCallback() {
super.connectedCallback();
if (!GrA11yAnnouncer.instance) {
GrA11yAnnouncer.instance = this;
}
document.addEventListener('iron-announce', this._onIronAnnounce);
}
override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('iron-announce', this._onIronAnnounce);
}
override render() {
return html`<div aria-live=${this.mode}>${this._text}</div>`;
}
private _onIronAnnounce = (event: Event) => {
const customEvent = event as CustomEvent<{text?: string}>;
if (customEvent.detail?.text) {
this.announce(customEvent.detail.text);
}
};
/**
* Causes the given text to be announced by screen readers.
*/
announce(text: string) {
// Clear first to allow repeated announcements of the same text.
this._text = '';
setTimeout(() => {
this._text = text;
}, this.timeout);
}
/**
* Ensures the singleton announcer element is available in the document.
*/
static requestAvailability(): GrA11yAnnouncer {
if (!GrA11yAnnouncer.instance) {
const announcer = document.createElement(
'gr-a11y-announcer'
) as GrA11yAnnouncer;
document.body.appendChild(announcer);
GrA11yAnnouncer.instance = announcer;
}
return GrA11yAnnouncer.instance;
}
}