Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin): add aria-* visualizer plugin #142

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 104 additions & 0 deletions plugins/aria-visualizer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Allows users to see what screen readers would see.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment!


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] = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably a much better way to handle this, but I wasn't sure how we'd deal with all the various different approaches you might need to take to visualizing the semantics of some of the aria-* properties.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not certain yet how we'd do that either. Might be something that evolves a bit as we add support for different aria-* features.

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;
5 changes: 5 additions & 0 deletions plugins/aria-visualizer/style.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import "../../less/variables.less";

.tota11y-aria-hidden-visualized {
visibility: hidden !important;
}
4 changes: 2 additions & 2 deletions plugins/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -24,5 +25,6 @@ module.exports = {

experimental: [
new A11yTextWand(),
new AriaVisualizer(),
],
};
19 changes: 15 additions & 4 deletions plugins/shared/annotate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
},
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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", () => {
Expand Down
38 changes: 28 additions & 10 deletions plugins/shared/info-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -179,6 +181,23 @@ class InfoPanel {
});
}

renderAnnotationCheckbox() {
if (!this.options.disableAnnotation) {
return (
<label className="tota11y-info-annotation-toggle">
Annotate:
{" "}
<input
className="toggle-annotation"
type="checkbox"
checked="checked" />
</label>
);
}

return null;
}

render() {
// Destroy the existing info panel to prevent double-renders
if (this.$el) {
Expand All @@ -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 = (
<div className="tota11y tota11y-info" tabindex="-1">
<div className={classNames.join(" ")} tabindex="-1">
<header className="tota11y-info-title">
{this.plugin.getTitle()}
<span className="tota11y-info-controls">
<label className="tota11y-info-annotation-toggle">
Annotate:
{" "}
<input
className="toggle-annotation"
type="checkbox"
checked="checked" />
</label>
{this.renderAnnotationCheckbox()}
<a aria-label="Close info panel"
href="#"
className="tota11y-info-dismiss-trigger">
Expand Down
14 changes: 14 additions & 0 deletions plugins/shared/info-panel/style.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
@panelBodyWidth: 400px;
@panelBodyHeight: 270px;

@statusPanelHeight: 35px;

@tabHoverColor: #555;
@tabActiveColor: @white;

Expand Down Expand Up @@ -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 {
Expand All @@ -109,6 +119,10 @@
&.active {
display: block;
}

.tota11y-info-status-panel-view & {
overflow: hidden;
}
}

&-errors {
Expand Down
6 changes: 6 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ <h1>Hello, world!</h1>

<p><a class="btn btn-primary btn-lg btn-success" href="#" role="button">Continue</a></p>

<article id="article">
<p>This paragraph is visible.</p>
<p id="p-with-hidden">This paragraph has an <span id="span" aria-hidden="true">aria-hidden</span> span.</p>
<p id="hidden-p" aria-hidden="true">This paragraph is aria-hidden.</p>
</article>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't currently working

image

I turn on the visualizer but they aren't getting hidden (also, can this text say aria-hidden instead of hidden so it's clear what we mean?

Perhaps it's just that this draft doesn't do that bit yet. Sorry if I'm jumping ahead here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was an error on my part. Should be working as expected after the next push.


<p>
<img width="40px" height="40px" src="http://s.gravatar.com/avatar/3517596df161030c3779c532f7844383?s=256" alt="jordan">
<img width="40px" height="40px" src="http://s.gravatar.com/avatar/3517596df161030c3779c532f7844383?s=256" alt="">
Expand Down