summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/reducers/accessibles.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/accessibility/reducers/accessibles.js')
-rw-r--r--devtools/client/accessibility/reducers/accessibles.js158
1 files changed, 158 insertions, 0 deletions
diff --git a/devtools/client/accessibility/reducers/accessibles.js b/devtools/client/accessibility/reducers/accessibles.js
new file mode 100644
index 0000000000..908d1b734d
--- /dev/null
+++ b/devtools/client/accessibility/reducers/accessibles.js
@@ -0,0 +1,158 @@
+/* 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/. */
+"use strict";
+
+const {
+ AUDIT,
+ FETCH_CHILDREN,
+ HIGHLIGHT,
+ RESET,
+ SELECT,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return new Map();
+}
+
+/**
+ * Maintain a cache of received accessibles responses from the backend.
+ */
+function accessibles(state = getInitialState(), action) {
+ switch (action.type) {
+ case FETCH_CHILDREN:
+ return onReceiveChildren(state, action);
+ case HIGHLIGHT:
+ case SELECT:
+ return onReceiveAncestry(state, action);
+ case AUDIT:
+ return onAudit(state, action);
+ case RESET:
+ return getInitialState();
+ default:
+ return state;
+ }
+}
+
+function getActorID(accessible) {
+ return accessible.actorID || accessible._form?.actor;
+}
+
+/**
+ * If accessible is cached recursively remove all its children and remove itself
+ * from cache.
+ * @param {Map} cache Previous state maintaining a cache of previously
+ * fetched accessibles.
+ * @param {Object} accessible Accessible object to remove from cache.
+ */
+function cleanupChild(cache, accessible) {
+ const actorID = getActorID(accessible);
+ const cached = cache.get(actorID);
+ if (!cached) {
+ return;
+ }
+
+ for (const child of cached.children) {
+ cleanupChild(cache, child);
+ }
+
+ cache.delete(actorID);
+}
+
+/**
+ * Determine if accessible in cache is stale. Accessible object is stale if its
+ * cached children array has the size other than the value of its childCount
+ * property that updates on accessible actor event.
+ * @param {Map} cache Previous state maintaining a cache of previously
+ * fetched accessibles.
+ * @param {Object} accessible Accessible object to test for staleness.
+ */
+function staleChildren(cache, accessible) {
+ const cached = cache.get(getActorID(accessible));
+ if (!cached) {
+ return false;
+ }
+
+ return cached.children.length !== accessible.childCount;
+}
+
+function updateChildrenCache(cache, accessible, children) {
+ const actorID = getActorID(accessible);
+
+ if (cache.has(actorID)) {
+ const cached = cache.get(actorID);
+ for (const child of cached.children) {
+ // If exhisting children cache includes an accessible that is not present
+ // any more or if child accessible is stale remove it and all its children
+ // from cache.
+ if (!children.includes(child) || staleChildren(cache, child)) {
+ cleanupChild(cache, child);
+ }
+ }
+ cached.children = children;
+ cache.set(actorID, cached);
+ } else {
+ cache.set(actorID, { children });
+ }
+
+ return cache;
+}
+
+function updateAncestry(cache, ancestry) {
+ ancestry.forEach(({ accessible, children }) =>
+ updateChildrenCache(cache, accessible, children)
+ );
+
+ return cache;
+}
+
+/**
+ * Handles fetching of accessible children.
+ * @param {Map} cache Previous state maintaining a cache of previously
+ * fetched accessibles.
+ * @param {Object} action Redux action object.
+ * @return {Object} updated state
+ */
+function onReceiveChildren(cache, action) {
+ const { error, accessible, response: children } = action;
+ if (!error) {
+ return updateChildrenCache(new Map(cache), accessible, children);
+ }
+
+ if (!accessible.isDestroyed()) {
+ console.warn(`Error fetching children: `, error);
+ return cache;
+ }
+
+ const newCache = new Map(cache);
+ cleanupChild(newCache, accessible);
+ return newCache;
+}
+
+function onReceiveAncestry(cache, action) {
+ const { error, response: ancestry } = action;
+ if (error) {
+ console.warn(`Error fetching ancestry: `, error);
+ return cache;
+ }
+
+ return updateAncestry(new Map(cache), ancestry);
+}
+
+function onAudit(cache, action) {
+ const { error, response: ancestries } = action;
+ if (error) {
+ console.warn(`Error performing an audit: `, error);
+ return cache;
+ }
+
+ const newCache = new Map(cache);
+ ancestries.forEach(ancestry => updateAncestry(newCache, ancestry));
+
+ return newCache;
+}
+
+exports.accessibles = accessibles;