src/Draggable/Plugins/Announcement/Announcement.js
import AbstractPlugin from 'shared/AbstractPlugin';
const onInitialize = Symbol('onInitialize');
const onDestroy = Symbol('onDestroy');
const announceEvent = Symbol('announceEvent');
const announceMessage = Symbol('announceMessage');
const ARIA_RELEVANT = 'aria-relevant';
const ARIA_ATOMIC = 'aria-atomic';
const ARIA_LIVE = 'aria-live';
const ROLE = 'role';
/**
* Announcement default options
* @property {Object} defaultOptions
* @property {Number} defaultOptions.expire
* @type {Object}
*/
export const defaultOptions = {
expire: 7000,
};
/**
* Announcement plugin
* @class Announcement
* @module Announcement
* @extends AbstractPlugin
*/
export default class Announcement extends AbstractPlugin {
/**
* Announcement constructor.
* @constructs Announcement
* @param {Draggable} draggable - Draggable instance
*/
constructor(draggable) {
super(draggable);
/**
* Plugin options
* @property options
* @type {Object}
*/
this.options = {
...defaultOptions,
...this.getOptions(),
};
/**
* Original draggable trigger method. Hack until we have onAll or on('all')
* @property originalTriggerMethod
* @type {Function}
*/
this.originalTriggerMethod = this.draggable.trigger;
this[onInitialize] = this[onInitialize].bind(this);
this[onDestroy] = this[onDestroy].bind(this);
}
/**
* Attaches listeners to draggable
*/
attach() {
this.draggable.on('draggable:initialize', this[onInitialize]);
}
/**
* Detaches listeners from draggable
*/
detach() {
this.draggable.off('draggable:destroy', this[onDestroy]);
}
/**
* Returns passed in options
*/
getOptions() {
return this.draggable.options.announcements || {};
}
/**
* Announces event
* @private
* @param {AbstractEvent} event
*/
[announceEvent](event) {
const message = this.options[event.type];
if (message && typeof message === 'string') {
this[announceMessage](message);
}
if (message && typeof message === 'function') {
this[announceMessage](message(event));
}
}
/**
* Announces message to screen reader
* @private
* @param {String} message
*/
[announceMessage](message) {
announce(message, {expire: this.options.expire});
}
/**
* Initialize hander
* @private
*/
[onInitialize]() {
// Hack until there is an api for listening for all events
this.draggable.trigger = (event) => {
try {
this[announceEvent](event);
} finally {
// Ensure that original trigger is called
this.originalTriggerMethod.call(this.draggable, event);
}
};
}
/**
* Destroy hander
* @private
*/
[onDestroy]() {
this.draggable.trigger = this.originalTriggerMethod;
}
}
/**
* @const {HTMLElement} liveRegion
*/
const liveRegion = createRegion();
/**
* Announces message via live region
* @param {String} message
* @param {Object} options
* @param {Number} options.expire
*/
function announce(message, {expire}) {
const element = document.createElement('div');
element.textContent = message;
liveRegion.appendChild(element);
return setTimeout(() => {
liveRegion.removeChild(element);
}, expire);
}
/**
* Creates region element
* @return {HTMLElement}
*/
function createRegion() {
const element = document.createElement('div');
element.setAttribute('id', 'draggable-live-region');
element.setAttribute(ARIA_RELEVANT, 'additions');
element.setAttribute(ARIA_ATOMIC, 'true');
element.setAttribute(ARIA_LIVE, 'assertive');
element.setAttribute(ROLE, 'log');
element.style.position = 'fixed';
element.style.width = '1px';
element.style.height = '1px';
element.style.top = '-1px';
element.style.overflow = 'hidden';
return element;
}
// Append live region element as early as possible
document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(liveRegion);
});