Skip to content

Instantly share code, notes, and snippets.

@tryggvigy
Last active February 29, 2020 16:02
Show Gist options
  • Save tryggvigy/ac4473d008756afa32113d1074ea18f1 to your computer and use it in GitHub Desktop.
Save tryggvigy/ac4473d008756afa32113d1074ea18f1 to your computer and use it in GitHub Desktop.
On demand live region
import { OnDemandLiveRegion } from './OnDemandLiveRegion';
describe('OnDemandLiveRegion', () => {
beforeEach(() => {
document.body.innerHTML = '';
jest.useFakeTimers();
});
test('works with defaults', () => {
const liveRegion = new OnDemandLiveRegion();
liveRegion.say('hi!');
const liveRegionNode = document.querySelector(
`[id^="live-region-"]`,
) as Element;
expect(liveRegionNode.getAttribute('aria-live')).toBe('polite');
expect(liveRegionNode.getAttribute('role')).toBe('status');
jest.advanceTimersByTime(0);
expect(liveRegionNode.textContent).toBe('hi!');
});
test('supports assertive announcements', () => {
const liveRegion = new OnDemandLiveRegion({ level: 'assertive' });
liveRegion.say('something bad!');
const liveRegionNode = document.querySelector(
`[id^="live-region-"]`,
) as Element;
expect(liveRegionNode.getAttribute('aria-live')).toBe('assertive');
expect(liveRegionNode.getAttribute('role')).toBe('alert');
jest.advanceTimersByTime(0);
expect(liveRegionNode.textContent).toBe('something bad!');
});
test('supports delayed announcements', () => {
const liveRegion = new OnDemandLiveRegion({ delay: 500 });
liveRegion.say('delayed message!');
const liveRegionNode = document.querySelector(
`[id^="live-region-"]`,
) as Element;
jest.advanceTimersByTime(499);
expect(liveRegionNode.textContent).toBe('');
jest.advanceTimersByTime(1);
expect(liveRegionNode.textContent).toBe('delayed message!');
});
test('supports custom parent node', () => {
const parent = document.createElement('div');
document.body.appendChild(parent);
const liveRegion = new OnDemandLiveRegion({ parent });
liveRegion.say('hi from custom parent!');
const liveRegionNode = parent.querySelector(
`[id^="live-region-"]`,
) as Element;
jest.advanceTimersByTime(0);
expect(liveRegionNode.textContent).toBe('hi from custom parent!');
});
test('supports custom live region node id prefix', () => {
const liveRegion = new OnDemandLiveRegion({ idPrefix: 'my-prefix-' });
liveRegion.say('hi with custom id prefix!');
const liveRegionNode = document.querySelector(
`[id^="my-prefix-"]`,
) as Element;
jest.advanceTimersByTime(0);
expect(liveRegionNode.textContent).toBe('hi with custom id prefix!');
});
test('removes old live region node if prompted to announce again', () => {
const liveRegion = new OnDemandLiveRegion();
liveRegion.say('hi!');
liveRegion.say('hello there!');
const liveRegionNodes = document.querySelectorAll(`[id^="live-region-"]`);
expect(liveRegionNodes).toHaveLength(1);
});
test('removes old live region node if explicitly cleared', () => {
const liveRegion = new OnDemandLiveRegion();
liveRegion.say('hi!');
liveRegion.clearNode();
const liveRegionNodes = document.querySelectorAll(`[id^="live-region-"]`);
expect(liveRegionNodes).toHaveLength(0);
});
});
/**
* Based off https://github.com/Heydon/on-demand-live-region
*
* This class can be used for making screen readers announce text on demand,
* without a visual change to the interface.
*
* Example usage:
* const liveRegionPolite = new OnDemandLiveRegion();
* const liveRegionAssertive = new OnDemandLiveRegion({ level: 'assertive' });
* liveRegionPolite.say('Downloading');
* liveRegionAssertive.say('Failed adding song to playing');
*/
export type OnDemandLiveRegionSettings = {
level: 'polite' | 'assertive';
parent: HTMLElement;
idPrefix: string;
delay: number;
};
export class OnDemandLiveRegion {
private settings: OnDemandLiveRegionSettings;
private currentRegion: HTMLSpanElement;
constructor(options: Partial<OnDemandLiveRegionSettings> = {}) {
// The default settings for the module.
this.settings = {
level: options.level || 'polite',
parent: options.parent || document.body,
idPrefix: options.idPrefix || 'live-region-',
delay: options.delay || 0,
};
this.currentRegion = document.createElement('span');
}
say(thingToSay: string, delay: number = this.settings.delay) {
this.clearNode();
// Create fresh live region
this.currentRegion = document.createElement('span');
this.currentRegion.id =
this.settings.idPrefix + Math.floor(Math.random() * 10000);
// Determine redundant role
const role = this.settings.level !== 'assertive' ? 'status' : 'alert';
// Add role and aria-live attribution
this.currentRegion.setAttribute('aria-live', this.settings.level);
this.currentRegion.setAttribute('role', role);
// Hide live region element, but not from assistive technologies
this.currentRegion.setAttribute(
'style',
'clip: rect(1px, 1px, 1px, 1px); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px',
);
// Add live region to its designated parent
this.settings.parent.appendChild(this.currentRegion);
// Populate live region to trigger it
window.setTimeout(() => {
this.currentRegion.textContent = thingToSay;
}, delay);
}
clearNode() {
// Get rid of old live region if it exists
const oldRegion = this.settings.parent.querySelector(
`[id^="${this.settings.idPrefix}"]`,
);
if (oldRegion) {
this.settings.parent.removeChild(oldRegion);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment