Skip to content

Commit

Permalink
♿️ Add accessibility focus ring tracking to browser UI
Browse files Browse the repository at this point in the history
  • Loading branch information
kierandrewett committed Jan 4, 2024
1 parent 06c67d7 commit 8cef2f0
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 0 deletions.
2 changes: 2 additions & 0 deletions base/content/browser-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ let noCallbackElements = [

"chrome://dot/content/widgets/browser-debug-hologram.js",

"chrome://dot/content/widgets/browser-a11y-ring.js",

"chrome://dot/content/widgets/browser-customizable-area.js",
"chrome://dot/content/widgets/browser-customizable-overflowable-area.js",
"chrome://dot/content/widgets/browser-contextual-element.js",
Expand Down
6 changes: 6 additions & 0 deletions base/content/browser-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ var { AccentColorManager } = ChromeUtils.importESModule(
"resource://gre/modules/AccentColorManager.sys.mjs"
);

var { AccessibilityFocus } = ChromeUtils.importESModule(
"resource://gre/modules/AccessibilityFocus.sys.mjs"
);

/**
* This is used to delay the startup of the browser
* until we have completed the delayed startup.
Expand Down Expand Up @@ -156,6 +160,8 @@ var gDotInit = {
new LightweightThemeConsumer(document);
new AccentColorManager(document);

new AccessibilityFocus(window);

// Check whether we are on Windows 8, if so apply a dark window frame if it is dark enough
if (AppConstants.platform == "win") {
if (
Expand Down
8 changes: 8 additions & 0 deletions base/content/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ var { BrowserStorage } = ChromeUtils.importESModule(
"resource:///modules/BrowserStorage.sys.mjs"
);

var { BrowserAccessibility } = ChromeUtils.importESModule(
"resource:///modules/BrowserAccessibility.sys.mjs"
);

var { NativeTitlebar } = ChromeUtils.importESModule(
"resource:///modules/NativeTitlebar.sys.mjs"
);
Expand Down Expand Up @@ -89,6 +93,9 @@ class BrowserApplication extends BrowserCustomizableArea {
/** @type {typeof BrowserStorage.prototype} */
storage = null;

/** @type {typeof BrowserAccessibility.prototype} */
accessibility = null;

/**
* Determines whether the browser session supports multiple processes
* @returns {boolean}
Expand Down Expand Up @@ -229,6 +236,7 @@ class BrowserApplication extends BrowserCustomizableArea {
this.commands = new BrowserCommands(window);
this.actions = new BrowserActions(this);
this.panels = new BrowserPanels(window);
this.accessibility = new BrowserAccessibility(window);

document.commandDispatcher.addCommandUpdater(this, "*", "*");

Expand Down
57 changes: 57 additions & 0 deletions components/accessibility/BrowserAccessibility.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export class BrowserAccessibility {
/** @type {Window} */
#win = null;

get #doc() {
return this.#win.document;
}

#a11yRingTrackerPref = "dot.a11y.track_focus_ring.enabled";

/**
* The focus ring element used to surround focused elements
* @type {BrowserA11yRing}
*/
get focusRingElement() {
return /** @type {BrowserA11yRing} */ (
this.#doc.querySelector("browser-a11y-ring") ||
this.#doc.createElement("browser-a11y-ring")
);
}

/**
* Tracks changes to the focus position in the DOM
* @param {CustomEvent<Element>} event
*/
#onFocusChange(event) {
if (this.shouldTrackFocusRing) {
this.#doc.documentElement.appendChild(this.focusRingElement);
this.focusRingElement.dispatchEvent(
new CustomEvent(event.type, { detail: event.detail })
);
}
}

/**
* Determines whether the track focus ring feature is enabled
*/
shouldTrackFocusRing() {
return Services.prefs.getBoolPref(this.#a11yRingTrackerPref, false);
}

/**
* @param {Window} win
*/
constructor(win) {
this.#win = win;

this.#doc.addEventListener(
"focuschange",
this.#onFocusChange.bind(this)
);
}
}
25 changes: 25 additions & 0 deletions components/accessibility/content/browser-a11y-ring.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

:host(browser-a11y-ring) {
border: 2px solid var(--browser-accent-color);
position: absolute;

left: var(--ring-x);
top: var(--ring-y);

width: calc(var(--ring-width) - 2px * 2);
height: calc(var(--ring-height) - 2px * 2);

z-index: 2147483647;

transition: 0.1s all;

pointer-events: none;
}

:host(browser-a11y-ring[inactive]) {
filter: contrast(0%);
border-style: dashed;
}
107 changes: 107 additions & 0 deletions components/accessibility/content/browser-a11y-ring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

class BrowserA11yRing extends MozHTMLElement {
constructor() {
super();

this.attachShadow({ mode: "open" });

this.resizeObserver = new ResizeObserver(
this.onElementResize.bind(this)
);

this.shadowRoot.appendChild(
html("link", {
rel: "stylesheet",
href: "chrome://dot/content/widgets/browser-a11y-ring.css"
})
);
}

get x() {
return parseFloat(this.style.getPropertyValue("--ring-x"));
}

set x(newValue) {
this.style.setProperty("--ring-x", newValue + "px");
}

get y() {
return parseFloat(this.style.getPropertyValue("--ring-y"));
}

set y(newValue) {
this.style.setProperty("--ring-y", newValue + "px");
}

get width() {
return parseFloat(this.style.getPropertyValue("--ring-width"));
}

set width(newValue) {
this.style.setProperty("--ring-width", newValue + "px");
}

get height() {
return parseFloat(this.style.getPropertyValue("--ring-height"));
}

set height(newValue) {
this.style.setProperty("--ring-height", newValue + "px");
}

/**
* Handles focus change events
* @param {CustomEvent<Element>} event
*/
handleEvent(event) {
this.resizeObserver.disconnect();

this.focusedElement = event.detail;
this.resizeObserver.observe(this.focusedElement);

this.onElementResize();
}

/**
* Fired when the resize observer detects an element's bounds changing
*/
onElementResize() {
const bounds = this.focusedElement.getBoundingClientRect();

this.x = bounds.x;
this.y = bounds.y;
this.width = bounds.width;
this.height = bounds.height;
}

/**
* Fired when the window changes activity
* @param {Event} event
*/
onWindowActivate(event) {
this.toggleAttribute("inactive", event.type == "deactivate");
}

connectedCallback() {
this.addEventListener("focuschange", this);
window.addEventListener("activate", this.onWindowActivate.bind(this));
window.addEventListener("deactivate", this.onWindowActivate.bind(this));
}

disconnectedCallback() {
this.removeEventListener("focuschange", this);
window.removeEventListener(
"activate",
this.onWindowActivate.bind(this)
);
window.removeEventListener(
"deactivate",
this.onWindowActivate.bind(this)
);
}
}

customElements.define("browser-a11y-ring", BrowserA11yRing);
10 changes: 10 additions & 0 deletions components/accessibility/jar.mn
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

dot.jar:
% content dot %content/ contentaccessible=yes
content/widgets/browser-a11y-ring.js (content/browser-a11y-ring.js)
content/widgets/browser-a11y-ring.css (content/browser-a11y-ring.css)
11 changes: 11 additions & 0 deletions components/accessibility/moz.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

JAR_MANIFESTS += ["jar.mn"]

EXTRA_JS_MODULES += [
"BrowserAccessibility.sys.mjs",
]
1 change: 1 addition & 0 deletions components/dev/content/dev-preferences-popout.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ class DevelopmentPreferencesPopout extends MozHTMLElement {
);
this.registerHandle("dot.tabs.loglevel", LOG_LEVELS);
this.registerHandle("dot.urlbar.debug_information.visible", false);
this.registerHandle("dot.a11y.track_focus_ring.enabled", false);

Services.prefs.addObserver("", this.observePreferences.bind(this));
}
Expand Down
1 change: 1 addition & 0 deletions components/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

DIRS += [
"about",
"accessibility",
"actions",
"commands",
"compat",
Expand Down
82 changes: 82 additions & 0 deletions modules/AccessibilityFocus.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export class AccessibilityFocus {
/** @type {Window} */
#win = null;

_focusedElement = null;
_focusChangeEventTimer = null;

/**
* The currently focused element
* @type {Element | null}
*/
get focusedElement() {
return this._focusedElement;
}

set focusedElement(newFocusElement) {
if (this._focusChangeEventTimer) {
this.#win.clearTimeout(this._focusChangeEventTimer);
}

if (newFocusElement == this._focusedElement) return;
this._focusedElement = newFocusElement;

// Delay the dispatch of the focus change so we don't
// end up with multiple events being fired at once
this._focusChangeEventTimer = this.#win.setTimeout(() => {
const evt = new CustomEvent("focuschange", {
detail: this.focusedElement
});

this.#win.document.dispatchEvent(evt);
}, 10);
}

/**
* Handles incoming focus events to the window
* @param {Event & { originalTarget: EventTarget }} event
*/
handleEvent(event) {
this.focusedElement = this.getFocusedElement();
}

/**
* Returns the focused element from an optional root
* @param {Document | ShadowRoot} root
*/
getFocusedElement(root = this.#win.document) {
const { activeElement } = root;

if (activeElement && activeElement.shadowRoot) {
return this.getFocusedElement(activeElement.shadowRoot);
}

return activeElement;
}

/**
* @param {Window} win
*/
constructor(win) {
this.#win = win;

// All events that could change focus
const events = [
"focusin",
"focusout",
"keydown",
"keyup",
"mousemove",
"mousedown",
"sizemodechange"
];

for (const ev of events) {
this.#win.addEventListener(ev, this);
}
}
}
1 change: 1 addition & 0 deletions modules/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

EXTRA_JS_MODULES += [
"AccessibilityFocus.sys.mjs",
"DevToolsServer.sys.mjs",
"DOMUtils.sys.mjs",
"DotWindowTracker.sys.mjs",
Expand Down
Loading

0 comments on commit 8cef2f0

Please sign in to comment.