summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/reducers
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/accessibility/reducers')
-rw-r--r--devtools/client/accessibility/reducers/accessibles.js158
-rw-r--r--devtools/client/accessibility/reducers/audit.js109
-rw-r--r--devtools/client/accessibility/reducers/details.js60
-rw-r--r--devtools/client/accessibility/reducers/index.js28
-rw-r--r--devtools/client/accessibility/reducers/moz.build7
-rw-r--r--devtools/client/accessibility/reducers/simulation.js52
-rw-r--r--devtools/client/accessibility/reducers/ui.js217
7 files changed, 631 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;
diff --git a/devtools/client/accessibility/reducers/audit.js b/devtools/client/accessibility/reducers/audit.js
new file mode 100644
index 0000000000..e65251ba64
--- /dev/null
+++ b/devtools/client/accessibility/reducers/audit.js
@@ -0,0 +1,109 @@
+/* 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 {
+ accessibility: { AUDIT_TYPE },
+} = require("resource://devtools/shared/constants.js");
+const {
+ AUDIT,
+ AUDITING,
+ AUDIT_PROGRESS,
+ FILTER_TOGGLE,
+ FILTERS,
+ RESET,
+ SELECT,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return {
+ filters: {
+ [FILTERS.ALL]: false,
+ [FILTERS.CONTRAST]: false,
+ [FILTERS.KEYBOARD]: false,
+ [FILTERS.TEXT_LABEL]: false,
+ },
+ auditing: [],
+ progress: null,
+ };
+}
+
+/**
+ * State with all filters active.
+ */
+function allActiveFilters() {
+ return {
+ [FILTERS.ALL]: true,
+ [FILTERS.CONTRAST]: true,
+ [FILTERS.KEYBOARD]: true,
+ [FILTERS.TEXT_LABEL]: true,
+ };
+}
+
+function audit(state = getInitialState(), action) {
+ switch (action.type) {
+ case FILTER_TOGGLE:
+ const { filter } = action;
+ let { filters } = state;
+ const isToggledToActive = !filters[filter];
+
+ if (filter === FILTERS.NONE) {
+ filters = getInitialState().filters;
+ } else if (filter === FILTERS.ALL) {
+ filters = isToggledToActive
+ ? allActiveFilters()
+ : getInitialState().filters;
+ } else {
+ filters = {
+ ...filters,
+ [filter]: isToggledToActive,
+ };
+
+ const allAuditTypesActive = Object.values(AUDIT_TYPE)
+ .filter(filterKey => filters.hasOwnProperty(filterKey))
+ .every(filterKey => filters[filterKey]);
+ if (isToggledToActive && !filters[FILTERS.ALL] && allAuditTypesActive) {
+ filters[FILTERS.ALL] = true;
+ } else if (!isToggledToActive && filters[FILTERS.ALL]) {
+ filters[FILTERS.ALL] = false;
+ }
+ }
+
+ return {
+ ...state,
+ filters,
+ };
+ case AUDITING:
+ const { auditing } = action;
+
+ return {
+ ...state,
+ auditing,
+ };
+ case AUDIT:
+ return {
+ ...state,
+ auditing: getInitialState().auditing,
+ progress: null,
+ };
+ case AUDIT_PROGRESS:
+ const { progress } = action;
+
+ return {
+ ...state,
+ progress,
+ };
+ case SELECT:
+ case RESET:
+ return getInitialState();
+ default:
+ return state;
+ }
+}
+
+exports.audit = audit;
diff --git a/devtools/client/accessibility/reducers/details.js b/devtools/client/accessibility/reducers/details.js
new file mode 100644
index 0000000000..25a1c42ed2
--- /dev/null
+++ b/devtools/client/accessibility/reducers/details.js
@@ -0,0 +1,60 @@
+/* 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 {
+ UPDATE_DETAILS,
+ RESET,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return {};
+}
+
+/**
+ * Maintain details of a current relevant accessible.
+ */
+function details(state = getInitialState(), action) {
+ switch (action.type) {
+ case UPDATE_DETAILS:
+ return onUpdateDetails(state, action);
+ case RESET:
+ return getInitialState();
+ default:
+ return state;
+ }
+}
+
+/**
+ * Handle details update for an accessible object
+ * @param {Object} state Current accessible object details.
+ * @param {Object} action Redux action object
+ * @return {Object} updated state
+ */
+function onUpdateDetails(state, action) {
+ const { accessible, response, error } = action;
+ if (error) {
+ if (!accessible.isDestroyed()) {
+ console.warn(
+ `Error fetching accessible details: `,
+ accessible.actorID,
+ error
+ );
+ }
+
+ return getInitialState();
+ }
+
+ const [DOMNode, relationObjects, audit] = response;
+ const relations = {};
+ relationObjects.forEach(({ type, targets }) => {
+ relations[type] = targets.length === 1 ? targets[0] : targets;
+ });
+ return { accessible, DOMNode, relations, audit };
+}
+
+exports.details = details;
diff --git a/devtools/client/accessibility/reducers/index.js b/devtools/client/accessibility/reducers/index.js
new file mode 100644
index 0000000000..d9ca8467ff
--- /dev/null
+++ b/devtools/client/accessibility/reducers/index.js
@@ -0,0 +1,28 @@
+/* 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 {
+ accessibles,
+} = require("resource://devtools/client/accessibility/reducers/accessibles.js");
+const {
+ audit,
+} = require("resource://devtools/client/accessibility/reducers/audit.js");
+const {
+ details,
+} = require("resource://devtools/client/accessibility/reducers/details.js");
+const {
+ simulation,
+} = require("resource://devtools/client/accessibility/reducers/simulation.js");
+const {
+ ui,
+} = require("resource://devtools/client/accessibility/reducers/ui.js");
+
+exports.reducers = {
+ accessibles,
+ audit,
+ details,
+ simulation,
+ ui,
+};
diff --git a/devtools/client/accessibility/reducers/moz.build b/devtools/client/accessibility/reducers/moz.build
new file mode 100644
index 0000000000..0c7398f397
--- /dev/null
+++ b/devtools/client/accessibility/reducers/moz.build
@@ -0,0 +1,7 @@
+# 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/.
+
+DevToolsModules(
+ "accessibles.js", "audit.js", "details.js", "index.js", "simulation.js", "ui.js"
+)
diff --git a/devtools/client/accessibility/reducers/simulation.js b/devtools/client/accessibility/reducers/simulation.js
new file mode 100644
index 0000000000..1b68331d93
--- /dev/null
+++ b/devtools/client/accessibility/reducers/simulation.js
@@ -0,0 +1,52 @@
+/* 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 {
+ accessibility: { SIMULATION_TYPE },
+} = require("resource://devtools/shared/constants.js");
+const {
+ SIMULATE,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return {
+ [SIMULATION_TYPE.ACHROMATOPSIA]: false,
+ [SIMULATION_TYPE.PROTANOPIA]: false,
+ [SIMULATION_TYPE.DEUTERANOPIA]: false,
+ [SIMULATION_TYPE.TRITANOPIA]: false,
+ [SIMULATION_TYPE.CONTRAST_LOSS]: false,
+ };
+}
+
+function simulation(state = getInitialState(), action) {
+ switch (action.type) {
+ case SIMULATE:
+ if (action.error) {
+ console.warn("Error running simulation", action.error);
+ return state;
+ }
+
+ const simTypes = action.simTypes;
+
+ if (simTypes.length === 0) {
+ return getInitialState();
+ }
+
+ const updatedState = getInitialState();
+ simTypes.forEach(simType => {
+ updatedState[simType] = true;
+ });
+
+ return updatedState;
+ default:
+ return state;
+ }
+}
+
+exports.simulation = simulation;
diff --git a/devtools/client/accessibility/reducers/ui.js b/devtools/client/accessibility/reducers/ui.js
new file mode 100644
index 0000000000..828889680c
--- /dev/null
+++ b/devtools/client/accessibility/reducers/ui.js
@@ -0,0 +1,217 @@
+/* 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,
+ ENABLE,
+ RESET,
+ SELECT,
+ HIGHLIGHT,
+ UNHIGHLIGHT,
+ UPDATE_CAN_BE_DISABLED,
+ UPDATE_CAN_BE_ENABLED,
+ UPDATE_PREF,
+ UPDATE_DETAILS,
+ PREF_KEYS,
+ PREFS,
+ UPDATE_DISPLAY_TABBING_ORDER,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+const TreeView = require("resource://devtools/client/shared/components/tree/TreeView.js");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return {
+ enabled: false,
+ canBeDisabled: true,
+ canBeEnabled: true,
+ selected: null,
+ highlighted: null,
+ expanded: new Set(),
+ [PREFS.SCROLL_INTO_VIEW]: Services.prefs.getBoolPref(
+ PREF_KEYS[PREFS.SCROLL_INTO_VIEW],
+ false
+ ),
+ tabbingOrderDisplayed: false,
+ supports: {},
+ };
+}
+
+/**
+ * Maintain ui components of the accessibility panel.
+ */
+function ui(state = getInitialState(), action) {
+ switch (action.type) {
+ case ENABLE:
+ return onToggle(state, action, true);
+ case UPDATE_CAN_BE_DISABLED:
+ return onCanBeDisabledChange(state, action);
+ case UPDATE_CAN_BE_ENABLED:
+ return onCanBeEnabledChange(state, action);
+ case UPDATE_PREF:
+ return onPrefChange(state, action);
+ case UPDATE_DETAILS:
+ return onUpdateDetails(state, action);
+ case HIGHLIGHT:
+ return onHighlight(state, action);
+ case AUDIT:
+ return onAudit(state, action);
+ case UNHIGHLIGHT:
+ return onUnhighlight(state, action);
+ case SELECT:
+ return onSelect(state, action);
+ case RESET:
+ return onReset(state, action);
+ case UPDATE_DISPLAY_TABBING_ORDER:
+ return onUpdateDisplayTabbingOrder(state, action);
+ default:
+ return state;
+ }
+}
+
+function onUpdateDetails(state) {
+ if (!state.selected) {
+ return state;
+ }
+
+ // Clear selected state that should only be set when select action is
+ // performed.
+ return Object.assign({}, state, { selected: null });
+}
+
+function onUnhighlight(state) {
+ return Object.assign({}, state, { highlighted: null });
+}
+
+function updateExpandedNodes(expanded, ancestry) {
+ expanded = new Set(expanded);
+ const path = ancestry.reduceRight((accPath, { accessible }) => {
+ accPath = TreeView.subPath(accPath, accessible.actorID);
+ expanded.add(accPath);
+ return accPath;
+ }, "");
+
+ return { path, expanded };
+}
+
+function onAudit(state, { response: ancestries, error }) {
+ if (error) {
+ console.warn("Error running audit", error);
+ return state;
+ }
+
+ let expanded = new Set(state.expanded);
+ for (const ancestry of ancestries) {
+ ({ expanded } = updateExpandedNodes(expanded, ancestry));
+ }
+
+ return {
+ ...state,
+ expanded,
+ };
+}
+
+function onHighlight(state, { accessible, response: ancestry, error }) {
+ if (error) {
+ console.warn("Error fetching ancestry", error);
+ return state;
+ }
+
+ const { expanded } = updateExpandedNodes(state.expanded, ancestry);
+ return Object.assign({}, state, { expanded, highlighted: accessible });
+}
+
+function onSelect(state, { accessible, response: ancestry, error }) {
+ if (error) {
+ console.warn("Error fetching ancestry", error);
+ return state;
+ }
+
+ const { path, expanded } = updateExpandedNodes(state.expanded, ancestry);
+ const selected = TreeView.subPath(path, accessible.actorID);
+
+ return Object.assign({}, state, { expanded, selected });
+}
+
+/**
+ * Handle "canBeDisabled" flag update for accessibility service
+ * @param {Object} state Current ui state
+ * @param {Object} action Redux action object
+ * @return {Object} updated state
+ */
+function onCanBeDisabledChange(state, { canBeDisabled }) {
+ return Object.assign({}, state, { canBeDisabled });
+}
+
+/**
+ * Handle "canBeEnabled" flag update for accessibility service
+ * @param {Object} state Current ui state.
+ * @param {Object} action Redux action object
+ * @return {Object} updated state
+ */
+function onCanBeEnabledChange(state, { canBeEnabled }) {
+ return Object.assign({}, state, { canBeEnabled });
+}
+
+/**
+ * Handle pref update for accessibility panel.
+ * @param {Object} state Current ui state.
+ * @param {Object} action Redux action object
+ * @return {Object} updated state
+ */
+function onPrefChange(state, { name, value }) {
+ return {
+ ...state,
+ [name]: value,
+ };
+}
+
+/**
+ * Handle reset action for the accessibility panel UI.
+ * @param {Object} state Current ui state.
+ * @param {Object} action Redux action object
+ * @return {Object} updated state
+ */
+function onReset(state, { enabled, canBeDisabled, canBeEnabled, supports }) {
+ const newState = {
+ ...getInitialState(),
+ enabled,
+ canBeDisabled,
+ canBeEnabled,
+ supports,
+ };
+
+ return newState;
+}
+
+/**
+ * Handle accessibilty service enabling/disabling.
+ * @param {Object} state Current accessibility services enabled state.
+ * @param {Object} action Redux action object
+ * @param {Boolean} enabled New enabled state.
+ * @return {Object} updated state
+ */
+function onToggle(state, { error }, enabled) {
+ if (error) {
+ console.warn("Error enabling accessibility service: ", error);
+ return state;
+ }
+
+ return Object.assign({}, state, { enabled });
+}
+
+function onUpdateDisplayTabbingOrder(state, { error, tabbingOrderDisplayed }) {
+ if (error) {
+ console.warn("Error updating displaying tabbing order: ", error);
+ return state;
+ }
+
+ return Object.assign({}, state, { tabbingOrderDisplayed });
+}
+
+exports.ui = ui;