addon-screen-reader
Version:
A Screen Reader Storybook addon
282 lines (238 loc) • 9.49 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _querySelectorShadowDom = require("query-selector-shadow-dom");
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function computeAccessibleName(element) {
var content = element.textContent.trim();
if (element.getAttribute('aria-label')) {
return element.getAttribute('aria-label');
}
if (element.getAttribute('alt')) {
return element.getAttribute('alt');
}
return content;
}
function dispatchTextChanged(text) {
var evt = new CustomEvent('screen-reader-text-changed', {
detail: {
text: text
}
});
window.dispatchEvent(evt);
}
var ScreenReader = /*#__PURE__*/function () {
function ScreenReader() {
_classCallCheck(this, ScreenReader);
this.isRunning = false;
this.focusList = [];
this.focusIndex = 0;
this.voiceEnabled = false;
this.textEnabled = false;
this.mappings = {
a: 'link',
button: 'button',
h1: 'heading',
h2: 'heading',
h3: 'heading',
h4: 'heading',
h5: 'heading',
p: 'paragraph',
html: 'page',
img: 'image'
};
this.storyDocument = {};
}
_createClass(ScreenReader, [{
key: "generateAnnouncers",
value: function generateAnnouncers() {
this.announcers = {
page: function page(element) {
var title = element.querySelector('title').textContent;
this.say("Page ".concat(title));
},
link: function link(element) {
this.say("Link, ".concat(computeAccessibleName(element), ". To follow the link, press Enter key."));
},
button: function button(element) {
this.say("Button, ".concat(computeAccessibleName(element), ". To press the button, press Space key."));
},
heading: function heading(element) {
var level = element.getAttribute('aria-level') || element.tagName[1];
this.say("Heading level ".concat(level, ", ").concat(computeAccessibleName(element)));
},
paragraph: function paragraph(element) {
this.say(element.textContent);
},
image: function image(element) {
this.say("Image, ".concat(computeAccessibleName(element)));
},
"default": function _default(element) {
this.say("".concat(element.tagName, " element: ").concat(computeAccessibleName(element)));
}
};
}
}, {
key: "addStyles",
value: function addStyles() {
var styleElement = this.storyDocument.createElement('style');
styleElement.textContent = "[tabindex=\"-1\"] {\n outline: none;;\n }\n [data-sr-current] {\n outline: 5px rgba( 0, 0, 0, .7 ) solid !important;\n }\n html[data-sr-current] {\n outline-offset: -5px;\n }";
this.storyDocument.head.appendChild(styleElement);
}
}, {
key: "say",
value: function say(speech, callback) {
if (this.voiceEnabled) {
var text = new SpeechSynthesisUtterance(speech);
if (callback) {
text.onend = callback;
}
speechSynthesis.cancel();
speechSynthesis.speak(text);
}
if (this.textEnabled) {
dispatchTextChanged(speech);
}
}
}, {
key: "computeRole",
value: function computeRole(element) {
var name = element.tagName.toLowerCase();
if (element.getAttribute('role')) {
return element.getAttribute('role');
}
return this.mappings[name] || 'default';
}
}, {
key: "announceElement",
value: function announceElement(element) {
var role = this.computeRole(element);
if (this.announcers[role]) {
this.announcers[role].call(this, element);
} else {
this.announcers["default"].call(this, element);
}
}
}, {
key: "isReadableElement",
value: function isReadableElement(element) {
var styles = getComputedStyle(element);
if (styles.visibility === 'hidden' || styles.display === 'none') {
return false;
}
if (this.hasDirectText(element)) {
return true;
} // focusable custom elements
if (element.tabIndex !== -1) {
return true;
}
return false;
} // Returns true if the element has a non-empty text outside of it's children
// eslint-disable-next-line class-methods-use-this
}, {
key: "hasDirectText",
value: function hasDirectText(node) {
var text = [].reduce.call(node.childNodes, function (a, b) {
return a + (b.nodeType === 3 ? b.textContent : '');
}, '');
return !!text.trim();
}
}, {
key: "createFocusList",
value: function createFocusList() {
var _this$focusList,
_this = this;
(_this$focusList = this.focusList).push.apply(_this$focusList, _toConsumableArray((0, _querySelectorShadowDom.querySelectorAllDeep)('body, #root >:not( [aria-hidden=true] )', this.storyDocument)));
this.focusList = this.focusList.filter(function (element) {
return _this.isReadableElement(element);
});
this.focusList.forEach(function (element) {
element.setAttribute('tabindex', element.tabIndex);
});
}
}, {
key: "getActiveElement",
value: function getActiveElement() {
return this.focusList[0];
}
}, {
key: "focus",
value: function focus(element) {
element.setAttribute('data-sr-current', true);
element.focus();
this.announceElement(element);
}
}, {
key: "moveFocus",
value: function moveFocus(offset) {
var last = (0, _querySelectorShadowDom.querySelectorDeep)('[data-sr-current]', this.storyDocument);
if (last) {
last.removeAttribute('data-sr-current');
}
if (typeof offset !== 'number') {
this.focusIndex = this.focusList.findIndex(function (element) {
return element === offset;
});
return this.focus(offset);
}
this.focusIndex += offset;
if (this.focusIndex < 0) {
this.focusIndex = this.focusList.length - 1;
} else if (this.focusIndex > this.focusList.length - 1) {
this.focusIndex = 0;
}
return this.focus(this.focusList[this.focusIndex]);
}
}, {
key: "start",
value: function start() {
this.storyDocument = document.querySelector('iframe').contentWindow.document;
this.addStyles();
this.generateAnnouncers();
this.createFocusList();
this.storyDocument.addEventListener('keydown', this.keyDownHandler.bind(this));
this.moveFocus(this.getActiveElement());
this.isRunning = true;
this.say('Screen reader on');
}
}, {
key: "stop",
value: function stop() {
var current = (0, _querySelectorShadowDom.querySelectorDeep)('[data-sr-current]', this.storyDocument);
if (current) {
current.removeAttribute('data-sr-current');
}
this.focusIndex = 0;
this.isRunning = false;
this.say('Screen reader off');
}
}, {
key: "keyDownHandler",
value: function keyDownHandler(evt) {
var _this2 = this;
if (!this.isRunning) {
return undefined;
}
if (evt.altKey && evt.keyCode === 9) {
evt.preventDefault();
this.moveFocus(evt.shiftKey ? -1 : 1);
} else if (evt.keyCode === 9) {
setTimeout(function () {
_this2.moveFocus(_this2.storyDocument.activeElement);
}, 0);
}
}
}]);
return ScreenReader;
}();
exports["default"] = ScreenReader;