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/focus-tracker/index.js b/plugins/focus-tracker/index.js new file mode 100644 index 00000000..43c81ae7 --- /dev/null +++ b/plugins/focus-tracker/index.js @@ -0,0 +1,97 @@ +/** + * Allows users to see what screen readers would see. + */ + +let Plugin = require("../base"); + +const FOCUS_EVENT = "focusin"; +const BLUR_EVENT = "focusout"; + +// 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 +}; + +// we're going to use focusin and focusout because they bubble +const FOCUS_STATES = { + [FOCUS_EVENT]: "tota11y-outlined", + [BLUR_EVENT]: "tota11y-was-focused" +}; + +// we'll use this to make sure we don't apply the was-focused +// indicator to our tota11y panels +const IGNORE_WAS_FOCUSED_CLASS = "tota11y"; + +// convenient method to quickly remove any classes this +// plugin applied +// it is outside of the class because it doesn't really need +// to access this and it lets us now worry about binding +// our event handlers +const removeFocusClasses = (element) => { + element.classList.remove(...Object.values(FOCUS_STATES)); +}; + +require("./style.less"); + +class FocusTracker extends Plugin { + constructor(...args) { + const options = Object.assign({}, args, { panel: PANEL_OPTIONS }); + + super(options); + } + + getTitle() { + return "Focus Tracker"; + } + + getDescription() { + return "Keep track of what's been focused as you tab through the page"; + } + + applyFocusClass(event) { + // get the event target and event name + const { target, type } = event; + + // remove any focused or was-focused indicators on the element + removeFocusClasses(target); + + // choose the class we want to add to this element + // based on whether this is the focusin or focusout event + const classToAdd = FOCUS_STATES[type]; + + // we want to ignore our tota11y toggle and panel because + // the user probably only cares about focusable elements on + // their page getting this visual treatment + if (type === FOCUS_EVENT || target.closest(`.${IGNORE_WAS_FOCUSED_CLASS}`) === null) { + target.classList.add(classToAdd); + } + } + + run() { + // pop up our info panel to let the user know what we're doing + this.summary("Tracking Focus"); + this.panel.render(); + + // dynamically apply our event listeners by looping through + // our defined focus states and adding an event handler + Object.keys(FOCUS_STATES).forEach((key) => { + document.addEventListener(key, this.applyFocusClass); + }); + } + + cleanup() { + // dynamically remove our event listeners by looping through + // our defined focus states and removing the event handler + Object.keys(FOCUS_STATES).forEach((key) => { + document.removeEventListener(key, this.applyFocusClass); + + // we'll also want to clean up all of the classes we added + [...document.querySelectorAll(`.${FOCUS_STATES[key]}`)].forEach((element) => { + removeFocusClasses(element); + }); + }); + } +} + +module.exports = FocusTracker; diff --git a/plugins/focus-tracker/style.less b/plugins/focus-tracker/style.less new file mode 100644 index 00000000..e76c4111 --- /dev/null +++ b/plugins/focus-tracker/style.less @@ -0,0 +1,9 @@ +@import "../../less/variables.less"; + +.tota11y-outlined { + outline: 2px solid fadein(@highlightColor, 100%); +} + +.tota11y-was-focused { + outline: 2px dashed fadein(@highlightColor, 80%); +} diff --git a/plugins/index.js b/plugins/index.js index b6447b9f..c2a3f157 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 FocusTracker = require("./focus-tracker"); module.exports = { default: [ @@ -24,5 +25,6 @@ module.exports = { experimental: [ new A11yTextWand(), + new FocusTracker(), ], }; diff --git a/plugins/shared/info-panel/index.js b/plugins/shared/info-panel/index.js index 6cfdc8b9..ecd64bab 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; @@ -187,8 +189,14 @@ class InfoPanel { let hasContent = false; + const classNames = ["tota11y", "tota11y-info"]; + + if (this.options.statusPanelView) { + classNames.push(STATUS_PANEL_VIEW_CLASS_NAME); + } + this.$el = ( -