diff options
Diffstat (limited to 'toolkit/components/reader')
-rw-r--r-- | toolkit/components/reader/AboutReader.sys.mjs | 242 | ||||
-rw-r--r-- | toolkit/components/reader/content/aboutReader.html | 58 | ||||
-rw-r--r-- | toolkit/components/reader/jar.mn | 2 | ||||
-rw-r--r-- | toolkit/components/reader/moz-slider.css | 43 | ||||
-rw-r--r-- | toolkit/components/reader/moz-slider.mjs | 126 | ||||
-rw-r--r-- | toolkit/components/reader/moz-slider.stories.mjs | 42 | ||||
-rw-r--r-- | toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js | 2 | ||||
-rw-r--r-- | toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js | 2 | ||||
-rw-r--r-- | toolkit/components/reader/tests/chrome/chrome.toml | 2 | ||||
-rw-r--r-- | toolkit/components/reader/tests/chrome/test_moz_slider.html | 48 |
10 files changed, 548 insertions, 19 deletions
diff --git a/toolkit/components/reader/AboutReader.sys.mjs b/toolkit/components/reader/AboutReader.sys.mjs index 75776b619f..57fa9d5b48 100644 --- a/toolkit/components/reader/AboutReader.sys.mjs +++ b/toolkit/components/reader/AboutReader.sys.mjs @@ -25,15 +25,19 @@ ChromeUtils.defineLazyGetter( "pluralRules", () => new Services.intl.PluralRules(undefined) ); +ChromeUtils.defineLazyGetter( + lazy, + "l10n", + () => new Localization(["toolkit/about/aboutReader.ftl"], true) +); const COLORSCHEME_L10N_IDS = { - auto: "about-reader-color-theme-auto", - light: "about-reader-color-theme-light", - dark: "about-reader-color-theme-dark", - sepia: "about-reader-color-theme-sepia", - contrast: "about-reader-color-theme-contrast", - gray: "about-reader-color-theme-gray", - custom: "about-reader-color-theme-custom", + auto: "about-reader-color-auto-theme", + light: "about-reader-color-light-theme", + dark: "about-reader-color-dark-theme", + sepia: "about-reader-color-sepia-theme", + contrast: "about-reader-color-contrast-theme", + gray: "about-reader-color-gray-theme", }; const CUSTOM_THEME_COLOR_INPUTS = [ @@ -201,6 +205,7 @@ export var AboutReader = function ( if (Services.prefs.getBoolPref("reader.colors_menu.enabled", false)) { doc.getElementById("regular-color-scheme").hidden = true; doc.getElementById("custom-colors-color-scheme").hidden = false; + this._setupSegmentedButton( "colors-menu-color-scheme-buttons", colorsMenuColorSchemeOptions, @@ -243,13 +248,140 @@ export var AboutReader = function ( ]; let fontType = Services.prefs.getCharPref("reader.font_type"); - this._setupSegmentedButton( - "font-type-buttons", - fontTypeOptions, - fontType, - this._setFontType.bind(this) - ); - this._setFontType(fontType); + + // Differentiates between the tick mark labels for width vs spacing controls + // for localization purposes. + const [narrowWidthLabel, wideWidthLabel] = lazy.l10n.formatMessagesSync([ + "about-reader-slider-label-width-narrow", + "about-reader-slider-label-width-wide", + ]); + const [narrowSpacingLabel, standardSpacingLabel, wideSpacingLabel] = + lazy.l10n.formatMessagesSync([ + "about-reader-slider-label-spacing-narrow", + "about-reader-slider-label-spacing-standard", + "about-reader-slider-label-spacing-wide", + ]); + + let contentWidthSliderOptions = { + min: 1, + max: 9, + ticks: 9, + tickLabels: `["${narrowWidthLabel.value}", "${wideWidthLabel.value}"]`, + l10nId: "about-reader-content-width-label", + icon: "chrome://global/skin/reader/content-width-20.svg", + }; + + let lineSpacingSliderOptions = { + min: 1, + max: 9, + ticks: 9, + tickLabels: `["${narrowSpacingLabel.value}", "${wideSpacingLabel.value}"]`, + l10nId: "about-reader-line-spacing-label", + icon: "chrome://global/skin/reader/line-spacing-20.svg", + }; + + let characterSpacingSliderOptions = { + min: 1, + max: 9, + ticks: 9, + tickLabels: `["${standardSpacingLabel.value}", "${wideSpacingLabel.value}"]`, + l10nId: "about-reader-character-spacing-label", + icon: "chrome://global/skin/reader/character-spacing-20.svg", + }; + + let wordSpacingSliderOptions = { + min: 1, + max: 9, + ticks: 9, + tickLabels: `["${standardSpacingLabel.value}", "${wideSpacingLabel.value}"]`, + l10nId: "about-reader-word-spacing-label", + icon: "chrome://global/skin/reader/word-spacing-20.svg", + }; + + let textAlignmentOptions = [ + { + l10nId: "about-reader-text-alignment-left", + groupName: "text-alignment", + value: "left", + itemClass: "left-align-button", + }, + { + l10nId: "about-reader-text-alignment-center", + groupName: "text-alignment", + value: "center", + itemClass: "center-align-button", + }, + { + l10nId: "about-reader-text-alignment-right", + groupName: "text-alignment", + value: "right", + itemClass: "right-align-button", + }, + ]; + + // If the page is rtl, reverse order of text alignment options. + if (isAppLocaleRTL) { + textAlignmentOptions = textAlignmentOptions.reverse(); + } + + if (Services.prefs.getBoolPref("reader.improved_text_menu.enabled", false)) { + doc.getElementById("regular-text-menu").hidden = true; + doc.getElementById("improved-text-menu").hidden = false; + + let contentWidth = Services.prefs.getIntPref("reader.content_width"); + this._setupSlider( + "content-width", + contentWidthSliderOptions, + contentWidth, + this._setContentWidthSlider.bind(this) + ); + this._setContentWidthSlider(contentWidth); + + let lineSpacing = Services.prefs.getIntPref("reader.line_height"); + this._setupSlider( + "line-spacing", + lineSpacingSliderOptions, + lineSpacing, + this._setLineSpacing.bind(this) + ); + this._setLineSpacing(lineSpacing); + + let characterSpacing = Services.prefs.getStringPref( + "reader.character_spacing" + ); + this._setupSlider( + "character-spacing", + characterSpacingSliderOptions, + characterSpacing, + this._setCharacterSpacing.bind(this) + ); + this._setCharacterSpacing(characterSpacing); + + let wordSpacing = Services.prefs.getStringPref("reader.word_spacing"); + this._setupSlider( + "word-spacing", + wordSpacingSliderOptions, + wordSpacing, + this._setWordSpacing.bind(this) + ); + this._setWordSpacing(wordSpacing); + + let textAlignment = Services.prefs.getCharPref("reader.text_alignment"); + this._setupSegmentedButton( + "text-alignment-buttons", + textAlignmentOptions, + textAlignment, + this._setTextAlignment.bind(this) + ); + this._setTextAlignment(textAlignment); + } else { + this._setupSegmentedButton( + "font-type-buttons", + fontTypeOptions, + fontType, + this._setFontType.bind(this) + ); + } this._setupFontSizeButtons(); @@ -257,6 +389,8 @@ export var AboutReader = function ( this._setupLineHeightButtons(); + this._setFontType(fontType); + if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) { new lazy.NarrateControls(win, this._languagePromise); } @@ -775,6 +909,80 @@ AboutReader.prototype = { ); }, + _setupSlider(id, options, initialValue, callback) { + let doc = this._doc; + let slider = doc.createElement("moz-slider"); + + slider.setAttribute("min", options.min); + slider.setAttribute("max", options.max); + slider.setAttribute("value", initialValue); + slider.setAttribute("ticks", options.ticks); + slider.setAttribute("tick-labels", options.tickLabels); + slider.setAttribute("data-l10n-id", options.l10nId); + slider.setAttribute("data-l10n-attrs", "label"); + slider.setAttribute("slider-icon", options.icon); + + slider.addEventListener("slider-changed", e => { + callback(e.detail); + }); + + let sliderContainer = doc.getElementById(`${id}-slider`); + sliderContainer.appendChild(slider); + }, + + // Rename this function to setContentWidth when the old menu is retired. + _setContentWidthSlider(newContentWidth) { + // We map the slider range [1-9] to 20-60em. + let width = 20 + 5 * (newContentWidth - 1) + "em"; + this._doc.body.style.setProperty("--content-width", width); + this._scheduleToolbarOverlapHandler(); + return lazy.AsyncPrefs.set( + "reader.content_width", + parseInt(newContentWidth) + ); + }, + + _setLineSpacing(newLineSpacing) { + // We map the slider range [1-9] to 1-2.6em. + let spacing = 1 + 0.2 * (newLineSpacing - 1) + "em"; + this._containerElement.style.setProperty("--line-height", spacing); + return lazy.AsyncPrefs.set("reader.line_height", parseInt(newLineSpacing)); + }, + + _setCharacterSpacing(newCharSpacing) { + // We map the slider range [1-9] to 0-0.24em. + let spacing = (newCharSpacing - 1) * 0.03; + this._containerElement.style.setProperty( + "--letter-spacing", + `${parseFloat(spacing).toFixed(2)}em` + ); + lazy.AsyncPrefs.set("reader.character_spacing", newCharSpacing); + }, + + _setWordSpacing(newWordSpacing) { + // We map the slider range [1-9] to 0-0.4em. + let spacing = (newWordSpacing - 1) * 0.05; + this._containerElement.style.setProperty( + "--word-spacing", + `${parseFloat(spacing).toFixed(2)}em` + ); + lazy.AsyncPrefs.set("reader.word_spacing", newWordSpacing); + }, + + _setTextAlignment(newTextAlignment) { + if (this._textAlignment === newTextAlignment) { + return false; + } + + this._containerElement.style.setProperty( + "--text-alignment", + newTextAlignment + ); + + lazy.AsyncPrefs.set("reader.text_alignment", newTextAlignment); + return true; + }, + _setColorScheme(newColorScheme) { // There's nothing to change if the new color scheme is the same as our current scheme. if (this._colorScheme === newColorScheme) { @@ -823,7 +1031,11 @@ AboutReader.prototype = { // The input event for the last selected segmented button is fired // upon loading a reader article in the same session. To prevent it // from overwriting custom colors, we return false. - if (this._colorScheme == "custom" && fromInputEvent) { + const colorsMenuEnabled = Services.prefs.getBoolPref( + "reader.colors_menu.enabled", + false + ); + if (colorsMenuEnabled && this._colorScheme == "custom" && fromInputEvent) { lastSelectedTheme = colorSchemePref; return false; } diff --git a/toolkit/components/reader/content/aboutReader.html b/toolkit/components/reader/content/aboutReader.html index d1c9164d42..452d4590b8 100644 --- a/toolkit/components/reader/content/aboutReader.html +++ b/toolkit/components/reader/content/aboutReader.html @@ -25,6 +25,10 @@ <link rel="localization" href="toolkit/branding/brandings.ftl" /> <script type="module" + src="chrome://global/content/reader/moz-slider.mjs" + ></script> + <script + type="module" src="chrome://global/content/reader/color-input.mjs" ></script> <script @@ -50,7 +54,57 @@ data-l10n-id="about-reader-toolbar-close" ></span> </button> - <ul class="dropdown style-dropdown"> + <ul + class="dropdown improved-style-dropdown" + id="improved-text-menu" + hidden="true" + > + <li> + <button + class="dropdown-toggle toolbar-button improved-style-button" + aria-labelledby="toolbar-text-layout-controls" + data-telemetry-id="reader-text-layout-controls" + > + <span + class="hover-label" + id="toolbar-text-layout-controls" + data-l10n-id="about-reader-toolbar-text-layout-controls" + ></span> + </button> + </li> + <li class="dropdown-popup" id="text-layout-controls"> + <h2 + data-l10n-id="about-reader-layout-header" + id="about-reader-layout-header" + ></h2> + <div id="content-width-slider" class="slider-container"></div> + <div id="line-spacing-slider" class="slider-container"></div> + <hr /> + <details id="about-reader-advanced-layout"> + <summary class="accordion-header"> + <h2 + data-l10n-id="about-reader-advanced-layout-header" + id="about-reader-advanced-layout-header" + ></h2> + <span class="chevron-icon"></span> + </summary> + <div + id="character-spacing-slider" + class="slider-container" + ></div> + <div id="word-spacing-slider" class="slider-container"></div> + <label + for="text-alignment-buttons" + data-l10n-id="about-reader-text-alignment-label" + ></label> + <div + class="text-alignment-buttons radiorow" + id="text-alignment-buttons" + ></div> + </details> + </li> + </ul> + <ul class="dropdown style-dropdown" id="regular-text-menu"> <li> <button class="dropdown-toggle toolbar-button style-button" @@ -120,7 +174,7 @@ <span class="hover-label" id="toolbar-color-controls" - data-l10n-id="about-reader-toolbar-color-controls" + data-l10n-id="about-reader-toolbar-theme-controls" ></span> </button> </li> diff --git a/toolkit/components/reader/jar.mn b/toolkit/components/reader/jar.mn index 587dd45cf8..a17d00c076 100644 --- a/toolkit/components/reader/jar.mn +++ b/toolkit/components/reader/jar.mn @@ -6,3 +6,5 @@ toolkit.jar: content/global/reader/aboutReader.html (content/aboutReader.html) content/global/reader/color-input.css (color-input.css) content/global/reader/color-input.mjs (color-input.mjs) + content/global/reader/moz-slider.css (moz-slider.css) + content/global/reader/moz-slider.mjs (moz-slider.mjs) diff --git a/toolkit/components/reader/moz-slider.css b/toolkit/components/reader/moz-slider.css new file mode 100644 index 0000000000..5238bd02a3 --- /dev/null +++ b/toolkit/components/reader/moz-slider.css @@ -0,0 +1,43 @@ +/* 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/. */ + +.container { + display: flex; + align-items: center; + gap: var(--space-large); + margin-inline-start: var(--space-medium); +} + +.slider-label { + display: inline-block; + margin-block-end: var(--space-medium); +} + +.icon-container + .slider-container .slider-label { + margin-inline-start: calc(-2 * var(--space-xlarge)); +} + +.icon-container { + position: relative; + top: 6px; +} + +.icon { + -moz-context-properties: fill; + fill: var(--popup-button-foreground); + height: 20px; + width: 20px; +} + +.slider-container, +input { + width: 100%; +} + +datalist { + display: flex; + justify-content: space-between; + white-space: nowrap; + font-size: var(--font-size-small); +} diff --git a/toolkit/components/reader/moz-slider.mjs b/toolkit/components/reader/moz-slider.mjs new file mode 100644 index 0000000000..057738f2cc --- /dev/null +++ b/toolkit/components/reader/moz-slider.mjs @@ -0,0 +1,126 @@ +/* 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/. */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +/** + * A range slider component that can be used to select a value. + * + * @tagname moz-slider + * @property {number} min - The minimum value of the slider. + * @property {number} max - The maximum value of the slider. + * @property {number} value - The initial value of the slider. + * @property {number} ticks - The number of tick marks to display under the slider. + * @property {Array<string>} tickLabels - A list containing the tick label strings. + * @property {string} label - The label text for the slider. + * @property {string} sliderIcon - The url of the slider icon. + */ + +export default class MozSlider extends MozLitElement { + static properties = { + min: { type: Number }, + max: { type: Number }, + value: { type: Number }, + ticks: { type: Number }, + tickLabels: { type: Array, attribute: "tick-labels" }, + label: { type: String }, + sliderIcon: { type: String, attribute: "slider-icon" }, + }; + + static get queries() { + return { + tickMarks: "datalist", + sliderTrack: "#inputSlider", + }; + } + + constructor() { + super(); + this.ticks = 0; + this.tickLabels = []; + this.sliderIcon = ""; + } + + getStepSize() { + const stepSize = (this.max - this.min) / (this.ticks - 1); + return stepSize; + } + + setupIcon() { + if (this.sliderIcon) { + return html`<div class="icon-container"> + <img class="icon" role="presentation" src=${this.sliderIcon} /> + </div> `; + } + return ""; + } + + ticksTemplate() { + if (this.ticks > 0) { + let tickList = []; + let value = this.min; + let stepSize = this.getStepSize(); + for (let i = 0; i < this.ticks; i++) { + let optionId = ""; + let label = ""; + if (this.tickLabels.length) { + if (i == 0) { + optionId = "inline-start-label"; + label = this.tickLabels[0]; + } else if (i == this.ticks - 1) { + optionId = "inline-end-label"; + label = this.tickLabels[1]; + } + } + tickList.push( + html`<option + id=${optionId} + value=${parseFloat(value).toFixed(2)} + label=${label} + ></option>` + ); + value += stepSize; + } + return html` <datalist id="slider-ticks">${tickList}</datalist> `; + } + return ""; + } + + handleChange(event) { + this.value = event.target.value; + this.dispatchEvent( + new CustomEvent("slider-changed", { + detail: this.value, + }) + ); + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://global/content/reader/moz-slider.css" + /> + <div class="container"> + ${this.setupIcon()} + <div class="slider-container"> + <label class="slider-label" for="inputSlider">${this.label}</label> + <input + id="inputSlider" + max=${this.max} + min=${this.min} + step=${this.getStepSize()} + type="range" + .value=${this.value} + list="slider-ticks" + @change=${this.handleChange} + /> + ${this.ticksTemplate()} + </div> + </div> + `; + } +} +customElements.define("moz-slider", MozSlider); diff --git a/toolkit/components/reader/moz-slider.stories.mjs b/toolkit/components/reader/moz-slider.stories.mjs new file mode 100644 index 0000000000..3f4083439c --- /dev/null +++ b/toolkit/components/reader/moz-slider.stories.mjs @@ -0,0 +1,42 @@ +/* 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/. */ + +import { html } from "../../content/widgets/vendor/lit.all.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/reader/moz-slider.mjs"; + +export default { + title: "Domain-specific UI Widgets/Reader View/Slider", + component: "moz-slider", + argTypes: {}, + parameters: { + status: "stable", + fluent: `moz-slider-label = + .label = Slider test + `, + }, +}; + +const Template = ({ min, max, value, ticks, labelL10nId, sliderIcon }) => html` + <moz-slider + min=${min} + max=${max} + value=${value} + ticks=${ticks} + tick-labels='["Narrow", "Wide"]' + data-l10n-id=${labelL10nId} + data-l10n-attrs="label" + slider-icon=${sliderIcon} + ></moz-slider> +`; + +export const Default = Template.bind({}); +Default.args = { + min: 0, + max: 4, + value: 2, + ticks: 9, + labelL10nId: "moz-slider-label", + sliderIcon: "chrome://global/skin/icons/defaultFavicon.svg", +}; diff --git a/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js b/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js index 2dae1872c3..615e67bd56 100644 --- a/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js +++ b/toolkit/components/reader/tests/browser/browser_drag_url_readerMode.js @@ -5,7 +5,7 @@ const TEST_PATH = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", - "http://example.com" + "https://example.com" ); add_task(async function test_readerModeURLDrag() { diff --git a/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js b/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js index 9ac0e367ca..d1d0e8b0d5 100644 --- a/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js +++ b/toolkit/components/reader/tests/browser/browser_readerMode_bc_reuse.js @@ -5,7 +5,7 @@ const TEST_PATH = getRootDirectory(gTestPath).replace( "chrome://mochitests/content", - "http://example.com" + "https://example.com" ); const TEST_URL = TEST_PATH + "readerModeArticle.html"; diff --git a/toolkit/components/reader/tests/chrome/chrome.toml b/toolkit/components/reader/tests/chrome/chrome.toml index e416c49181..2697e13db8 100644 --- a/toolkit/components/reader/tests/chrome/chrome.toml +++ b/toolkit/components/reader/tests/chrome/chrome.toml @@ -1,3 +1,5 @@ [DEFAULT] ["test_color_input.html"] + +["test_moz_slider.html"] diff --git a/toolkit/components/reader/tests/chrome/test_moz_slider.html b/toolkit/components/reader/tests/chrome/test_moz_slider.html new file mode 100644 index 0000000000..01af33ed14 --- /dev/null +++ b/toolkit/components/reader/tests/chrome/test_moz_slider.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozSlider Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="module" src="chrome://global/content/reader/moz-slider.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="max-width: fit-content"> + <moz-slider min="0" max="3" value="2" ticks="7" ticklabels='["Narrow", "Wide"]'></moz-slider> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + add_task(async function testMozSliderDisplay() { + const mozSlider = document.querySelector("moz-slider"); + ok(mozSlider, "moz-slider is rendered"); + is(mozSlider.value, 2, "moz-slider should set initial value based on prop"); + + const ticks = mozSlider.tickMarks; + ok(ticks, "moz-slider contains tick marks"); + is(ticks.childElementCount, 7, "should have the correct number of ticks"); + }); + + add_task(async function testMozSliderValues() { + const mozSlider = document.querySelector("moz-slider"); + const firstVal = mozSlider.value; + + const sliderChanged = new Promise((resolve) => { + mozSlider.addEventListener("slider-changed", (event) => resolve(event.detail), { once: true }); + }); + mozSlider.sliderTrack.focus(); + synthesizeKey("KEY_ArrowRight"); + await sliderChanged; + ok(mozSlider.value > firstVal, "moving slider to the right should increase value"); + is(mozSlider.value - firstVal, 0.5, "should increment by correct step size"); + + synthesizeKey("KEY_ArrowRight", { repeat: 2 }); + await sliderChanged; + ok(mozSlider.value == 3, "should not increment beyond max value"); + }); +</script> +</pre> +</body> +</html> |