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 = ` + + + Accessibility Simulations Test + + +

+ Top level header +

+

+ Second level header +

+ +`; + +if (!isWebRenderEnabled(window)) { + addA11YPanelTask( + "Test that simulation menu button does not exist without WebRender.", + TEST_URI, + async function({ doc }) { + ok( + !doc.querySelector("#simulation-menu-button"), + "Simulation menu is not shown without WebRender." + ); + } + ); +} else { + /** + * Test data has the format of: + * { + * desc {String} description for better logging + * setup {Function} An optional setup that needs to be performed before + * the state of the simulation components can be checked. + * expected {JSON} An expected states for the simulation components. + * } + */ + const tests = [ + { + desc: + "Check that the menu button is inactivate and the menu is closed initially.", + expected: { + simulation: { + buttonActive: false, + }, + }, + }, + { + desc: + "Clicking the menu button shows the menu with No Simulation selected.", + setup: async ({ doc }) => { + await openSimulationMenu(doc); + }, + expected: { + simulation: { + buttonActive: false, + checkedOptionIndices: [0], + }, + }, + }, + { + desc: + "Selecting an option renders the menu button active and closes the menu.", + setup: async ({ doc }) => { + await toggleSimulationOption(doc, 2); + }, + expected: { + simulation: { + buttonActive: true, + checkedOptionIndices: [2], + }, + }, + }, + { + desc: "Reopening the menu preserves the previously selected option.", + setup: async ({ doc }) => { + await openSimulationMenu(doc); + }, + expected: { + simulation: { + buttonActive: true, + checkedOptionIndices: [2], + }, + }, + }, + { + desc: + "Unselecting the option renders the button inactive and closes the menu.", + setup: async ({ doc }) => { + await toggleSimulationOption(doc, 2); + }, + expected: { + simulation: { + buttonActive: false, + checkedOptionIndices: [0], + }, + }, + }, + ]; + + /** + * Test that checks state of simulation button and menu when + * options are selected/unselected with web render enabled. + */ + addA11yPanelTestsTask( + tests, + TEST_URI, + "Test selecting and unselecting simulation options updates UI." + ); +} diff --git a/devtools/client/accessibility/test/browser/head.js b/devtools/client/accessibility/test/browser/head.js index df96d3575340..ad31926d898c 100644 --- a/devtools/client/accessibility/test/browser/head.js +++ b/devtools/client/accessibility/test/browser/head.js @@ -6,7 +6,7 @@ /* global waitUntilState, gBrowser */ /* exported addTestTab, checkTreeState, checkSidebarState, checkAuditState, selectRow, - toggleRow, toggleMenuItem, addA11yPanelTestsTask, reload, navigate */ + toggleRow, toggleMenuItem, addA11yPanelTestsTask, reload, navigate, openSimulationMenu, toggleSimulationOption */ "use strict"; @@ -36,6 +36,8 @@ const { // Enable the Accessibility panel Services.prefs.setBoolPref("devtools.accessibility.enabled", true); +const SIMULATION_MENU_BUTTON_ID = "#simulation-menu-button"; + /** * Enable accessibility service and wait for a11y init event. * @return {Object} instance of accessibility service. @@ -470,6 +472,42 @@ async function checkToolbarState(doc, activeToolbarFilters) { ok(hasExpectedStructure, "Toolbar state is correct."); } +/** + * Check the state of the simulation button and menu components. + * @param {Object} doc Panel document. + * @param {Object} expected Expected states of the simulation components: + * menuVisible, buttonActive, checkedOptionIndices (Optional) + */ +async function checkSimulationState(doc, expected) { + const { buttonActive, checkedOptionIndices } = expected; + const simulationMenuOptions = doc + .querySelector(SIMULATION_MENU_BUTTON_ID + "-menu") + .querySelectorAll(".menuitem"); + + // Check simulation menu button state + is( + doc.querySelector(SIMULATION_MENU_BUTTON_ID).className, + `devtools-button toolbar-menu-button simulation${ + buttonActive ? " active" : "" + }`, + `Simulation menu button contains ${buttonActive ? "active" : "base"} class.` + ); + + // Check simulation menu options states, if specified + if (checkedOptionIndices) { + simulationMenuOptions.forEach((menuListItem, index) => { + const isChecked = checkedOptionIndices.includes(index); + const button = menuListItem.firstChild; + + is( + button.getAttribute("aria-checked"), + isChecked ? "true" : null, + `Simulation option ${index} is ${isChecked ? "" : "not "}selected.` + ); + }); + } +} + /** * Focus accessibility properties tree in the a11y inspector sidebar. If focused for the * first time, the tree will select first rendered node as defult selection for keyboard @@ -590,6 +628,25 @@ async function toggleMenuItem(doc, menuButtonIndex, menuItemIndex) { ); } +async function openSimulationMenu(doc) { + doc.querySelector(SIMULATION_MENU_BUTTON_ID).click(); + + await BrowserTestUtils.waitForCondition(() => + doc + .querySelector(SIMULATION_MENU_BUTTON_ID + "-menu") + .classList.contains("tooltip-visible") + ); +} + +async function toggleSimulationOption(doc, optionIndex) { + const simulationMenu = doc.querySelector(SIMULATION_MENU_BUTTON_ID + "-menu"); + simulationMenu.querySelectorAll(".menuitem")[optionIndex].firstChild.click(); + + await BrowserTestUtils.waitForCondition( + () => !simulationMenu.classList.contains("tooltip-visible") + ); +} + async function findAccessibleFor( { toolbox: { target }, panel: { walker: accessibilityWalker } }, selector @@ -648,6 +705,7 @@ async function runA11yPanelTests(tests, env) { audit, toolbarPrefValues, activeToolbarFilters, + simulation, } = expected; if (tree) { await checkTreeState(env.doc, tree); @@ -668,6 +726,10 @@ async function runA11yPanelTests(tests, env) { if (typeof audit !== "undefined") { await checkAuditState(env.store, audit); } + + if (simulation) { + await checkSimulationState(env.doc, simulation); + } } } diff --git a/devtools/client/jar.mn b/devtools/client/jar.mn index 2a6092359a76..b063681ba976 100644 --- a/devtools/client/jar.mn +++ b/devtools/client/jar.mn @@ -152,6 +152,7 @@ devtools.jar: skin/images/item-arrow-ltr.svg (themes/images/item-arrow-ltr.svg) skin/images/dropmarker.svg (themes/images/dropmarker.svg) skin/boxmodel.css (themes/boxmodel.css) + skin/images/eye.svg (themes/images/eye.svg) skin/images/geometry-editor.svg (themes/images/geometry-editor.svg) skin/images/open-inspector.svg (themes/images/open-inspector.svg) skin/images/more.svg (themes/images/more.svg) diff --git a/devtools/client/locales/en-US/accessibility.properties b/devtools/client/locales/en-US/accessibility.properties index 0d5ebe2a7ba4..6963d3e101c5 100644 --- a/devtools/client/locales/en-US/accessibility.properties +++ b/devtools/client/locales/en-US/accessibility.properties @@ -163,7 +163,8 @@ accessibility.badges=Accessibility checks # LOCALIZATION NOTE (accessibility.filter.none): A title text for the filter # that is rendered within the accessibility panel toolbar for a menu item that -# resets all filtering in tree. +# resets all filtering in tree, and for the simulation menu item that resets +# applied color matrices to the default matrix. accessibility.filter.none=None # LOCALIZATION NOTE (accessibility.filter.all2): A title text for the filter @@ -268,3 +269,38 @@ accessibility.pref.scroll.into.view.label=Scroll into view # LOCALIZATION NOTE (accessibility.documentation.label): This is the label for # the Documentation menu item. accessibility.documentation.label=Documentation… + +# LOCALIZATION NOTE (accessibility.simulation): A title text for the toolbar +# within the main accessibility panel that contains a list of simulations for +# vision deficiencies. +accessibility.simulation=Simulate: + +# LOCALIZATION NOTE (accessibility.simulation.deuteranomaly): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the deuteranomaly simulation option. +accessibility.simulation.deuteranomaly=Deuteranomaly (low green) + +# LOCALIZATION NOTE (accessibility.simulation.protanomaly): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the protanomaly simulation option. +accessibility.simulation.protanomaly=Protanomaly (low red) + +# LOCALIZATION NOTE (accessibility.simulation.protanopia): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the protanopia simulation option. +accessibility.simulation.protanopia=Protanopia (no red) + +# LOCALIZATION NOTE (accessibility.simulation.deuteranopia): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the deuteranopia simulation option. +accessibility.simulation.deuteranopia=Deuteranopia (no green) + +# LOCALIZATION NOTE (accessibility.simulation.tritanopia): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the tritanopia simulation option. +accessibility.simulation.tritanopia=Tritanopia (no blue) + +# LOCALIZATION NOTE (accessibility.simulation.tritanomaly): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the tritanomaly simulation option. +accessibility.simulation.tritanomaly=Tritanomaly (low blue) + +# LOCALIZATION NOTE (accessibility.simulation.contrastLoss): This label is shown +# in the "Simulate" menu in the accessibility panel and represent the contrast loss simulation option. +# It is also shown in the simulation menu button in the accessibility panel and represent the +# contrast loss simulation option currently selected. +accessibility.simulation.contrastLoss=Contrast loss diff --git a/devtools/client/themes/images/eye.svg b/devtools/client/themes/images/eye.svg new file mode 100644 index 000000000000..d0e964b60649 --- /dev/null +++ b/devtools/client/themes/images/eye.svg @@ -0,0 +1,8 @@ + + + + +