diff --git a/devtools/client/accessibility/accessibility-startup.js b/devtools/client/accessibility/accessibility-startup.js index dbfa89432dfe..3520ff9af336 100644 --- a/devtools/client/accessibility/accessibility-startup.js +++ b/devtools/client/accessibility/accessibility-startup.js @@ -59,11 +59,13 @@ class AccessibilityStartup { this._supports.snapshot, this._supports.audit, this._supports.hydration, + this._supports.simulation, ] = await Promise.all([ this.target.actorHasMethod("accessible", "getRelations"), this.target.actorHasMethod("accessible", "snapshot"), this.target.actorHasMethod("accessible", "audit"), this.target.actorHasMethod("accessible", "hydrate"), + this.target.actorHasMethod("accessibility", "getSimulator"), ]); await this._accessibility.bootstrap(); diff --git a/devtools/client/accessibility/accessibility-view.js b/devtools/client/accessibility/accessibility-view.js index 4ba5b6623eb0..74af66097982 100644 --- a/devtools/client/accessibility/accessibility-view.js +++ b/devtools/client/accessibility/accessibility-view.js @@ -47,19 +47,28 @@ AccessibilityView.prototype = { * Initialize accessibility view, create its top level component and set the * data store. * - * @param {Object} accessibility front that can initialize accessibility + * @param {Object} + * Object that contains the following properties: + * - front {Object} + * front that can initialize accessibility * walker and enable/disable accessibility * services. - * @param {Object} walker front for accessibility walker actor responsible for + * - walker {Object} + * front for accessibility walker actor responsible for * managing accessible objects actors/fronts. - * @param {JSON} supports a collection of flags indicating which accessibility + * - supports {JSON} + * a collection of flags indicating which accessibility * panel features are supported by the current serverside * version. - * @param {Array} fluentBundles array of FluentBundles elements for localization + * - fluentBundles {Array} + * array of FluentBundles elements for localization + * - simulator {Object} + * front for simulator actor responsible for setting + * color matrices in docShell */ - async initialize(accessibility, walker, supports, fluentBundles) { + async initialize({ front, walker, supports, fluentBundles, simulator }) { // Make sure state is reset every time accessibility panel is initialized. - await this.store.dispatch(reset(accessibility, supports)); + await this.store.dispatch(reset(front, supports)); const container = document.getElementById("content"); if (!supports.enableDisable) { @@ -68,9 +77,10 @@ AccessibilityView.prototype = { } const mainFrame = MainFrame({ - accessibility, + accessibility: front, accessibilityWalker: walker, fluentBundles, + simulator, }); // Render top level component const provider = createElement(Provider, { store: this.store }, mainFrame); diff --git a/devtools/client/accessibility/accessibility.css b/devtools/client/accessibility/accessibility.css index c3601b1fc1f1..13805e451f04 100644 --- a/devtools/client/accessibility/accessibility.css +++ b/devtools/client/accessibility/accessibility.css @@ -140,13 +140,13 @@ body { margin-inline: 5px; } -.devtools-toolbar .accessibility-tree-filters { +.devtools-toolbar .accessibility-tree-filters, +.devtools-toolbar .accessibility-simulation { display: flex; flex-wrap: nowrap; flex-direction: row; align-items: center; white-space: nowrap; - margin-inline-end: 5px; } .devtools-toolbar .toolbar-menu-button { @@ -156,12 +156,21 @@ body { .devtools-toolbar .toolbar-menu-button.filters { max-width: 100px; +} + +.devtools-toolbar .toolbar-menu-button.simulation { + max-width: 200px; +} + +.devtools-toolbar .toolbar-menu-button.filters, +.devtools-toolbar .toolbar-menu-button.simulation { text-overflow: ellipsis; overflow-x: hidden; margin-inline-start: 3px; } -.devtools-toolbar .toolbar-menu-button::after { +.devtools-toolbar .toolbar-menu-button::after, +.devtools-toolbar .toolbar-menu-button.simulation::before { content: ""; display: inline-block; -moz-context-properties: fill; @@ -169,7 +178,8 @@ body { margin-inline-start: 3px; } -.devtools-toolbar .toolbar-menu-button.filters::after { +.devtools-toolbar .toolbar-menu-button.filters::after, +.devtools-toolbar .toolbar-menu-button.simulation::after { background: url("chrome://devtools/skin/images/select-arrow.svg") no-repeat; width: 8px; height: 8px; @@ -286,22 +296,26 @@ body { margin: auto; } -.description .link { +.description .link, +.accessibility-check-annotation .link { color: var(--accessibility-link-color); cursor: pointer; outline: 0; } -.description .link:hover:not(:focus) { +.description .link:hover:not(:focus), +.accessibility-check-annotation .link:hover:not(:focus) { text-decoration: underline; } -.description .link:focus:not(:active) { +.description .link:focus:not(:active), +.accessibility-check-annotation .link:focus:not(:active) { box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus), 0 0 0 4px var(--accessibility-toolbar-focus-alpha30); border-radius: 2px; } -.description .link:active { +.description .link:active, +.accessibility-check-annotation .link:active { color: var(--accessibility-link-color-active); text-decoration: underline; } @@ -764,27 +778,10 @@ body { } .accessibility-check-annotation .link { - color: var(--accessibility-link-color); - cursor: pointer; - outline: 0; white-space: nowrap; font-style: normal; } -.accessibility-check-annotation .link:hover:not(:focus) { - text-decoration: underline; -} - -.accessibility-check-annotation .link:focus:not(:active) { - box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus), 0 0 0 4px var(--accessibility-toolbar-focus-alpha30); - border-radius: 2px; -} - -.accessibility-check-annotation .link:active { - color: var(--accessibility-link-color-active); - text-decoration: underline; -} - .accessibility-color-contrast .accessibility-contrast-value:not(:empty) { margin-block-end: 4px; } @@ -815,3 +812,35 @@ body { .accessibility-color-contrast .accessibility-color-contrast-separator:before { margin-inline-end: 3px; } + +.devtools-toolbar .toolbar-menu-button.simulation::before { + width: 12px; + height: 12px; + margin-inline-end: 3px; + margin-inline-start: 0px; + background: url("chrome://devtools/skin/images/eye.svg") no-repeat; + -moz-context-properties: fill, stroke; + fill: var(--theme-icon-color); + stroke: var(--theme-icon-color); + vertical-align: -2px; +} + +.devtools-toolbar .toolbar-menu-button.active, +.devtools-toolbar .toolbar-menu-button.active.devtools-button:not(:empty):not(.checked):not(:disabled):focus { + color: var(--theme-toolbar-selected-color); +} + +.devtools-toolbar .toolbar-menu-button.simulation.active::before { + fill: var(--theme-toolbar-selected-color); + stroke: var(--theme-toolbar-selected-color); +} + +#simulation-menu-button-menu .link { + background-color: transparent; + border: none; +} + +#simulation-menu-button-menu .link:focus, +#simulation-menu-button-menu .link:hover { + background-color: var(--theme-arrowpanel-dimmed); +} diff --git a/devtools/client/accessibility/actions/moz.build b/devtools/client/accessibility/actions/moz.build index 808b6f55cd4e..887fc12ab06e 100644 --- a/devtools/client/accessibility/actions/moz.build +++ b/devtools/client/accessibility/actions/moz.build @@ -6,5 +6,6 @@ DevToolsModules( 'accessibles.js', 'audit.js', 'details.js', + 'simulation.js', 'ui.js' ) diff --git a/devtools/client/accessibility/actions/simulation.js b/devtools/client/accessibility/actions/simulation.js new file mode 100644 index 000000000000..0ebea083b376 --- /dev/null +++ b/devtools/client/accessibility/actions/simulation.js @@ -0,0 +1,13 @@ +/* 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 { SIMULATE } = require("devtools/client/accessibility/constants"); + +exports.simulate = (simulator, simTypes = []) => dispatch => + simulator + .simulate({ types: simTypes }) + .then(success => dispatch({ error: !success, simTypes, type: SIMULATE })) + .catch(error => dispatch({ error, type: SIMULATE })); diff --git a/devtools/client/accessibility/components/MainFrame.js b/devtools/client/accessibility/components/MainFrame.js index 0254c6a1ea5b..23f55b6a4807 100644 --- a/devtools/client/accessibility/components/MainFrame.js +++ b/devtools/client/accessibility/components/MainFrame.js @@ -47,6 +47,7 @@ class MainFrame extends Component { dispatch: PropTypes.func.isRequired, auditing: PropTypes.array.isRequired, supports: PropTypes.object, + simulator: PropTypes.object, }; } @@ -115,6 +116,7 @@ class MainFrame extends Component { fluentBundles, enabled, auditing, + simulator, } = this.props; if (!enabled) { @@ -128,7 +130,7 @@ class MainFrame extends Component { { bundles: fluentBundles }, div( { className: "mainFrame", role: "presentation" }, - Toolbar({ accessibility, accessibilityWalker }), + Toolbar({ accessibility, accessibilityWalker, simulator }), isAuditing && AuditProgressOverlay(), span( { diff --git a/devtools/client/accessibility/components/SimulationMenuButton.js b/devtools/client/accessibility/components/SimulationMenuButton.js new file mode 100644 index 000000000000..80172fa06c9b --- /dev/null +++ b/devtools/client/accessibility/components/SimulationMenuButton.js @@ -0,0 +1,155 @@ +/* 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"; + +// React +const { + createFactory, + Component, +} = require("devtools/client/shared/vendor/react"); +const { + hr, + span, + div, +} = require("devtools/client/shared/vendor/react-dom-factories"); +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const { L10N } = require("../utils/l10n"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const MenuButton = createFactory( + require("devtools/client/shared/components/menu/MenuButton") +); +const { openDocLink } = require("devtools/client/shared/link"); +const { + A11Y_SIMULATION_DOCUMENTATION_LINK, +} = require("devtools/client/accessibility/constants"); +const { + accessibility: { SIMULATION_TYPE }, +} = require("devtools/shared/constants"); +const actions = require("../actions/simulation"); + +loader.lazyGetter(this, "MenuItem", function() { + return createFactory( + require("devtools/client/shared/components/menu/MenuItem") + ); +}); +loader.lazyGetter(this, "MenuList", function() { + return createFactory( + require("devtools/client/shared/components/menu/MenuList") + ); +}); + +const SIMULATION_MENU_LABELS = { + NONE: "accessibility.filter.none", + [SIMULATION_TYPE.DEUTERANOMALY]: "accessibility.simulation.deuteranomaly", + [SIMULATION_TYPE.PROTANOMALY]: "accessibility.simulation.protanomaly", + [SIMULATION_TYPE.PROTANOPIA]: "accessibility.simulation.protanopia", + [SIMULATION_TYPE.DEUTERANOPIA]: "accessibility.simulation.deuteranopia", + [SIMULATION_TYPE.TRITANOPIA]: "accessibility.simulation.tritanopia", + [SIMULATION_TYPE.TRITANOMALY]: "accessibility.simulation.tritanomaly", + [SIMULATION_TYPE.CONTRAST_LOSS]: "accessibility.simulation.contrastLoss", + DOCUMENTATION: "accessibility.documentation.label", +}; + +class SimulationMenuButton extends Component { + static get propTypes() { + return { + simulator: PropTypes.object.isRequired, + simulation: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + + this.disableSimulation = this.disableSimulation.bind(this); + } + + disableSimulation() { + const { dispatch, simulator } = this.props; + + dispatch(actions.simulate(simulator)); + } + + toggleSimulation(simKey) { + const { dispatch, simulation, simulator } = this.props; + + if (simulation[simKey]) { + this.disableSimulation(); + } else { + dispatch(actions.simulate(simulator, [simKey])); + } + } + + render() { + const { simulation } = this.props; + const simulationMenuButtonId = "simulation-menu-button"; + const toolbarLabelID = "accessibility-simulation-label"; + const currSimulation = Object.entries(simulation).find( + simEntry => simEntry[1] + ); + + const items = [ + // No simulation option + MenuItem({ + key: "simulation-none", + label: L10N.getStr(SIMULATION_MENU_LABELS.NONE), + checked: !currSimulation, + onClick: this.disableSimulation, + }), + hr(), + // Simulation options + ...Object.keys(SIMULATION_TYPE).map(simType => + MenuItem({ + key: `simulation-${simType}`, + label: L10N.getStr(SIMULATION_MENU_LABELS[simType]), + checked: simulation[simType], + onClick: this.toggleSimulation.bind(this, simType), + }) + ), + hr(), + // Documentation link + MenuItem({ + className: "link", + key: "simulation-documentation", + label: L10N.getStr(SIMULATION_MENU_LABELS.DOCUMENTATION), + role: "link", + onClick: () => openDocLink(A11Y_SIMULATION_DOCUMENTATION_LINK), + }), + ]; + + return div( + { + role: "group", + className: "accessibility-simulation", + "aria-labelledby": toolbarLabelID, + }, + span( + { id: toolbarLabelID, role: "presentation" }, + L10N.getStr("accessibility.simulation") + ), + MenuButton( + { + id: simulationMenuButtonId, + menuId: simulationMenuButtonId + "-menu", + className: `devtools-button toolbar-menu-button simulation${ + currSimulation ? " active" : "" + }`, + doc: document, + label: L10N.getStr( + SIMULATION_MENU_LABELS[currSimulation ? currSimulation[0] : "NONE"] + ), + }, + MenuList({}, items) + ) + ); + } +} + +const mapStateToProps = ({ simulation }) => { + return { simulation }; +}; + +// Exports from this module +module.exports = connect(mapStateToProps)(SimulationMenuButton); diff --git a/devtools/client/accessibility/components/Toolbar.js b/devtools/client/accessibility/components/Toolbar.js index 0205ffd00d05..d74e2590b767 100644 --- a/devtools/client/accessibility/components/Toolbar.js +++ b/devtools/client/accessibility/components/Toolbar.js @@ -19,6 +19,9 @@ const AccessibilityTreeFilter = createFactory( require("./AccessibilityTreeFilter") ); const AccessibilityPrefs = createFactory(require("./AccessibilityPrefs")); +loader.lazyGetter(this, "SimulationMenuButton", function() { + return createFactory(require("./SimulationMenuButton")); +}); const { connect } = require("devtools/client/shared/vendor/react-redux"); const { disable, updateCanBeDisabled } = require("../actions/ui"); @@ -30,6 +33,7 @@ class Toolbar extends Component { dispatch: PropTypes.func.isRequired, accessibility: PropTypes.object.isRequired, canBeDisabled: PropTypes.bool.isRequired, + simulator: PropTypes.object, }; } @@ -72,7 +76,7 @@ class Toolbar extends Component { } render() { - const { canBeDisabled, accessibilityWalker } = this.props; + const { canBeDisabled, accessibilityWalker, simulator } = this.props; const { disabling } = this.state; const disableButtonStr = disabling ? "accessibility.disabling" @@ -88,6 +92,16 @@ class Toolbar extends Component { title = L10N.getStr("accessibility.disable.disabledTitle"); } + const optionalSimulationSection = simulator + ? [ + div({ + role: "separator", + className: "devtools-separator", + }), + SimulationMenuButton({ simulator }), + ] + : []; + return div( { className: "devtools-toolbar", @@ -118,6 +132,8 @@ class Toolbar extends Component { L10N.getStr("accessibility.beta") ), AccessibilityTreeFilter({ accessibilityWalker, describedby: betaID }), + // Simulation section is shown if webrender is enabled + ...optionalSimulationSection, AccessibilityPrefs() ); } diff --git a/devtools/client/accessibility/components/moz.build b/devtools/client/accessibility/components/moz.build index b0480651b127..bae6cd9bbed1 100644 --- a/devtools/client/accessibility/components/moz.build +++ b/devtools/client/accessibility/components/moz.build @@ -25,6 +25,7 @@ DevToolsModules( 'LearnMoreLink.js', 'MainFrame.js', 'RightSidebar.js', + 'SimulationMenuButton.js', 'TextLabelBadge.js', 'TextLabelCheck.js', 'Toolbar.js' diff --git a/devtools/client/accessibility/constants.js b/devtools/client/accessibility/constants.js index 0807b6b9d286..dc32acbee4de 100644 --- a/devtools/client/accessibility/constants.js +++ b/devtools/client/accessibility/constants.js @@ -69,6 +69,7 @@ exports.FILTER_TOGGLE = "FILTER_TOGGLE"; exports.AUDIT = "AUDIT"; exports.AUDITING = "AUDITING"; exports.AUDIT_PROGRESS = "AUDIT_PROGRESS"; +exports.SIMULATE = "SIMULATE"; // List of filters for accessibility checks. exports.FILTERS = { @@ -122,6 +123,8 @@ exports.A11Y_LEARN_MORE_LINK = exports.A11Y_CONTRAST_LEARN_MORE_LINK = "https://developer.mozilla.org/docs/Web/Accessibility/Understanding_WCAG/Perceivable/" + "Color_contrast?utm_source=devtools&utm_medium=a11y-panel-checks-color-contrast"; +exports.A11Y_SIMULATION_DOCUMENTATION_LINK = + "https://developer.mozilla.org/docs/Tools/Accessibility_inspector/Simulation"; const A11Y_TEXT_LABEL_LINK_BASE = "https://developer.mozilla.org/docs/Web/Accessibility/Understanding_WCAG/Text_labels_and_names" + diff --git a/devtools/client/accessibility/panel.js b/devtools/client/accessibility/panel.js index 27c16c977694..1678ad9c1ff8 100644 --- a/devtools/client/accessibility/panel.js +++ b/devtools/client/accessibility/panel.js @@ -94,6 +94,10 @@ AccessibilityPanel.prototype = { this.picker = new Picker(this); } + if (this.supports.simulation) { + this.simulator = await this.front.getSimulator(); + } + this.fluentBundles = await this.createFluentBundles(); this.updateA11YServiceDurationTimer(); @@ -168,13 +172,13 @@ AccessibilityPanel.prototype = { } // Alright reset the flag we are about to refresh the panel. this.shouldRefresh = false; - this.postContentMessage( - "initialize", - this.front, - this.walker, - this.supports, - this.fluentBundles - ); + this.postContentMessage("initialize", { + front: this.front, + walker: this.walker, + supports: this.supports, + fluentBundles: this.fluentBundles, + simulator: this.simulator, + }); }, updateA11YServiceDurationTimer() { diff --git a/devtools/client/accessibility/reducers/index.js b/devtools/client/accessibility/reducers/index.js index c1c8bea359cd..8092eb8a0451 100644 --- a/devtools/client/accessibility/reducers/index.js +++ b/devtools/client/accessibility/reducers/index.js @@ -6,11 +6,13 @@ const { accessibles } = require("./accessibles"); const { audit } = require("./audit"); const { details } = require("./details"); +const { simulation } = require("./simulation"); const { ui } = require("./ui"); exports.reducers = { accessibles, audit, details, + simulation, ui, }; diff --git a/devtools/client/accessibility/reducers/moz.build b/devtools/client/accessibility/reducers/moz.build index ee15ded296f5..3687ab0de0f8 100644 --- a/devtools/client/accessibility/reducers/moz.build +++ b/devtools/client/accessibility/reducers/moz.build @@ -7,5 +7,6 @@ DevToolsModules( '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 000000000000..56edf8a7e74e --- /dev/null +++ b/devtools/client/accessibility/reducers/simulation.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 { + accessibility: { SIMULATION_TYPE }, +} = require("devtools/shared/constants"); +const { SIMULATE } = require("devtools/client/accessibility/constants"); + +/** + * Initial state definition + */ +function getInitialState() { + return { + [SIMULATION_TYPE.PROTANOMALY]: false, + [SIMULATION_TYPE.DEUTERANOMALY]: false, + [SIMULATION_TYPE.TRITANOMALY]: false, + [SIMULATION_TYPE.PROTANOPIA]: false, + [SIMULATION_TYPE.DEUTERANOPIA]: false, + [SIMULATION_TYPE.TRITANOPIA]: false, + [SIMULATION_TYPE.CONTRAST_LOSS]: false, + }; +} + +function simulation(state = getInitialState(), action) { + const { error } = action; + + if (error) { + console.warn( + `Error running simulation: ${ + typeof error == "string" + ? error + : "simulate function in simulator.js returned an error" + }` + ); + return state; + } + + switch (action.type) { + case SIMULATE: + 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/test/browser/browser.ini b/devtools/client/accessibility/test/browser/browser.ini index 5d696d346716..e3ef86e8664d 100644 --- a/devtools/client/accessibility/test/browser/browser.ini +++ b/devtools/client/accessibility/test/browser/browser.ini @@ -26,6 +26,7 @@ skip-if = (os == 'linux' && debug && bits == 64) # Bug 1511247 [browser_accessibility_reload.js] [browser_accessibility_sidebar_checks.js] [browser_accessibility_sidebar.js] +[browser_accessibility_simulation.js] [browser_accessibility_tree_audit_long.js] [browser_accessibility_tree_audit_reset.js] [browser_accessibility_tree_audit_toolbar.js] diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js b/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js new file mode 100644 index 000000000000..3d122f2324a2 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global openSimulationMenu, toggleSimulationOption */ + +const { + isWebRenderEnabled, +} = require("devtools/server/actors/utils/accessibility"); + +const TEST_URI = ` +
+ +