From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../shopping/content/adjusted-rating.mjs | 53 +++ .../shopping/content/analysis-explainer.css | 39 ++ .../shopping/content/analysis-explainer.mjs | 158 +++++++ .../shopping/content/assets/competitiveness.svg | 6 + .../shopping/content/assets/optInDark.avif | Bin 0 -> 9746 bytes .../shopping/content/assets/optInLight.avif | Bin 0 -> 9651 bytes .../shopping/content/assets/packaging.svg | 6 + .../components/shopping/content/assets/price.svg | 7 + .../content/assets/priceTagButtonCallout.svg | 41 ++ .../components/shopping/content/assets/quality.svg | 7 + .../shopping/content/assets/ratingDark.avif | Bin 0 -> 14230 bytes .../shopping/content/assets/ratingLight.avif | Bin 0 -> 14071 bytes .../content/assets/reviewsVisualCallout.svg | 77 ++++ .../shopping/content/assets/shipping.svg | 6 + .../shopping/content/assets/shopping.svg | 6 + .../shopping/content/assets/unanalyzedDark.avif | Bin 0 -> 11485 bytes .../shopping/content/assets/unanalyzedLight.avif | Bin 0 -> 11070 bytes .../components/shopping/content/highlight-item.css | 66 +++ .../components/shopping/content/highlight-item.mjs | 57 +++ browser/components/shopping/content/highlights.mjs | 124 ++++++ .../components/shopping/content/letter-grade.css | 132 ++++++ .../components/shopping/content/letter-grade.mjs | 78 ++++ browser/components/shopping/content/onboarding.mjs | 69 +++ .../components/shopping/content/recommended-ad.css | 59 +++ .../components/shopping/content/recommended-ad.mjs | 150 +++++++ .../components/shopping/content/reliability.mjs | 48 +++ browser/components/shopping/content/settings.css | 69 +++ browser/components/shopping/content/settings.mjs | 210 +++++++++ .../components/shopping/content/shopping-card.css | 201 +++++++++ .../components/shopping/content/shopping-card.mjs | 204 +++++++++ .../shopping/content/shopping-container.css | 152 +++++++ .../shopping/content/shopping-container.mjs | 471 +++++++++++++++++++++ .../shopping/content/shopping-message-bar.css | 78 ++++ .../shopping/content/shopping-message-bar.mjs | 278 ++++++++++++ .../components/shopping/content/shopping-page.css | 28 ++ .../shopping/content/shopping-sidebar.js | 80 ++++ browser/components/shopping/content/shopping.ftl | 7 + browser/components/shopping/content/shopping.html | 53 +++ browser/components/shopping/content/unanalyzed.css | 41 ++ browser/components/shopping/content/unanalyzed.mjs | 61 +++ 40 files changed, 3122 insertions(+) create mode 100644 browser/components/shopping/content/adjusted-rating.mjs create mode 100644 browser/components/shopping/content/analysis-explainer.css create mode 100644 browser/components/shopping/content/analysis-explainer.mjs create mode 100644 browser/components/shopping/content/assets/competitiveness.svg create mode 100644 browser/components/shopping/content/assets/optInDark.avif create mode 100644 browser/components/shopping/content/assets/optInLight.avif create mode 100644 browser/components/shopping/content/assets/packaging.svg create mode 100644 browser/components/shopping/content/assets/price.svg create mode 100644 browser/components/shopping/content/assets/priceTagButtonCallout.svg create mode 100644 browser/components/shopping/content/assets/quality.svg create mode 100644 browser/components/shopping/content/assets/ratingDark.avif create mode 100644 browser/components/shopping/content/assets/ratingLight.avif create mode 100644 browser/components/shopping/content/assets/reviewsVisualCallout.svg create mode 100644 browser/components/shopping/content/assets/shipping.svg create mode 100644 browser/components/shopping/content/assets/shopping.svg create mode 100644 browser/components/shopping/content/assets/unanalyzedDark.avif create mode 100644 browser/components/shopping/content/assets/unanalyzedLight.avif create mode 100644 browser/components/shopping/content/highlight-item.css create mode 100644 browser/components/shopping/content/highlight-item.mjs create mode 100644 browser/components/shopping/content/highlights.mjs create mode 100644 browser/components/shopping/content/letter-grade.css create mode 100644 browser/components/shopping/content/letter-grade.mjs create mode 100644 browser/components/shopping/content/onboarding.mjs create mode 100644 browser/components/shopping/content/recommended-ad.css create mode 100644 browser/components/shopping/content/recommended-ad.mjs create mode 100644 browser/components/shopping/content/reliability.mjs create mode 100644 browser/components/shopping/content/settings.css create mode 100644 browser/components/shopping/content/settings.mjs create mode 100644 browser/components/shopping/content/shopping-card.css create mode 100644 browser/components/shopping/content/shopping-card.mjs create mode 100644 browser/components/shopping/content/shopping-container.css create mode 100644 browser/components/shopping/content/shopping-container.mjs create mode 100644 browser/components/shopping/content/shopping-message-bar.css create mode 100644 browser/components/shopping/content/shopping-message-bar.mjs create mode 100644 browser/components/shopping/content/shopping-page.css create mode 100644 browser/components/shopping/content/shopping-sidebar.js create mode 100644 browser/components/shopping/content/shopping.ftl create mode 100644 browser/components/shopping/content/shopping.html create mode 100644 browser/components/shopping/content/unanalyzed.css create mode 100644 browser/components/shopping/content/unanalyzed.mjs (limited to 'browser/components/shopping/content') diff --git a/browser/components/shopping/content/adjusted-rating.mjs b/browser/components/shopping/content/adjusted-rating.mjs new file mode 100644 index 0000000000..a49b821145 --- /dev/null +++ b/browser/components/shopping/content/adjusted-rating.mjs @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-five-star.mjs"; + +/** + * Class for displaying the adjusted ratings for a given product. + */ +class AdjustedRating extends MozLitElement { + static properties = { + rating: { type: Number, reflect: true }, + }; + + static get queries() { + return { + ratingEl: "moz-five-star", + }; + } + + render() { + if (!this.rating && this.rating !== 0) { + this.hidden = true; + return null; + } + + this.hidden = false; + + return html` + + +
+ +
+
+ `; + } +} + +customElements.define("adjusted-rating", AdjustedRating); diff --git a/browser/components/shopping/content/analysis-explainer.css b/browser/components/shopping/content/analysis-explainer.css new file mode 100644 index 0000000000..ce7ebf55cc --- /dev/null +++ b/browser/components/shopping/content/analysis-explainer.css @@ -0,0 +1,39 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +#analysis-explainer-wrapper p, +.analysis-explainer-grading-scale-description { + line-height: 150%; +} + +#analysis-explainer-grading-scale-wrapper { + margin-inline-start: 0.54em; +} + +#analysis-explainer-grading-scale-list { + list-style: none; + padding: 0; +} + +.analysis-explainer-grading-scale-entry { + display: flex; + align-items: flex-start; + align-self: stretch; + gap: 0.54rem; + padding: 0.54em; + padding-inline-start: 0; +} + +.analysis-explainer-grading-scale-letters { + display: flex; + align-items: flex-start; + gap: 0.2rem; + width: 3.47em; +} + +.analysis-explainer-grading-scale-description { + margin: 0; +} diff --git a/browser/components/shopping/content/analysis-explainer.mjs b/browser/components/shopping/content/analysis-explainer.mjs new file mode 100644 index 0000000000..a32c64dfeb --- /dev/null +++ b/browser/components/shopping/content/analysis-explainer.mjs @@ -0,0 +1,158 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-card.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/letter-grade.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-support-link.mjs"; + +const VALID_EXPLAINER_L10N_IDS = new Map([ + ["reliable", "shopping-analysis-explainer-review-grading-scale-reliable"], + ["mixed", "shopping-analysis-explainer-review-grading-scale-mixed"], + ["unreliable", "shopping-analysis-explainer-review-grading-scale-unreliable"], +]); + +/** + * Class for displaying details about letter grades, adjusted rating, and highlights. + */ +class AnalysisExplainer extends MozLitElement { + static properties = { + productUrl: { type: String, reflect: true }, + }; + + static get queries() { + return { + reviewQualityExplainerLink: "#review-quality-url", + }; + } + + getGradesDescriptionTemplate() { + return html` +
+

+
+ `; + } + + createGradingScaleEntry(letters, descriptionL10nId) { + let letterGradesTemplate = []; + for (let letter of letters) { + letterGradesTemplate.push( + html`` + ); + } + return html` +
+
+ + ${letterGradesTemplate} + +
+
+
+ `; + } + + getGradingScaleListTemplate() { + return html` +
+
+ ${this.createGradingScaleEntry( + ["A", "B"], + VALID_EXPLAINER_L10N_IDS.get("reliable") + )} + ${this.createGradingScaleEntry( + ["C"], + VALID_EXPLAINER_L10N_IDS.get("mixed") + )} + ${this.createGradingScaleEntry( + ["D", "F"], + VALID_EXPLAINER_L10N_IDS.get("unreliable") + )} +
+
+ `; + } + + // It turns out we must always return a non-empty string: if not, the fluent + // resolver will complain that the variable value is missing. We use the + // placeholder "retailer", which should never be visible to users. + getRetailerDisplayName() { + let defaultName = "retailer"; + if (!this.productUrl) { + return defaultName; + } + let url = new URL(this.productUrl); + let hostname = url.hostname; + let displayNames = { + "www.amazon.com": "Amazon", + "www.bestbuy.com": "Best Buy", + "www.walmart.com": "Walmart", + }; + return displayNames[hostname] ?? defaultName; + } + + handleReviewQualityUrlClicked(e) { + if (e.target.localName == "a" && e.button == 0) { + Glean.shopping.surfaceShowQualityExplainerUrlClicked.record(); + } + } + + // Bug 1857620: rather than manually set the utm parameters on the SUMO link, + // we should instead update moz-support-link to allow arbitrary utm parameters. + render() { + return html` + + +
+
+

+ ${this.getGradesDescriptionTemplate()} + ${this.getGradingScaleListTemplate()} +

+

+

+ +

+
+
+
+ `; + } +} + +customElements.define("analysis-explainer", AnalysisExplainer); diff --git a/browser/components/shopping/content/assets/competitiveness.svg b/browser/components/shopping/content/assets/competitiveness.svg new file mode 100644 index 0000000000..a192205fc2 --- /dev/null +++ b/browser/components/shopping/content/assets/competitiveness.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/shopping/content/assets/optInDark.avif b/browser/components/shopping/content/assets/optInDark.avif new file mode 100644 index 0000000000..fb662e63dd Binary files /dev/null and b/browser/components/shopping/content/assets/optInDark.avif differ diff --git a/browser/components/shopping/content/assets/optInLight.avif b/browser/components/shopping/content/assets/optInLight.avif new file mode 100644 index 0000000000..e148580976 Binary files /dev/null and b/browser/components/shopping/content/assets/optInLight.avif differ diff --git a/browser/components/shopping/content/assets/packaging.svg b/browser/components/shopping/content/assets/packaging.svg new file mode 100644 index 0000000000..fddf88c878 --- /dev/null +++ b/browser/components/shopping/content/assets/packaging.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/shopping/content/assets/price.svg b/browser/components/shopping/content/assets/price.svg new file mode 100644 index 0000000000..e5b8ba7a22 --- /dev/null +++ b/browser/components/shopping/content/assets/price.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/browser/components/shopping/content/assets/priceTagButtonCallout.svg b/browser/components/shopping/content/assets/priceTagButtonCallout.svg new file mode 100644 index 0000000000..26431804cb --- /dev/null +++ b/browser/components/shopping/content/assets/priceTagButtonCallout.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/shopping/content/assets/quality.svg b/browser/components/shopping/content/assets/quality.svg new file mode 100644 index 0000000000..cdc092d1ea --- /dev/null +++ b/browser/components/shopping/content/assets/quality.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/browser/components/shopping/content/assets/ratingDark.avif b/browser/components/shopping/content/assets/ratingDark.avif new file mode 100644 index 0000000000..240c2d6238 Binary files /dev/null and b/browser/components/shopping/content/assets/ratingDark.avif differ diff --git a/browser/components/shopping/content/assets/ratingLight.avif b/browser/components/shopping/content/assets/ratingLight.avif new file mode 100644 index 0000000000..a6eb3a0316 Binary files /dev/null and b/browser/components/shopping/content/assets/ratingLight.avif differ diff --git a/browser/components/shopping/content/assets/reviewsVisualCallout.svg b/browser/components/shopping/content/assets/reviewsVisualCallout.svg new file mode 100644 index 0000000000..9a9b93ba06 --- /dev/null +++ b/browser/components/shopping/content/assets/reviewsVisualCallout.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/components/shopping/content/assets/shipping.svg b/browser/components/shopping/content/assets/shipping.svg new file mode 100644 index 0000000000..d9b01a0a96 --- /dev/null +++ b/browser/components/shopping/content/assets/shipping.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/shopping/content/assets/shopping.svg b/browser/components/shopping/content/assets/shopping.svg new file mode 100644 index 0000000000..5b81219d48 --- /dev/null +++ b/browser/components/shopping/content/assets/shopping.svg @@ -0,0 +1,6 @@ + + + + diff --git a/browser/components/shopping/content/assets/unanalyzedDark.avif b/browser/components/shopping/content/assets/unanalyzedDark.avif new file mode 100644 index 0000000000..66efc3bc6e Binary files /dev/null and b/browser/components/shopping/content/assets/unanalyzedDark.avif differ diff --git a/browser/components/shopping/content/assets/unanalyzedLight.avif b/browser/components/shopping/content/assets/unanalyzedLight.avif new file mode 100644 index 0000000000..28cd1c26f1 Binary files /dev/null and b/browser/components/shopping/content/assets/unanalyzedLight.avif differ diff --git a/browser/components/shopping/content/highlight-item.css b/browser/components/shopping/content/highlight-item.css new file mode 100644 index 0000000000..2139363862 --- /dev/null +++ b/browser/components/shopping/content/highlight-item.css @@ -0,0 +1,66 @@ +/* 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/. */ + +/* Separate logo, label and details into rows and columns */ +.highlight-item-wrapper { + align-items: center; + display: grid; + column-gap: 1em; + grid-template-columns: 1rem auto; + grid-template-rows: 1rem auto; +} + +.highlight-icon { + content: url("chrome://global/skin/icons/defaultFavicon.svg"); + color: var(--icon-color); + fill: currentColor; + -moz-context-properties: fill; + grid-row-start: 1; + grid-column-start: 1; + + &.price { + content: url("chrome://browser/content/shopping/assets/price.svg"); + } + + &.quality { + content: url("chrome://browser/content/shopping/assets/quality.svg"); + } + + &.shipping { + content: url("chrome://browser/content/shopping/assets/shipping.svg"); + } + + &.competitiveness { + content: url("chrome://browser/content/shopping/assets/competitiveness.svg"); + } + + &.packaging\/appearance { + content: url("chrome://browser/content/shopping/assets/packaging.svg"); + } +} + +.highlight-label { + font-weight: 590; + grid-column-start: 2; + grid-row-start: 1; +} + +.highlight-details { + grid-column-start: 2; + grid-row-start: 2; + margin: 0; + padding: 0; +} + +.highlight-details-list { + list-style-type: none; + padding: 0; + margin: 0; +} + +.highlight-details-list > li { + /* Render LTR since English review snippets are only supported at this time. */ + direction: ltr; + margin: 1em 0; +} diff --git a/browser/components/shopping/content/highlight-item.mjs b/browser/components/shopping/content/highlight-item.mjs new file mode 100644 index 0000000000..a61764dc86 --- /dev/null +++ b/browser/components/shopping/content/highlight-item.mjs @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +/** + * Class for displaying a list of highlighted product reviews, according to highlight category. + */ +class Highlight extends MozLitElement { + l10nId; + highlightType; + /** + * reviews is a list of Strings, representing all the reviews to display + * under a highlight category. + */ + reviews; + + /** + * lang defines the language in which the reviews are written. We should specify + * language so that screen readers can read text with the appropriate language packs. + */ + lang; + + render() { + let ulTemplate = []; + + for (let review of this.reviews) { + ulTemplate.push( + html`
  • + ${review} +
  • ` + ); + } + + return html` + +
    + +
    +
    +
      + ${ulTemplate} +
    +
    +
    + `; + } +} + +customElements.define("highlight-item", Highlight); diff --git a/browser/components/shopping/content/highlights.mjs b/browser/components/shopping/content/highlights.mjs new file mode 100644 index 0000000000..09ed055b28 --- /dev/null +++ b/browser/components/shopping/content/highlights.mjs @@ -0,0 +1,124 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/highlight-item.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-card.mjs"; + +const VALID_HIGHLIGHT_L10N_IDs = new Map([ + ["price", "shopping-highlight-price"], + ["quality", "shopping-highlight-quality"], + ["shipping", "shopping-highlight-shipping"], + ["competitiveness", "shopping-highlight-competitiveness"], + ["packaging/appearance", "shopping-highlight-packaging"], +]); + +/** + * Class for displaying all available highlight categories for a product and any + * highlight reviews per category. + */ +class ReviewHighlights extends MozLitElement { + /** + * highlightsMap is a map of highlight categories to an array of reviews per category. + * It is possible for a category to have no reviews. + */ + #highlightsMap; + + static properties = { + highlights: { type: Object }, + lang: { type: String, reflect: true }, + }; + + static get queries() { + return { + reviewHighlightsListEl: "#review-highlights-list", + }; + } + + updateHighlightsMap() { + let availableKeys; + this.#highlightsMap = null; + + try { + if (!this.highlights) { + return; + } + + // Filter highlights that have data. + let keys = Object.keys(this.highlights); + availableKeys = keys.filter( + key => Object.values(this.highlights[key]).flat().length !== 0 + ); + + // Filter valid highlight category types. Valid types are guaranteed to have data-l10n-ids. + availableKeys = availableKeys.filter(key => + VALID_HIGHLIGHT_L10N_IDs.has(key) + ); + + // If there are no highlights to show in the end, stop here and don't render content. + if (!availableKeys.length) { + return; + } + } catch (e) { + return; + } + + this.#highlightsMap = new Map(); + + for (let key of availableKeys) { + // Ignore negative,neutral,positive sentiments and simply append review strings into one array. + let reviews = Object.values(this.highlights[key]).flat(); + this.#highlightsMap.set(key, reviews); + } + } + + createHighlightElement(type, reviews) { + let highlightEl = document.createElement("highlight-item"); + // We already verify highlight type and its l10n id in updateHighlightsMap. + let l10nId = VALID_HIGHLIGHT_L10N_IDs.get(type); + highlightEl.id = type; + highlightEl.l10nId = l10nId; + highlightEl.highlightType = type; + highlightEl.reviews = reviews; + highlightEl.lang = this.lang; + return highlightEl; + } + + render() { + this.updateHighlightsMap(); + + if (!this.#highlightsMap) { + this.hidden = true; + return null; + } + + this.hidden = false; + + let highlightsTemplate = []; + for (let [key, value] of this.#highlightsMap) { + let highlightEl = this.createHighlightElement(key, value); + highlightsTemplate.push(highlightEl); + } + + return html` + +
    +
    ${highlightsTemplate}
    +
    +
    + `; + } +} + +customElements.define("review-highlights", ReviewHighlights); diff --git a/browser/components/shopping/content/letter-grade.css b/browser/components/shopping/content/letter-grade.css new file mode 100644 index 0000000000..75be411151 --- /dev/null +++ b/browser/components/shopping/content/letter-grade.css @@ -0,0 +1,132 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +:host { + --background-term-a: #B3FFE3; + --background-term-b: #80EBFF; + --background-term-c: #FFEA80; + --background-term-d: #FFB587; + --background-term-f: #FF848B; + --in-content-box-border-color: rgba(0, 0, 0, 0.15); + --inner-border: 1px solid var(--in-content-box-border-color); + --letter-grade-width: 1.5rem; + --letter-grade-term-inline-padding: 0.25rem; +} + +#letter-grade-wrapper { + border-radius: 4px; + color: #000; + display: flex; + font-weight: 600; + line-height: 150%; + margin: 0; + overflow-wrap: anywhere; +} + +#letter-grade-term { + align-items: center; + align-self: stretch; + box-sizing: border-box; + display: flex; + flex-shrink: 0; + font-size: 1em; + justify-content: center; + margin: 0; + padding: 0.0625rem var(--letter-grade-term-inline-padding); + text-align: center; + width: var(--letter-grade-width); +} + +:host([showdescription]) #letter-grade-term { + /* For border "shadow" inside the container */ + border-block: var(--inner-border); + border-inline-start: var(--inner-border); + border-start-start-radius: 4px; + border-end-start-radius: 4px; + /* Add 1px padding so that the letter does not shift when changing + * between the show description and no description variants. */ + padding-inline-end: calc(var(--letter-grade-term-inline-padding) + 1px); +} + +:host(:not([showdescription])) #letter-grade-term { + border: var(--inner-border); + border-radius: 4px; +} + +#letter-grade-description { + /* For border "shadow" inside the container */ + border-block: var(--inner-border); + border-inline-end: var(--inner-border); + border-start-end-radius: 4px; + border-end-end-radius: 4px; + + align-items: center; + align-self: stretch; + box-sizing: border-box; + display: flex; + font-size: 0.87rem; + font-weight: var(--font-weight-default); + margin: 0; + padding: 0.125rem 0.5rem; +} + +/* Letter grade colors */ + +:host([letter="A"]) #letter-grade-term { + background-color: var(--background-term-a); +} + +:host([letter="A"]) #letter-grade-description { + background-color: rgba(231, 255, 246, 1); +} + +:host([letter="B"]) #letter-grade-term { + background-color: var(--background-term-b); +} + +:host([letter="B"]) #letter-grade-description { + background-color: rgba(222, 250, 255, 1); +} + +:host([letter="C"]) #letter-grade-term { + background-color: var(--background-term-c); +} + +:host([letter="C"]) #letter-grade-description { + background-color: rgba(255, 249, 218, 1); +} + +:host([letter="D"]) #letter-grade-term { + background-color: var(--background-term-d); +} + +:host([letter="D"]) #letter-grade-description { + background-color: rgba(252, 230, 213, 1); +} + +:host([letter="F"]) #letter-grade-term { + background-color: var(--background-term-f); +} + +:host([letter="F"]) #letter-grade-description { + background-color: rgba(255, 228, 230, 1); +} + +@media (prefers-contrast) { + :host { + --in-content-box-border-color: unset; + } + + #letter-grade-term { + background-color: var(--in-content-page-color) !important; + color: var(--in-content-page-background) !important; + } + + #letter-grade-description { + background-color: var(--in-content-page-background) !important; + color: var(--in-content-page-color) !important; + } +} diff --git a/browser/components/shopping/content/letter-grade.mjs b/browser/components/shopping/content/letter-grade.mjs new file mode 100644 index 0000000000..71badb1c45 --- /dev/null +++ b/browser/components/shopping/content/letter-grade.mjs @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +const VALID_LETTER_GRADE_L10N_IDS = new Map([ + ["A", "shopping-letter-grade-description-ab"], + ["B", "shopping-letter-grade-description-ab"], + ["C", "shopping-letter-grade-description-c"], + ["D", "shopping-letter-grade-description-df"], + ["F", "shopping-letter-grade-description-df"], +]); + +class LetterGrade extends MozLitElement { + #descriptionL10N; + + static properties = { + letter: { type: String, reflect: true }, + showdescription: { type: Boolean, reflect: true }, + }; + + get fluentStrings() { + if (!this._fluentStrings) { + this._fluentStrings = new Localization(["browser/shopping.ftl"], true); + } + return this._fluentStrings; + } + + descriptionTemplate() { + if (this.showdescription) { + return html`

    `; + } + + return null; + } + + render() { + // Do not render if letter is invalid + if (!VALID_LETTER_GRADE_L10N_IDS.has(this.letter)) { + return null; + } + + this.#descriptionL10N = VALID_LETTER_GRADE_L10N_IDS.get(this.letter); + let tooltipL10NArgs; + // Do not localize tooltip if using Storybook. + if (!window.IS_STORYBOOK) { + const localizedDescription = this.fluentStrings.formatValueSync( + this.#descriptionL10N + ); + tooltipL10NArgs = `{"letter": "${this.letter}", "description": "${localizedDescription}"}`; + } + + return html` + +
    +

    ${this.letter}

    + ${this.descriptionTemplate()} +
    + `; + } +} + +customElements.define("letter-grade", LetterGrade); diff --git a/browser/components/shopping/content/onboarding.mjs b/browser/components/shopping/content/onboarding.mjs new file mode 100644 index 0000000000..68124c0253 --- /dev/null +++ b/browser/components/shopping/content/onboarding.mjs @@ -0,0 +1,69 @@ +/* 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/. */ + +const BUNDLE_SRC = + "chrome://browser/content/aboutwelcome/aboutwelcome.bundle.js"; + +class Onboarding { + constructor({ win } = {}) { + this.doc = win.document; + win.addEventListener("RenderWelcome", () => this._addScriptsAndRender(), { + once: true, + }); + } + + async _addScriptsAndRender() { + const addStylesheet = href => { + if (this.doc.head.querySelector(`link[href="${href}"]`)) { + return; + } + const link = this.doc.head.appendChild(this.doc.createElement("link")); + link.rel = "stylesheet"; + link.href = href; + }; + addStylesheet("chrome://browser/content/aboutwelcome/aboutwelcome.css"); + const reactSrc = "resource://activity-stream/vendor/react.js"; + const domSrc = "resource://activity-stream/vendor/react-dom.js"; + // Add React script + const getReactReady = async () => { + return new Promise(resolve => { + let reactScript = this.doc.createElement("script"); + reactScript.src = reactSrc; + this.doc.head.appendChild(reactScript); + reactScript.addEventListener("load", resolve); + }); + }; + // Add ReactDom script + const getDomReady = async () => { + return new Promise(resolve => { + let domScript = this.doc.createElement("script"); + domScript.src = domSrc; + this.doc.head.appendChild(domScript); + domScript.addEventListener("load", resolve); + }); + }; + // Load React, then React Dom + if (!this.doc.querySelector(`[src="${reactSrc}"]`)) { + await getReactReady(); + } + if (!this.doc.querySelector(`[src="${domSrc}"]`)) { + await getDomReady(); + } + // Load the bundle to render the content as configured. + this.doc.querySelector(`[src="${BUNDLE_SRC}"]`)?.remove(); + let bundleScript = this.doc.createElement("script"); + bundleScript.src = BUNDLE_SRC; + this.doc.head.appendChild(bundleScript); + } + + static getOnboarding() { + if (!this.onboarding) { + this.onboarding = new Onboarding({ win: window }); + } + return this.onboarding; + } +} + +const OnboardingContainer = Onboarding.getOnboarding(); +export default OnboardingContainer; diff --git a/browser/components/shopping/content/recommended-ad.css b/browser/components/shopping/content/recommended-ad.css new file mode 100644 index 0000000000..20f0775ce0 --- /dev/null +++ b/browser/components/shopping/content/recommended-ad.css @@ -0,0 +1,59 @@ +/* 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/. */ + +#recommended-ad-wrapper { + display: flex; + flex-direction: column; + gap: 8px; + text-decoration: none; + color: var(--in-content-text-color); +} + +#recommended-ad-wrapper:hover { + cursor: pointer; +} + +#recommended-ad-wrapper:hover #ad-title { + text-decoration: underline; + color: var(--link-color-hover); +} + +#recommended-ad-wrapper:focus-visible { + outline-offset: 4px; + border-radius: 1px; +} + +#ad-content { + display: flex; + justify-content: space-between; + gap: 12px; + height: 80px; +} + +#ad-preview-image { + max-width: 80px; + max-height: 80px; +} + +#ad-title { + overflow: hidden; + text-overflow: ellipsis; + height: fit-content; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + /* This text won't be localized and when in RTL, the ellipsis is positioned + awkwardly so we are forcing LTR */ + direction: ltr; +} + +#price { + font-size: 1em; + font-weight: 600; +} + +#footer { + display: flex; + justify-content: space-between; +} diff --git a/browser/components/shopping/content/recommended-ad.mjs b/browser/components/shopping/content/recommended-ad.mjs new file mode 100644 index 0000000000..09351ffe85 --- /dev/null +++ b/browser/components/shopping/content/recommended-ad.mjs @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-card.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-five-star.mjs"; + +const AD_IMPRESSION_TIMEOUT = 1500; + +class RecommendedAd extends MozLitElement { + static properties = { + product: { type: Object, reflect: true }, + }; + + static get queries() { + return { + letterGradeEl: "letter-grade", + linkEl: "#recommended-ad-wrapper", + priceEl: "#price", + ratingEl: "moz-five-star", + }; + } + + connectedCallback() { + super.connectedCallback(); + if (this.initialized) { + return; + } + this.initialized = true; + + document.addEventListener("visibilitychange", this); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener("visibilitychange", this); + this.resetImpressionTimer(); + this.revokeImageUrl(); + } + + startImpressionTimer() { + if (!this.timeout && document.visibilityState === "visible") { + this.timeout = setTimeout( + () => this.recordImpression(), + AD_IMPRESSION_TIMEOUT + ); + } + } + + resetImpressionTimer() { + this.timeout = clearTimeout(this.timeout); + } + + revokeImageUrl() { + if (this.imageUrl) { + URL.revokeObjectURL(this.imageUrl); + } + } + + recordImpression() { + if (this.hasImpressed) { + return; + } + + this.dispatchEvent( + new CustomEvent("AdImpression", { + bubbles: true, + detail: { aid: this.product.aid }, + }) + ); + + document.removeEventListener("visibilitychange", this); + this.resetImpressionTimer(); + + this.hasImpressed = true; + } + + handleClick(event) { + if (event.button === 0 || event.button === 1) { + this.dispatchEvent( + new CustomEvent("AdClicked", { + bubbles: true, + detail: { aid: this.product.aid }, + }) + ); + } + } + + handleEvent(event) { + if (event.type !== "visibilitychange") { + return; + } + if (document.visibilityState === "visible") { + this.startImpressionTimer(); + } else if (!this.hasImpressed) { + this.resetImpressionTimer(); + } + } + + priceTemplate() { + // We are only showing prices in USD for now. In the future we will need + // to update this to include other currencies. + return html`$${this.product.price}`; + } + + render() { + this.startImpressionTimer(); + + this.revokeImageUrl(); + this.imageUrl = URL.createObjectURL(this.product.image_blob); + + return html` + + + +
    + + ${this.product.name} + +
    + +
    +
    +

    + `; + } +} + +customElements.define("recommended-ad", RecommendedAd); diff --git a/browser/components/shopping/content/reliability.mjs b/browser/components/shopping/content/reliability.mjs new file mode 100644 index 0000000000..1e46b30c4b --- /dev/null +++ b/browser/components/shopping/content/reliability.mjs @@ -0,0 +1,48 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/letter-grade.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-card.mjs"; + +class ReviewReliability extends MozLitElement { + static properties = { + letter: { type: String }, + }; + + static get queries() { + return { + letterGradeEl: "letter-grade", + }; + } + + render() { + if (!this.letter) { + this.hidden = true; + return null; + } + + return html` + +
    + +
    +
    + `; + } +} + +customElements.define("review-reliability", ReviewReliability); diff --git a/browser/components/shopping/content/settings.css b/browser/components/shopping/content/settings.css new file mode 100644 index 0000000000..cd0f497a23 --- /dev/null +++ b/browser/components/shopping/content/settings.css @@ -0,0 +1,69 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +#shopping-settings-wrapper { + --shopping-settings-between-label-and-control-option-gap: 4px; + display: grid; + grid-template-rows: auto; + row-gap: 8px; + margin-top: 12px; + + .shopping-settings-toggle-option-wrapper { + display: grid; + row-gap: var(--shopping-settings-between-label-and-control-option-gap); + } + + #shopping-settings-opt-out-section { + display: grid; + justify-content: center; + + #shopping-settings-opt-out-button { + display: flex; + align-items: center; + } + } + + #powered-by-fakespot { + font-size: 12px; + color: var(--text-color-deemphasized); + } + + #shopping-settings-toggles-section { + display: grid; + row-gap: 8px; + + #shopping-ads-learn-more, + #shopping-auto-open-description { + margin-inline-end: 20px; + color: var(--text-color-deemphasized); + } + } + + /* When `browser.shopping.experience2023.autoOpen` is true. */ + &.shopping-settings-auto-open-ui-enabled { + --shopping-settings-between-options-gap: 12px; + row-gap: var(--shopping-settings-between-options-gap); + + #shopping-settings-toggles-section { + display: grid; + row-gap: var(--shopping-settings-between-options-gap); + } + + .divider { + border: var(--shopping-card-border-width) solid var(--shopping-card-border-color); + } + + #shopping-settings-opt-out-section { + justify-content: flex-start; + row-gap: var(--shopping-settings-between-label-and-control-option-gap); + + #shopping-settings-opt-out-button { + width: fit-content; + margin-inline-start: 0; + } + } + } +} diff --git a/browser/components/shopping/content/settings.mjs b/browser/components/shopping/content/settings.mjs new file mode 100644 index 0000000000..7619bbb131 --- /dev/null +++ b/browser/components/shopping/content/settings.mjs @@ -0,0 +1,210 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ + +/* eslint-env mozilla/remote-page */ + +import { html } from "chrome://global/content/vendor/lit.all.mjs"; + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-toggle.mjs"; + +class ShoppingSettings extends MozLitElement { + static properties = { + adsEnabled: { type: Boolean }, + adsEnabledByUser: { type: Boolean }, + autoOpenEnabled: { type: Boolean }, + autoOpenEnabledByUser: { type: Boolean }, + hostname: { type: String }, + }; + + static get queries() { + return { + wrapperEl: "#shopping-settings-wrapper", + recommendationsToggleEl: "#shopping-settings-recommendations-toggle", + autoOpenToggleEl: "#shopping-settings-auto-open-toggle", + autoOpenToggleDescriptionEl: "#shopping-auto-open-description", + dividerEl: ".divider", + sidebarEnabledStateEl: "#shopping-settings-sidebar-enabled-state", + optOutButtonEl: "#shopping-settings-opt-out-button", + shoppingCardEl: "shopping-card", + adsLearnMoreLinkEl: "#shopping-ads-learn-more-link", + fakespotLearnMoreLinkEl: "#powered-by-fakespot-link", + }; + } + + onToggleRecommendations() { + this.adsEnabledByUser = this.recommendationsToggleEl.pressed; + let action = this.adsEnabledByUser ? "enabled" : "disabled"; + Glean.shopping.surfaceAdsSettingToggled.record({ action }); + RPMSetPref( + "browser.shopping.experience2023.ads.userEnabled", + this.adsEnabledByUser + ); + } + + onToggleAutoOpen() { + this.autoOpenEnabledByUser = this.autoOpenToggleEl.pressed; + let action = this.autoOpenEnabledByUser ? "enabled" : "disabled"; + Glean.shopping.surfaceAutoOpenSettingToggled.record({ action }); + RPMSetPref( + "browser.shopping.experience2023.autoOpen.userEnabled", + this.autoOpenEnabledByUser + ); + if (!this.autoOpenEnabledByUser) { + RPMSetPref("browser.shopping.experience2023.active", false); + } + } + + onDisableShopping() { + window.dispatchEvent( + new CustomEvent("DisableShopping", { bubbles: true, composed: true }) + ); + Glean.shopping.surfaceOptOutButtonClicked.record(); + } + + fakespotLinkClicked(e) { + if (e.target.localName == "a" && e.button == 0) { + Glean.shopping.surfacePoweredByFakespotLinkClicked.record(); + } + } + + render() { + // Whether we show recommendations at all (including offering a user + // control for them) is controlled via a nimbus-enabled pref. + let canShowRecommendationToggle = this.adsEnabled; + + let adsToggleMarkup = canShowRecommendationToggle + ? html` +
    + + + + + +
    ` + : null; + + /* Auto-open experiment changes how the settings card appears by: + * 1. Showing a new toggle for enabling/disabling auto-open behaviour + * 2. Adding a divider between the toggles and opt-out button + * 3. Showing text indicating that Review Checker is enabled (not opted-out) above the opt-out button + * + * Only show if `browser.shopping.experience2023.autoOpen.enabled` is true. + */ + let autoOpenDescriptionL10nId; + let autoOpenDescriptionL10nArgs; + + switch (this.hostname) { + case "www.amazon.fr": + case "www.amazon.de": + autoOpenDescriptionL10nId = + "shopping-settings-auto-open-description-single-site"; + autoOpenDescriptionL10nArgs = { + currentSite: "Amazon", + }; + break; + default: + autoOpenDescriptionL10nId = + "shopping-settings-auto-open-description-three-sites"; + autoOpenDescriptionL10nArgs = { + firstSite: "Amazon", + secondSite: "Best Buy", + thirdSite: "Walmart", + }; + } + + let autoOpenToggleMarkup = this.autoOpenEnabled + ? html`
    + + + +
    ` + : null; + + return html` + + + +
    +
    + ${adsToggleMarkup} ${autoOpenToggleMarkup} +
    + ${this.autoOpenEnabled + ? html`` + : null} +
    + ${this.autoOpenEnabled + ? html`` + : null} + +
    +
    +
    +

    + +

    + `; + } +} + +customElements.define("shopping-settings", ShoppingSettings); diff --git a/browser/components/shopping/content/shopping-card.css b/browser/components/shopping/content/shopping-card.css new file mode 100644 index 0000000000..5b0ce9c280 --- /dev/null +++ b/browser/components/shopping/content/shopping-card.css @@ -0,0 +1,201 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +:host { + display: block; + --shopping-card-border-color: light-dark(#f0f0f4, #52525e); + --shopping-card-border-radius: 8px; + --shopping-card-border-width: 1px; + --shopping-card-summary-border-radius: calc(var(--shopping-card-border-radius) - var(--shopping-card-border-width)); + --shopping-card-padding: 12px; + border-radius: var(--shopping-card-border-radius); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + box-sizing: border-box; +} + +.shopping-card { + display: flex; + border: var(--shopping-card-border-width) solid var(--shopping-card-border-color); + border-radius: var(--shopping-card-border-radius); + background-color: var(--in-content-page-background); + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding: var(--shopping-card-padding); + position: relative; +} + +/* For accordions, adjust padding and border so that focus outlines wrap around the summary. */ +:host([type="accordion"]) > .shopping-card { + padding: 0; + + summary { + padding: var(--shopping-card-padding); + border-radius: var(--shopping-card-summary-border-radius); + } + + #content { + padding: 0 var(--shopping-card-padding) var(--shopping-card-padding); + } + + > details[open] > summary { + border-radius: var(--shopping-card-summary-border-radius) var(--shopping-card-summary-border-radius) 0 0; + } +} + +button { + margin: 0; +} + +#shopping-details { + width: 100%; +} + +#label-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + width: 100%; +} + +:host([type="accordion"]) #label-wrapper { + cursor: pointer; + position: relative; + + & > #header { + /* 24px for the ghost button, 12px gap between + * text and button. */ + margin-inline-end: 36px; + } +} + +#header { + color: var(--in-content-text-color); + font-size: 1em; + margin: 0; +} + +#content { + align-self: stretch; +} + +details > summary { + list-style: none; +} + +details > summary:focus-visible { + outline: var(--in-content-focus-outline); +} + +.chevron-icon { + background-image: url("chrome://global/skin/icons/arrow-down.svg"); + background-position: center; + background-repeat: no-repeat; + color: var(--icon-color); + -moz-context-properties: fill; + fill: currentColor; + width: 24px; + height: 24px; + /* override some default button sizing styles */ + min-width: 24px; + min-height: 24px; + padding: 0; + /* Abspos rather than flexbox so we don't influence the padding + * around the title. + */ + position: absolute; + top: calc(50% - var(--shopping-card-padding)); + /* This approximates the top/bottom 'padding' gap created by the abspos + * above. It won't always be perfectly accurate, and that's OK. */ + inset-inline-end: -2px; +} + +details[open] .chevron-icon { + background-image: url("chrome://global/skin/icons/arrow-up.svg"); +} + +.show-more footer { + width: 100%; + background-color: var(--in-content-page-background); + box-shadow: 2px -10px 11px var(--in-content-page-background); + border-top: var(--shopping-card-border-width) solid var(--shopping-card-border-color); + border-radius: 0 0 var(--shopping-card-border-radius) var(--shopping-card-border-radius); + position: absolute; + bottom: 0; + text-align: center; + padding-block: 8px; + left: 0; + right: 0; +} + +.show-more[expanded="false"] { + overflow: clip; + height: 200px; +} + +:host(:not([showMoreButtonDisabled])) .show-more ::slotted(div) { + margin-block-end: 4rem; +} + +:host([showMoreButtonDisabled]) footer { + display: none; +} + +@media (prefers-color-scheme: dark) { + :host > .shopping-card { + background-color: #42414d; + } + + .show-more footer { + background-color: #42414d; + box-shadow: 2px -10px 11px #42414d; + } +} + +@media (prefers-contrast) { + /* Style accordion card like a dropdown button. */ + :host([type="accordion"]) > .shopping-card { + border: var(--shopping-card-border-width) solid ButtonText; + + summary { + background-color: var(--button-background-color); + color: ButtonText; + + #label-wrapper { + color: inherit; + + > :is(#header, .chevron-icon) { + color: inherit; + } + } + + .chevron-icon { + background-color: transparent; + border: none; + } + + &:hover { + background-color: SelectedItemText; + border-color: SelectedItem; + color: SelectedItem; + } + + &:hover:active { + border-color: ButtonText; + } + } + + details[open] { + border-color: var(--shopping-card-border-color); + + summary { + border-block-end: var(--shopping-card-border-width) solid ButtonText; + } + } + } +} diff --git a/browser/components/shopping/content/shopping-card.mjs b/browser/components/shopping/content/shopping-card.mjs new file mode 100644 index 0000000000..7b2448df36 --- /dev/null +++ b/browser/components/shopping/content/shopping-card.mjs @@ -0,0 +1,204 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +const MIN_SHOW_MORE_HEIGHT = 200; +/** + * A card container to be used in the shopping sidebar. There are three card types. + * The default type where no type attribute is required and the card will have no extra functionality. + * The "accordion" type will initially not show any content. The card will contain a arrow to expand the + * card so all of the content is visible. + * + * @property {string} label - The label text that will be used for the card header + * @property {string} type - (optional) The type of card. No type specified + * will be the default card. The other available types are "accordion" and "show-more". + */ +class ShoppingCard extends MozLitElement { + static properties = { + label: { type: String }, + type: { type: String }, + _isExpanded: { type: Boolean }, + }; + + static get queries() { + return { + detailsEl: "#shopping-details", + contentEl: "#content", + }; + } + + labelTemplate() { + if (this.label) { + if (this.type === "accordion") { + return html` +
    + + +
    + `; + } + return html` +
    + + +
    + `; + } + return ""; + } + + cardTemplate() { + if (this.type === "accordion") { + return html` +
    + ${this.labelTemplate()} +
    +
    + `; + } else if (this.type === "show-more") { + return html` + ${this.labelTemplate()} +
    + + +
    + +
    +
    + `; + } + return html` + ${this.labelTemplate()} +
    + +
    + `; + } + + onCardToggle() { + const action = this.detailsEl.open ? "expanded" : "collapsed"; + let l10nId = this.getAttribute("data-l10n-id"); + switch (l10nId) { + case "shopping-settings-label": + Glean.shopping.surfaceSettingsExpandClicked.record({ action }); + break; + case "shopping-analysis-explainer-label": + Glean.shopping.surfaceShowQualityExplainerClicked.record({ + action, + }); + break; + } + } + + handleShowMoreButtonClick(e) { + this._isExpanded = !this._isExpanded; + // toggle show more/show less text + e.target.setAttribute( + "data-l10n-id", + this._isExpanded + ? "shopping-show-less-button" + : "shopping-show-more-button" + ); + // toggle content expanded attribute + this.contentEl.attributes.expanded.value = this._isExpanded; + + let action = this._isExpanded ? "expanded" : "collapsed"; + Glean.shopping.surfaceShowMoreReviewsButtonClicked.record({ + action, + }); + } + + enableShowMoreButton() { + this._isExpanded = false; + this.toggleAttribute("showMoreButtonDisabled", false); + this.contentEl.attributes.expanded.value = false; + } + + disableShowMoreButton() { + this._isExpanded = true; + this.toggleAttribute("showMoreButtonDisabled", true); + this.contentEl.attributes.expanded.value = true; + } + + handleChevronButtonClick() { + this.detailsEl.open = !this.detailsEl.open; + } + + firstUpdated() { + if (this.type !== "show-more") { + return; + } + + let contentSlot = this.shadowRoot.querySelector("slot[name='content']"); + let contentSlotEls = contentSlot.assignedElements(); + if (!contentSlotEls.length) { + return; + } + + let slottedDiv = contentSlotEls[0]; + + this.handleContentSlotResize = this.handleContentSlotResize.bind(this); + this.contentResizeObserver = new ResizeObserver( + this.handleContentSlotResize + ); + this.contentResizeObserver.observe(slottedDiv); + } + + disconnectedCallback() { + this.contentResizeObserver?.disconnect(); + } + + handleContentSlotResize(entries) { + for (let entry of entries) { + if (entry.contentRect.height === 0) { + return; + } + + if (entry.contentRect.height < MIN_SHOW_MORE_HEIGHT) { + this.disableShowMoreButton(); + } else if (this.hasAttribute("showMoreButtonDisabled")) { + this.enableShowMoreButton(); + } + } + } + + render() { + return html` + + +
    + ${this.cardTemplate()} +
    + `; + } +} +customElements.define("shopping-card", ShoppingCard); diff --git a/browser/components/shopping/content/shopping-container.css b/browser/components/shopping/content/shopping-container.css new file mode 100644 index 0000000000..6ac09964bf --- /dev/null +++ b/browser/components/shopping/content/shopping-container.css @@ -0,0 +1,152 @@ +/* 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/. */ + +:host { + --shopping-close-button-size: var(--button-min-height); + --shopping-header-font-size: 1.3rem; +} + +#shopping-container { + display: flex; + flex-direction: column; + width: 100%; +} + +#header-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + background-color: var(--shopping-header-background); + box-sizing: border-box; + padding-block: 16px 8px; + padding-inline: 16px 8px; + position: sticky; + top: 0; + width: 100%; + z-index: 2; +} + +#shopping-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +#shopping-header-title { + font-size: var(--shopping-header-font-size); + font-weight: var(--font-weight-bold); + margin: 0; +} + +.shopping-header-overflow { + box-shadow: 0 1px 2px 0 rgba(58, 57, 68, 0.20); +} + +#beta-marker { + font-size: var(--font-size-small); + font-weight: var(--font-weight-default); + padding: 2px 4px; + margin: 0; + line-height: 150%; + text-transform: uppercase; + color: var(--icon-color); + border: 1px solid var(--icon-color); + border-radius: var(--border-radius-small); +} + +#close-button { + min-width: var(--shopping-close-button-size); + min-height: var(--shopping-close-button-size); + -moz-context-properties: fill; + fill: currentColor; + background-image: url("chrome://global/skin/icons/close.svg"); + background-repeat: no-repeat; + background-position: center; + margin-inline-end: 0; + + @media not (prefers-contrast) { + color: var(--icon-color); + } +} + +#content { + overflow: auto; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 16px; + padding: 0 16px 16px; +} + +#content:focus-visible { + outline-offset: -2px; +} + +.loading-box { + box-shadow: none; + border: none; + background: var(--in-content-button-background); + margin-block: 1rem; +} + +.loading-box.small { + height: 2.67rem; +} + +.loading-box.medium { + height: 5.34rem; +} + +.loading-box.large { + height: 12.8rem; +} + +.loading-box:nth-child(odd) { + background-color: var(--in-content-button-background); +} + +.loading-box:nth-child(even) { + background-color: var(--in-content-button-background-hover); +} + +@media not (prefers-reduced-motion) { + .animate > .loading-box { + animation-name: fade-in; + animation-direction: alternate; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + } + + /* First box + every 4th box, fifth box + every 4th box */ + .loading-box:nth-child(4n-3) { + animation-delay: -1s; + } + + /* Second box + every 4th box, sixth box + every 4th box */ + .loading-box:nth-child(4n-2) { + animation-delay: 0s; + } + + /* Third box + every 4th box */ + .loading-box:nth-child(4n-1) { + animation-delay: -1.5s; + } + + /* Fourth box + every 4th box */ + .loading-box:nth-child(4n) { + animation-delay: -0.5s; + } + + @keyframes fade-in { + from { + opacity: .25; + } + to { + opacity: 1; + } + } +} diff --git a/browser/components/shopping/content/shopping-container.mjs b/browser/components/shopping/content/shopping-container.mjs new file mode 100644 index 0000000000..a1a6774d49 --- /dev/null +++ b/browser/components/shopping/content/shopping-container.mjs @@ -0,0 +1,471 @@ +/* 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/. */ + +/* eslint-env mozilla/remote-page */ + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/highlights.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/settings.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/adjusted-rating.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/reliability.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/analysis-explainer.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-message-bar.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/unanalyzed.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/recommended-ad.mjs"; + +// The number of pixels that must be scrolled from the +// top of the sidebar to show the header box shadow. +const HEADER_SCROLL_PIXEL_OFFSET = 8; + +const SIDEBAR_CLOSED_COUNT_PREF = + "browser.shopping.experience2023.sidebarClosedCount"; +const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF = + "browser.shopping.experience2023.showKeepSidebarClosedMessage"; +const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active"; + +export class ShoppingContainer extends MozLitElement { + static properties = { + data: { type: Object }, + showOnboarding: { type: Boolean }, + productUrl: { type: String }, + recommendationData: { type: Array }, + isOffline: { type: Boolean }, + analysisEvent: { type: Object }, + userReportedAvailable: { type: Boolean }, + adsEnabled: { type: Boolean }, + adsEnabledByUser: { type: Boolean }, + isAnalysisInProgress: { type: Boolean }, + analysisProgress: { type: Number }, + isOverflow: { type: Boolean }, + autoOpenEnabled: { type: Boolean }, + autoOpenEnabledByUser: { type: Boolean }, + showingKeepClosedMessage: { type: Boolean }, + }; + + static get queries() { + return { + reviewReliabilityEl: "review-reliability", + adjustedRatingEl: "adjusted-rating", + highlightsEl: "review-highlights", + settingsEl: "shopping-settings", + analysisExplainerEl: "analysis-explainer", + unanalyzedProductEl: "unanalyzed-product-card", + shoppingMessageBarEl: "shopping-message-bar", + recommendedAdEl: "recommended-ad", + loadingEl: "#loading-wrapper", + closeButtonEl: "#close-button", + keepClosedMessageBarEl: "#keep-closed-message-bar", + }; + } + + connectedCallback() { + super.connectedCallback(); + if (this.initialized) { + return; + } + this.initialized = true; + + window.document.addEventListener("Update", this); + window.document.addEventListener("NewAnalysisRequested", this); + window.document.addEventListener("ReanalysisRequested", this); + window.document.addEventListener("ReportedProductAvailable", this); + window.document.addEventListener("adsEnabledByUserChanged", this); + window.document.addEventListener("scroll", this); + window.document.addEventListener("UpdateRecommendations", this); + window.document.addEventListener("UpdateAnalysisProgress", this); + window.document.addEventListener("autoOpenEnabledByUserChanged", this); + window.document.addEventListener("ShowKeepClosedMessage", this); + window.document.addEventListener("HideKeepClosedMessage", this); + + window.dispatchEvent( + new CustomEvent("ContentReady", { + bubbles: true, + composed: true, + }) + ); + } + + updated() { + if (this.focusCloseButton) { + this.closeButtonEl.focus(); + } + } + + async _update({ + data, + showOnboarding, + productUrl, + recommendationData, + adsEnabled, + adsEnabledByUser, + isAnalysisInProgress, + analysisProgress, + focusCloseButton, + autoOpenEnabled, + autoOpenEnabledByUser, + }) { + // If we're not opted in or there's no shopping URL in the main browser, + // the actor will pass `null`, which means this will clear out any existing + // content in the sidebar. + this.data = data; + this.showOnboarding = showOnboarding; + this.productUrl = productUrl; + this.recommendationData = recommendationData; + this.isOffline = !navigator.onLine; + this.isAnalysisInProgress = isAnalysisInProgress; + this.adsEnabled = adsEnabled; + this.adsEnabledByUser = adsEnabledByUser; + this.analysisProgress = analysisProgress; + this.focusCloseButton = focusCloseButton; + this.autoOpenEnabled = autoOpenEnabled; + this.autoOpenEnabledByUser = autoOpenEnabledByUser; + } + + _updateRecommendations({ recommendationData }) { + this.recommendationData = recommendationData; + } + + _updateAnalysisProgress({ progress }) { + this.analysisProgress = progress; + } + + handleEvent(event) { + switch (event.type) { + case "Update": + this._update(event.detail); + break; + case "NewAnalysisRequested": + case "ReanalysisRequested": + this.isAnalysisInProgress = true; + this.analysisEvent = { + type: event.type, + productUrl: this.productUrl, + }; + window.dispatchEvent( + new CustomEvent("PolledRequestMade", { + bubbles: true, + composed: true, + }) + ); + break; + case "ReportedProductAvailable": + this.userReportedAvailable = true; + window.dispatchEvent( + new CustomEvent("ReportProductAvailable", { + bubbles: true, + composed: true, + }) + ); + Glean.shopping.surfaceReactivatedButtonClicked.record(); + break; + case "adsEnabledByUserChanged": + this.adsEnabledByUser = event.detail?.adsEnabledByUser; + break; + case "scroll": + let scrollYPosition = window.scrollY; + this.isOverflow = scrollYPosition > HEADER_SCROLL_PIXEL_OFFSET; + break; + case "UpdateRecommendations": + this._updateRecommendations(event.detail); + break; + case "UpdateAnalysisProgress": + this._updateAnalysisProgress(event.detail); + break; + case "autoOpenEnabledByUserChanged": + this.autoOpenEnabledByUser = event.detail?.autoOpenEnabledByUser; + break; + case "ShowKeepClosedMessage": + this.showingKeepClosedMessage = true; + break; + case "HideKeepClosedMessage": + this.showingKeepClosedMessage = false; + break; + } + } + + getHostnameFromProductUrl() { + let hostname; + try { + hostname = new URL(this.productUrl)?.hostname; + return hostname; + } catch (e) { + console.error(`Unknown product url ${this.productUrl}.`); + return null; + } + } + + analysisDetailsTemplate() { + /* At present, en is supported as the default language for reviews. As we support more sites, + * update `lang` accordingly if highlights need to be displayed in other languages. */ + let lang; + let hostname = this.getHostnameFromProductUrl(); + + switch (hostname) { + case "www.amazon.fr": + lang = "fr"; + break; + case "www.amazon.de": + lang = "de"; + break; + default: + lang = "en"; + } + return html` + + + + `; + } + + contentTemplate() { + // The user requested an analysis which is not done yet. + if ( + this.analysisEvent?.productUrl == this.productUrl && + this.isAnalysisInProgress + ) { + const isReanalysis = this.analysisEvent.type === "ReanalysisRequested"; + return html` + ${isReanalysis ? this.analysisDetailsTemplate() : null}`; + } + + if (this.data?.error) { + return html``; + } + + if (this.data.page_not_supported) { + return html``; + } + + if (this.data.deleted_product_reported) { + return html``; + } + + if (this.data.deleted_product) { + return this.userReportedAvailable + ? html`` + : html``; + } + + if (this.data.needs_analysis) { + if (!this.data.product_id || typeof this.data.grade != "string") { + // Product is new to us. + return html``; + } + + // We successfully analyzed the product before, but the current analysis is outdated and can be updated + // via a re-analysis. + return html` + + ${this.analysisDetailsTemplate()} + `; + } + + if (this.data.not_enough_reviews) { + // We already saw and tried to analyze this product before, but there are not enough reviews + // to make a detailed analysis. + return html``; + } + + return this.analysisDetailsTemplate(); + } + + recommendationTemplate() { + const canShowAds = this.adsEnabled && this.adsEnabledByUser; + if (this.recommendationData?.length && canShowAds) { + return html``; + } + return null; + } + + /** + * @param {object?} options + * @param {boolean?} options.animate = true + * Whether to animate the loading state. Defaults to true. + * There will be no animation for users who prefer reduced motion, + * irrespective of the value of this option. + */ + loadingTemplate({ animate = true } = {}) { + /* Due to limitations with aria-busy for certain screen readers + * (see Bug 1682063), mark loading container as a pseudo image and + * use aria-label as a workaround. */ + return html` + + `; + } + + renderContainer(sidebarContent, hideFooter = false) { + return html` + + +
    +
    +
    +

    +

    +
    + +
    +
    + + ${this.keepClosedMessageTemplate()}${sidebarContent} + ${!hideFooter ? this.footerTemplate() : null} +
    +
    `; + } + + footerTemplate() { + let hostname = this.getHostnameFromProductUrl(); + return html` + + ${this.recommendationTemplate()} + + `; + } + + keepClosedMessageTemplate() { + if ( + this.autoOpenEnabled && + this.autoOpenEnabledByUser && + this.showingKeepClosedMessage && + RPMGetBoolPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true) + ) { + return html``; + } + return null; + } + + render() { + let content; + let hideFooter; + if (this.showOnboarding) { + content = html``; + hideFooter = true; + } else if (this.isOffline) { + content = this.loadingTemplate({ animate: false }); + hideFooter = true; + } else if (!this.data) { + if (this.isAnalysisInProgress) { + content = html``; + } else { + content = this.loadingTemplate(); + hideFooter = true; + } + } else { + content = this.contentTemplate(); + } + return this.renderContainer(content, hideFooter); + } + + handleCloseButtonClick() { + if (this.autoOpenEnabled && this.autoOpenEnabledByUser) { + let sidebarClosedCount = RPMGetIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0); + if ( + !this.showingKeepClosedMessage && + sidebarClosedCount >= 4 && + RPMGetBoolPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, true) + ) { + this.showingKeepClosedMessage = true; + return; + } + + this.showingKeepClosedMessage = false; + + if (sidebarClosedCount >= 6) { + RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false); + } + + RPMSetPref(SIDEBAR_CLOSED_COUNT_PREF, sidebarClosedCount + 1); + } + + RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false); + Glean.shopping.surfaceClosed.record({ source: "closeButton" }); + } +} + +customElements.define("shopping-container", ShoppingContainer); diff --git a/browser/components/shopping/content/shopping-message-bar.css b/browser/components/shopping/content/shopping-message-bar.css new file mode 100644 index 0000000000..f88b18be45 --- /dev/null +++ b/browser/components/shopping/content/shopping-message-bar.css @@ -0,0 +1,78 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +:host { + display: block; + border-radius: 4px; +} + +#message-bar-container { + display: flex; + flex-direction: column; + gap: 4px; + padding: 0; + margin: 0; +} + +:host([type="stale"]) #message-bar-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + column-gap: 8px; + + > span { + display: flex; + flex-direction: column; + align-self: center; + } +} + +button { + margin-inline-start: 0; +} + +message-bar::part(container) { + align-items: start; + padding: 0.5rem 0.75rem; + gap: 0.75rem; +} + +message-bar::part(icon) { + padding: 0; +} + +:host([type=analysis-in-progress]) message-bar::part(icon), +:host([type=reanalysis-in-progress]) message-bar::part(icon) { + border: 1px solid var(--icon-color); + border-radius: 50%; +} + +:host([type=analysis-in-progress]) message-bar::part(icon)::after, +:host([type=reanalysis-in-progress]) message-bar::part(icon)::after { + --message-bar-icon-url: conic-gradient(var(--icon-color-information) var(--analysis-progress-pcent, 0%), transparent var(--analysis-progress-pcent, 0%)); + border-radius: 50%; + margin-block: 1px 0; + margin-inline: 1px 0; + width: calc(var(--icon-size) - 2px); + height: calc(var(--icon-size) - 2px); +} + +:host([type=reanalysis-in-progress]) message-bar::part(container), +:host([type=stale]) message-bar::part(container) { + align-items: center; + background-color: transparent; + padding: 0; +} + +:host([type=thank-you-for-feedback]) message-bar::part(icon) { + --message-bar-icon-url: url("chrome://global/skin/icons/check-filled.svg"); +} + +:host([type=thank-you-for-feedback]) message-bar::part(container) { + text-align: start; + align-items: center; + gap: 12px; +} diff --git a/browser/components/shopping/content/shopping-message-bar.mjs b/browser/components/shopping/content/shopping-message-bar.mjs new file mode 100644 index 0000000000..d6ec9c0888 --- /dev/null +++ b/browser/components/shopping/content/shopping-message-bar.mjs @@ -0,0 +1,278 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ + +/* eslint-env mozilla/remote-page */ + +import { html, styleMap } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-message-bar.mjs"; + +const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active"; +const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF = + "browser.shopping.experience2023.showKeepSidebarClosedMessage"; +const SHOPPING_AUTO_OPEN_SIDEBAR_PREF = + "browser.shopping.experience2023.autoOpen.userEnabled"; + +class ShoppingMessageBar extends MozLitElement { + #MESSAGE_TYPES_RENDER_TEMPLATE_MAPPING = new Map([ + ["stale", () => this.staleWarningTemplate()], + ["generic-error", () => this.genericErrorTemplate()], + ["not-enough-reviews", () => this.notEnoughReviewsTemplate()], + ["product-not-available", () => this.productNotAvailableTemplate()], + ["thanks-for-reporting", () => this.thanksForReportingTemplate()], + [ + "product-not-available-reported", + () => this.productNotAvailableReportedTemplate(), + ], + ["analysis-in-progress", () => this.analysisInProgressTemplate()], + ["reanalysis-in-progress", () => this.reanalysisInProgressTemplate()], + ["page-not-supported", () => this.pageNotSupportedTemplate()], + ["thank-you-for-feedback", () => this.thankYouForFeedbackTemplate()], + ["keep-closed", () => this.keepClosedTemplate()], + ]); + + static properties = { + type: { type: String }, + productUrl: { type: String, reflect: true }, + progress: { type: Number, reflect: true }, + }; + + static get queries() { + return { + reAnalysisButtonEl: "#message-bar-reanalysis-button", + productAvailableBtnEl: "#message-bar-report-product-available-btn", + yesKeepClosedButtonEl: "#yes-keep-closed-button", + noThanksButtonEl: "#no-thanks-button", + }; + } + + onClickAnalysisButton() { + this.dispatchEvent( + new CustomEvent("ReanalysisRequested", { + bubbles: true, + composed: true, + }) + ); + Glean.shopping.surfaceReanalyzeClicked.record(); + } + + onClickProductAvailable() { + this.dispatchEvent( + new CustomEvent("ReportedProductAvailable", { + bubbles: true, + composed: true, + }) + ); + } + + handleNoThanksClick() { + RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false); + RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false); + this.dispatchEvent( + new CustomEvent("HideKeepClosedMessage", { + bubbles: true, + composed: true, + }) + ); + Glean.shopping.surfaceNoThanksButtonClicked.record(); + } + + handleKeepClosedClick() { + RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false); + RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false); + RPMSetPref(SHOPPING_AUTO_OPEN_SIDEBAR_PREF, false); + this.dispatchEvent( + new CustomEvent("HideKeepClosedMessage", { + bubbles: true, + composed: true, + }) + ); + Glean.shopping.surfaceYesKeepClosedButtonClicked.record(); + } + + staleWarningTemplate() { + return html` +
    + + +
    +
    `; + } + + genericErrorTemplate() { + return html` + `; + } + + notEnoughReviewsTemplate() { + return html` + `; + } + + productNotAvailableTemplate() { + return html` + + `; + } + + thanksForReportingTemplate() { + return html` + `; + } + + productNotAvailableReportedTemplate() { + return html` + `; + } + + analysisInProgressTemplate() { + return html` +
    + + +
    +
    `; + } + + reanalysisInProgressTemplate() { + return html` +
    + +
    +
    `; + } + + pageNotSupportedTemplate() { + return html` + `; + } + + thankYouForFeedbackTemplate() { + return html` + `; + } + + keepClosedTemplate() { + return html` + + + + + `; + } + + render() { + let messageBarTemplate = this.#MESSAGE_TYPES_RENDER_TEMPLATE_MAPPING.get( + this.type + )(); + if (messageBarTemplate) { + if (this.type == "stale") { + Glean.shopping.surfaceStaleAnalysisShown.record(); + } + return html` + + + ${messageBarTemplate} + `; + } + return null; + } +} + +customElements.define("shopping-message-bar", ShoppingMessageBar); diff --git a/browser/components/shopping/content/shopping-page.css b/browser/components/shopping/content/shopping-page.css new file mode 100644 index 0000000000..b6fd43704f --- /dev/null +++ b/browser/components/shopping/content/shopping-page.css @@ -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/. */ + +:root { + --shopping-header-background: light-dark(#f9f9fb, #2b2a33); + background-color: var(--shopping-header-background); + font: menu; +} + +@media (prefers-contrast) { + button.shopping-button:enabled, + button.ghost-button:not(.semi-transparent):enabled { + background-color: ButtonFace; + border-color: ButtonText; + color: ButtonText; + + &:hover { + background-color: SelectedItemText; + border-color: SelectedItem; + color: SelectedItem; + } + + &:hover:active { + border-color: ButtonText; + } + } +} diff --git a/browser/components/shopping/content/shopping-sidebar.js b/browser/components/shopping/content/shopping-sidebar.js new file mode 100644 index 0000000000..6873682ae6 --- /dev/null +++ b/browser/components/shopping/content/shopping-sidebar.js @@ -0,0 +1,80 @@ +/* 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"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + const SHOPPING_SIDEBAR_WIDTH_PREF = + "browser.shopping.experience2023.sidebarWidth"; + class ShoppingSidebar extends MozXULElement { + #browser; + #initialized; + + static get markup() { + return ` + + `; + } + + constructor() { + super(); + } + + connectedCallback() { + this.initialize(); + } + + initialize() { + if (this.#initialized) { + return; + } + this.resizeObserverFn = this.resizeObserverFn.bind(this); + this.appendChild(this.constructor.fragment); + this.#browser = this.querySelector(".shopping-sidebar"); + + let previousWidth = Services.prefs.getIntPref( + SHOPPING_SIDEBAR_WIDTH_PREF, + 0 + ); + if (previousWidth > 0) { + this.style.width = `${previousWidth}px`; + } + + this.resizeObserver = new ResizeObserver(this.resizeObserverFn); + this.resizeObserver.observe(this); + + this.#initialized = true; + } + + resizeObserverFn(entries) { + for (let entry of entries) { + if (entry.contentBoxSize[0].inlineSize < 1) { + return; + } + + Services.prefs.setIntPref( + SHOPPING_SIDEBAR_WIDTH_PREF, + entry.contentBoxSize[0].inlineSize + ); + } + } + } + + customElements.define("shopping-sidebar", ShoppingSidebar); +} diff --git a/browser/components/shopping/content/shopping.ftl b/browser/components/shopping/content/shopping.ftl new file mode 100644 index 0000000000..282c65656b --- /dev/null +++ b/browser/components/shopping/content/shopping.ftl @@ -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/. + +### This file is not in a locales directory to prevent it from +### being translated as the feature is still in heavy development +### and strings are likely to change often. diff --git a/browser/components/shopping/content/shopping.html b/browser/components/shopping/content/shopping.html new file mode 100644 index 0000000000..1c5d627869 --- /dev/null +++ b/browser/components/shopping/content/shopping.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + diff --git a/browser/components/shopping/content/unanalyzed.css b/browser/components/shopping/content/unanalyzed.css new file mode 100644 index 0000000000..d2e2b0b4b0 --- /dev/null +++ b/browser/components/shopping/content/unanalyzed.css @@ -0,0 +1,41 @@ +/* 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 url("chrome://global/skin/in-content/common.css"); + +#unanalyzed-product-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +#unanalyzed-product-icon { + max-width: 264px; + max-height: 290px; + width: 100%; + content: url("chrome://browser/content/shopping/assets/unanalyzedLight.avif"); + + @media (prefers-color-scheme: dark) { + content: url("chrome://browser/content/shopping/assets/unanalyzedDark.avif"); + } +} + +#unanalyzed-product-message-content { + display: flex; + flex-direction: column; + line-height: 1.5; + + > h2 { + font-size: inherit; + } + + > p { + margin-block: 0.25rem; + } +} + +#unanalyzed-product-analysis-button { + width: 100%; +} diff --git a/browser/components/shopping/content/unanalyzed.mjs b/browser/components/shopping/content/unanalyzed.mjs new file mode 100644 index 0000000000..0be85b65e4 --- /dev/null +++ b/browser/components/shopping/content/unanalyzed.mjs @@ -0,0 +1,61 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/shopping/shopping-card.mjs"; + +class UnanalyzedProductCard extends MozLitElement { + static properties = { + productURL: { type: String, reflect: true }, + }; + + static get queries() { + return { + analysisButtonEl: "#unanalyzed-product-analysis-button", + }; + } + + onClickAnalysisButton() { + this.dispatchEvent( + new CustomEvent("NewAnalysisRequested", { + bubbles: true, + composed: true, + }) + ); + Glean.shopping.surfaceAnalyzeReviewsNoneAvailableClicked.record(); + } + + render() { + return html` + + +
    + +
    +

    +

    +
    + +
    +
    + `; + } +} + +customElements.define("unanalyzed-product-card", UnanalyzedProductCard); -- cgit v1.2.3