diff options
Diffstat (limited to 'devtools/client/accessibility/reducers')
-rw-r--r-- | devtools/client/accessibility/reducers/accessibles.js | 158 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/audit.js | 109 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/details.js | 60 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/index.js | 28 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/moz.build | 7 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/simulation.js | 52 | ||||
-rw-r--r-- | devtools/client/accessibility/reducers/ui.js | 217 |
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; |