diff --git a/package-lock.json b/package-lock.json index 8162c18c..72742caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4267,12 +4267,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4292,7 +4294,8 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", @@ -4440,6 +4443,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4447,7 +4451,8 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", @@ -4551,7 +4556,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", diff --git a/plugins/aria-visualizer/index.js b/plugins/aria-visualizer/index.js new file mode 100644 index 00000000..a13b0ea4 --- /dev/null +++ b/plugins/aria-visualizer/index.js @@ -0,0 +1,104 @@ +/** + * Allows users to see what screen readers would see. + */ + +const Plugin = require("../base"); + +const annotate = require("../shared/annotate")("roles"); + +// this will let us get a shorter info panel that just +// lets the user know we are visualizing a Screen Reader View +const PANEL_OPTIONS = { + statusPanelView: true +}; + +const ATTRIBUTES = [ + "role", + "aria-hidden" +]; + +require("./style.less"); + +const formatAttributeForMethodName = (attribute) => { + return attribute.split("-").map(part => `${part[0].toUpperCase()}${part.substr(1)}`).join(""); +} + +class AriaVisualizer extends Plugin { + constructor(...args) { + const options = Object.assign({}, args, { panel: PANEL_OPTIONS }); + + super(options); + + this.ariaAttributes = ATTRIBUTES.reduce((functionMap, attribute) => { + const methodSuffix = formatAttributeForMethodName(attribute); + + functionMap[attribute] = { + enable: this[`start${methodSuffix}`], + disable: this[`stop${methodSuffix}`] + }; + + return functionMap; + }, {}); + } + + getTitle() { + return "Screen Reader View"; + } + + getDescription() { + return "View the page as if you were a Screen Reader. See the effects of aria-* and other a11y attributes."; + } + + startAriaHidden(attribute) { + const className = `tota11y-${attribute}-visualized`; + + [...document.querySelectorAll(`[${attribute}="true"]:not(.tota11y)`)].forEach((element) => { + // make sure we aren't visualizing our tota11y DOM + if (!element.closest(".tota11y")) { + element.classList.add(className); + } + }); + } + + stopAriaHidden(attribute) { + const className = `tota11y-${attribute}-visualized`; + + [...document.querySelectorAll(`.${className}`)].forEach((element) => { + element.classList.remove(className); + }); + } + + startRole(attribute) { + [...document.querySelectorAll(`[${attribute}]:not(.tota11y)`)].forEach((element) => { + // make sure we aren't annotating our tota11y DOM + if (!element.closest(".tota11y")) { + annotate.label( + element, + `role: ${element.getAttribute("role")}` + ); + } + }); + } + + stopRole() { + annotate.removeAll(); + } + + run() { + // pop up our info panel to let the user know what we're doing + this.summary("Visualizing a11y attributes"); + this.panel.render(); + + Object.keys(this.ariaAttributes).forEach((property) => { + this.ariaAttributes[property].enable(property); + }); + } + + cleanup() { + Object.keys(this.ariaAttributes).forEach((property) => { + this.ariaAttributes[property].disable(property); + }); + } +} + +module.exports = AriaVisualizer; diff --git a/plugins/aria-visualizer/style.less b/plugins/aria-visualizer/style.less new file mode 100644 index 00000000..5d6f05cc --- /dev/null +++ b/plugins/aria-visualizer/style.less @@ -0,0 +1,5 @@ +@import "../../less/variables.less"; + +.tota11y-aria-hidden-visualized { + visibility: hidden !important; +} diff --git a/plugins/base.js b/plugins/base.js index 1ca07b47..994df40f 100644 --- a/plugins/base.js +++ b/plugins/base.js @@ -14,8 +14,8 @@ let InfoPanel = require("./shared/info-panel"); require("./style.less"); class Plugin { - constructor() { - this.panel = new InfoPanel(this); + constructor(options = {}) { + this.panel = new InfoPanel(this, options.panel); this.$checkbox = null; } diff --git a/plugins/index.js b/plugins/index.js index b6447b9f..4dc6e656 100644 --- a/plugins/index.js +++ b/plugins/index.js @@ -11,6 +11,7 @@ let LabelsPlugin = require("./labels"); let LandmarksPlugin = require("./landmarks"); let LinkTextPlugin = require("./link-text"); let A11yTextWand = require("./a11y-text-wand"); +let AriaVisualizer = require('./aria-visualizer'); module.exports = { default: [ @@ -24,5 +25,6 @@ module.exports = { experimental: [ new A11yTextWand(), + new AriaVisualizer(), ], }; diff --git a/plugins/shared/annotate/index.js b/plugins/shared/annotate/index.js index d01b7bf0..38bbf715 100644 --- a/plugins/shared/annotate/index.js +++ b/plugins/shared/annotate/index.js @@ -20,6 +20,9 @@ require("./style.less"); // and across. const MIN_HIGHLIGHT_SIZE = 25; +// typecast to jQuery collection in case our plugin is jquery-less +const ensureJqueryCollection = (el) => (el instanceof $) ? el : $(el); + // Polyfill fallback for IE < 10 window.requestAnimationFrame = window.requestAnimationFrame || function(callback) { @@ -102,7 +105,9 @@ module.exports = (namespace) => { return { // Places a small label in the top left corner of a given jQuery // element. By default, this label contains the element's tagName. - label($el, text=$el.prop("tagName").toLowerCase()) { + label(el, text=$el.prop("tagName").toLowerCase()) { + const $el = ensureJqueryCollection(el); + let $label = createAnnotation($el, "tota11y-label"); return $label.html(text); }, @@ -115,7 +120,9 @@ module.exports = (namespace) => { // object will contain a "show()" method when the info panel is // rendered, allowing us to externally open the entry in the info // panel corresponding to this error. - errorLabel($el, text, expanded, errorEntry) { + errorLabel(el, text, expanded, errorEntry) { + const $el = ensureJqueryCollection(el); + let $innerHtml = $(errorLabelTemplate({ text: text, detail: expanded, @@ -143,7 +150,9 @@ module.exports = (namespace) => { // Highlights a given jQuery element by placing a translucent // rectangle directly over it - highlight($el) { + highlight(el) { + const $el = ensureJqueryCollection(el); + let $highlight = createAnnotation($el, "tota11y-highlight"); return $highlight.css({ // include margins @@ -154,7 +163,9 @@ module.exports = (namespace) => { // Toggles a highlight on a given jQuery element `$el` when `$trigger` // is hovered (mouseenter/mouseleave) or focused (focus/blur) - toggleHighlight($el, $trigger) { + toggleHighlight(el, $trigger) { + const $el = ensureJqueryCollection(el); + let $highlight; $trigger.on("mouseenter focus", () => { diff --git a/plugins/shared/info-panel/index.js b/plugins/shared/info-panel/index.js index 6cfdc8b9..c4df4692 100644 --- a/plugins/shared/info-panel/index.js +++ b/plugins/shared/info-panel/index.js @@ -23,10 +23,12 @@ require("./style.less"); const INITIAL_PANEL_MARGIN_PX = 10; const COLLAPSED_CLASS_NAME = "tota11y-collapsed"; const HIDDEN_CLASS_NAME = "tota11y-info-hidden"; +const STATUS_PANEL_VIEW_CLASS_NAME = "tota11y-info-status-panel-view"; class InfoPanel { - constructor(plugin) { + constructor(plugin, options = {}) { this.plugin = plugin; + this.options = options; this.about = null; this.summary = null; @@ -179,6 +181,23 @@ class InfoPanel { }); } + renderAnnotationCheckbox() { + if (!this.options.disableAnnotation) { + return ( + + ); + } + + return null; + } + render() { // Destroy the existing info panel to prevent double-renders if (this.$el) { @@ -187,19 +206,18 @@ class InfoPanel { let hasContent = false; + const classNames = ["tota11y", "tota11y-info"]; + + if (this.options.statusPanelView) { + classNames.push(STATUS_PANEL_VIEW_CLASS_NAME); + } + this.$el = ( -
+
{this.plugin.getTitle()} - + {this.renderAnnotationCheckbox()} diff --git a/plugins/shared/info-panel/style.less b/plugins/shared/info-panel/style.less index 8337abe4..c7e8e76e 100644 --- a/plugins/shared/info-panel/style.less +++ b/plugins/shared/info-panel/style.less @@ -3,6 +3,8 @@ @panelBodyWidth: 400px; @panelBodyHeight: 270px; +@statusPanelHeight: 35px; + @tabHoverColor: #555; @tabActiveColor: @white; @@ -86,12 +88,20 @@ &.active &-anchor-text { color: @darkGray; } + + .tota11y-info-status-panel-view & { + display: none; + } } &-sections { position: relative; height: @panelBodyHeight; width: @panelBodyWidth; + + .tota11y-info-status-panel-view & { + height: @statusPanelHeight; + } } &-section { @@ -109,6 +119,10 @@ &.active { display: block; } + + .tota11y-info-status-panel-view & { + overflow: hidden; + } } &-errors { diff --git a/test/index.html b/test/index.html index 35fc2586..547e8ec6 100644 --- a/test/index.html +++ b/test/index.html @@ -60,6 +60,12 @@

Hello, world!

Continue

+
+

This paragraph is visible.

+

This paragraph has an span.

+ +
+

jordan