From dcec4252fa97634c82c1a42c50de0874fa1b4c59 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Wed, 20 Mar 2019 23:41:51 -0400 Subject: [PATCH 1/3] feat(plugin): add aria-* visualizer plugin --- package-lock.json | 28 ++++++++++++---- plugins/aria-visualizer/index.js | 50 ++++++++++++++++++++++++++++ plugins/aria-visualizer/style.less | 5 +++ plugins/base.js | 4 +-- plugins/index.js | 2 ++ plugins/shared/info-panel/index.js | 38 +++++++++++++++------ plugins/shared/info-panel/style.less | 14 ++++++++ test/index.html | 6 ++++ 8 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 plugins/aria-visualizer/index.js create mode 100644 plugins/aria-visualizer/style.less diff --git a/package-lock.json b/package-lock.json index 8162c18c..f4143745 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" @@ -4287,17 +4289,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4414,7 +4419,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4426,6 +4432,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4440,6 +4447,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4447,12 +4455,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4471,6 +4481,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4551,7 +4562,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4563,6 +4575,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4684,6 +4697,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/plugins/aria-visualizer/index.js b/plugins/aria-visualizer/index.js new file mode 100644 index 00000000..38ff260b --- /dev/null +++ b/plugins/aria-visualizer/index.js @@ -0,0 +1,50 @@ +/** + * Allows users to see what screen readers would see. + */ + +let Plugin = require("../base"); + +// this will let us get a shorter info panel that just +// lets the user know we are tracking their focus +const PANEL_OPTIONS = { + statusPanelView: true, + disableAnnotation: true, +}; + +require("./style.less"); + +class AriaVisualizer extends Plugin { + constructor(...args) { + const options = Object.assign({}, args, { panel: PANEL_OPTIONS }); + + super(options); + } + + getTitle() { + return "aria-* Visualizer"; + } + + getDescription() { + return "See the effects of your aria-* attributes"; + } + + run() { + // pop up our info panel to let the user know what we're doing + this.summary("Visualizing aria-*"); + this.panel.render(); + + [...document.querySelectorAll("[aria-hidden=\"true\"]:not(.tota11y)")].forEach((element) => { + if (!element.closest(".tota11y")) { + element.classList.add("tota11y-aria-hidden-visualized"); + } + }); + } + + cleanup() { + [...document.querySelectorAll(".tota11y-aria-hidden-visualized")].forEach((element) => { + element.classList.remove("tota11y-aria-hidden-visualized"); + }); + } +} + +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/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..4f7b4989 100644 --- a/test/index.html +++ b/test/index.html @@ -60,6 +60,12 @@

Hello, world!

Continue

+
+

This paragraph is visible.

+

This paragraph has a span.

+ +
+

jordan From a7a728a553adad9363e8b8ddfd388b8e2d4fd98f Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Thu, 21 Mar 2019 11:45:04 -0400 Subject: [PATCH 2/3] feat(plugin): update AriaVisualizer to support role annotations --- plugins/aria-visualizer/index.js | 78 +++++++++++++++++++++++++++----- plugins/shared/annotate/index.js | 19 ++++++-- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/plugins/aria-visualizer/index.js b/plugins/aria-visualizer/index.js index 38ff260b..4c8f043a 100644 --- a/plugins/aria-visualizer/index.js +++ b/plugins/aria-visualizer/index.js @@ -4,20 +4,41 @@ let Plugin = require("../base"); +let annotate = require("../shared/annotate")("roles"); + // this will let us get a shorter info panel that just // lets the user know we are tracking their focus const PANEL_OPTIONS = { - statusPanelView: true, - disableAnnotation: true, + 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 }); + const options = Object.assign({}, args, { panel: PANEL_OPTIONS }); + + super(options); + + this.ariaAttributes = ATTRIBUTES.reduce((functionMap, attribute) => { + const methodSuffix = formatAttributeForMethodName(attribute); - super(options); + functionMap[attribute] = { + enable: this[`start${methodSuffix}`], + disable: this[`stop${methodSuffix}`] + }; + + return functionMap; + }, {}); } getTitle() { @@ -25,24 +46,57 @@ class AriaVisualizer extends Plugin { } getDescription() { - return "See the effects of your aria-* attributes"; + return "See the effects of your aria-* and other a11y attributes"; + } + + startAriaHidden(attribute) { + const ariaName = `aria-${attribute}`; + + [...document.querySelectorAll(`[${ariaName}="true"]:not(.tota11y)`)].forEach((element) => { + if (!element.closest(".tota11y")) { + element.classList.add(`tota11y-${ariaName}-visualized`); + } + }); + } + + stopAriaHidden(attribute) { + const ariaName = `aria-${attribute}`; + + const className = `tota11y-${ariaName}-visualized`; + + [...document.querySelectorAll(`.${className}`)].forEach((element) => { + element.classList.remove(className); + }); + } + + startRole(attribute) { + [...document.querySelectorAll(`[${attribute}]:not(.tota11y)`)].forEach((element) => { + 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 aria-*"); + this.summary("Visualizing a11y attributes"); this.panel.render(); - [...document.querySelectorAll("[aria-hidden=\"true\"]:not(.tota11y)")].forEach((element) => { - if (!element.closest(".tota11y")) { - element.classList.add("tota11y-aria-hidden-visualized"); - } + Object.keys(this.ariaAttributes).forEach((property) => { + this.ariaAttributes[property].enable(property); }); } cleanup() { - [...document.querySelectorAll(".tota11y-aria-hidden-visualized")].forEach((element) => { - element.classList.remove("tota11y-aria-hidden-visualized"); + Object.keys(this.ariaAttributes).forEach((property) => { + this.ariaAttributes[property].disable(property); }); } } 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", () => { From 968f87b46923a58f9a1ad444a8c7892d831eb599 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Sun, 19 May 2019 10:13:26 -0400 Subject: [PATCH 3/3] fix(plugin): fix issue with aria-hidden visualization --- package-lock.json | 14 +++----------- plugins/aria-visualizer/index.js | 22 +++++++++++----------- test/index.html | 4 ++-- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4143745..72742caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4289,8 +4289,7 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", @@ -4301,8 +4300,7 @@ "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4419,8 +4417,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4432,7 +4429,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4462,7 +4458,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4481,7 +4476,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4575,7 +4569,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4697,7 +4690,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/plugins/aria-visualizer/index.js b/plugins/aria-visualizer/index.js index 4c8f043a..a13b0ea4 100644 --- a/plugins/aria-visualizer/index.js +++ b/plugins/aria-visualizer/index.js @@ -2,12 +2,12 @@ * Allows users to see what screen readers would see. */ -let Plugin = require("../base"); +const Plugin = require("../base"); -let annotate = require("../shared/annotate")("roles"); +const annotate = require("../shared/annotate")("roles"); // this will let us get a shorter info panel that just -// lets the user know we are tracking their focus +// lets the user know we are visualizing a Screen Reader View const PANEL_OPTIONS = { statusPanelView: true }; @@ -42,27 +42,26 @@ class AriaVisualizer extends Plugin { } getTitle() { - return "aria-* Visualizer"; + return "Screen Reader View"; } getDescription() { - return "See the effects of your aria-* and other a11y attributes"; + return "View the page as if you were a Screen Reader. See the effects of aria-* and other a11y attributes."; } startAriaHidden(attribute) { - const ariaName = `aria-${attribute}`; + const className = `tota11y-${attribute}-visualized`; - [...document.querySelectorAll(`[${ariaName}="true"]:not(.tota11y)`)].forEach((element) => { + [...document.querySelectorAll(`[${attribute}="true"]:not(.tota11y)`)].forEach((element) => { + // make sure we aren't visualizing our tota11y DOM if (!element.closest(".tota11y")) { - element.classList.add(`tota11y-${ariaName}-visualized`); + element.classList.add(className); } }); } stopAriaHidden(attribute) { - const ariaName = `aria-${attribute}`; - - const className = `tota11y-${ariaName}-visualized`; + const className = `tota11y-${attribute}-visualized`; [...document.querySelectorAll(`.${className}`)].forEach((element) => { element.classList.remove(className); @@ -71,6 +70,7 @@ class AriaVisualizer extends Plugin { 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, diff --git a/test/index.html b/test/index.html index 4f7b4989..547e8ec6 100644 --- a/test/index.html +++ b/test/index.html @@ -62,8 +62,8 @@

Hello, world!

This paragraph is visible.

-

This paragraph has a span.

- +

This paragraph has an span.

+