Bug 1838041 - Add moz-five-star reuseable component. r=shopping-reviewers,desktop-theme-reviewers,dao,fluent-reviewers,flod,robwu,mstriemer,kpatenio

Differential Revision: https://phabricator.services.mozilla.com/D180877
This commit is contained in:
Niklas Baumgardner 2023-06-22 21:29:28 +00:00
Родитель 615db5a796
Коммит 5f14327281
18 изменённых файлов: 272 добавлений и 113 удалений

Просмотреть файл

@ -0,0 +1,9 @@
# 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/.
# The rating out of 5 stars.
# Variables:
# $rating (number) - A number between 0 and 5. The translation should show at most one digit after the comma.
moz-five-star-rating =
.title = Rated { NUMBER($rating, maximumFractionDigits: 1) } out of 5

Просмотреть файл

@ -526,16 +526,16 @@
}
.urlbarView-dynamic-addons-rating[fill="empty"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#empty");
background-image: url("chrome://global/skin/icons/rating-star.svg#empty");
}
.urlbarView-dynamic-addons-rating[fill="half"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#half");
background-image: url("chrome://global/skin/icons/rating-star.svg#half");
}
.urlbarView-dynamic-addons-rating[fill="half"]:-moz-locale-dir(rtl) {
transform: scaleX(-1);
}
.urlbarView-dynamic-addons-rating[fill="full"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#full");
background-image: url("chrome://global/skin/icons/rating-star.svg#full");
}
.urlbarView-dynamic-addons-reviews {

Просмотреть файл

@ -0,0 +1,20 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
from fluent.migrate.helpers import transforms_from
from fluent.migrate import COPY_PATTERN
def migrate(ctx):
"""Bug 1838041 - Create MozFiveStar reusable component , part {index}."""
ctx.add_transforms(
"browser/browser/components/mozFiveStar.ftl",
"browser/browser/components/mozFiveStar.ftl",
transforms_from(
"""
moz-five-star-rating =
.title = {COPY_PATTERN(from_path, "five-star-rating.title")}
""",
from_path="toolkit/toolkit/about/aboutAddons.ftl",
),
)

Просмотреть файл

@ -95,6 +95,8 @@ toolkit.jar:
content/global/elements/menupopup.js (widgets/menupopup.js)
content/global/elements/moz-button-group.css (widgets/moz-button-group/moz-button-group.css)
content/global/elements/moz-button-group.mjs (widgets/moz-button-group/moz-button-group.mjs)
content/global/elements/moz-five-star.css (widgets/moz-five-star/moz-five-star.css)
content/global/elements/moz-five-star.mjs (widgets/moz-five-star/moz-five-star.mjs)
content/global/elements/moz-input-box.js (widgets/moz-input-box.js)
content/global/elements/moz-label.css (widgets/moz-label/moz-label.css)
content/global/elements/moz-label.mjs (widgets/moz-label/moz-label.mjs)

Просмотреть файл

@ -26,6 +26,7 @@ run-if = os == "mac" && os_version != "10.15" # Mac only feature, requires > 10.
[test_menubar.xhtml]
skip-if = os == 'mac'
[test_moz_button_group.html]
[test_moz_five_star.html]
[test_moz_label.html]
[test_moz_support_link.html]
[test_moz_toggle.html]

Просмотреть файл

@ -0,0 +1,74 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>MozFiveStar Tests</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<script type="module" src="chrome://global/content/elements/moz-five-star.mjs"></script>
</head>
<body>
<p id="display"></p>
<div id="content" style="max-width: fit-content">
<moz-five-star label="Label" rating="2.5"></moz-five-star>
</div>
<pre id="test">
<script class="testbody" type="application/javascript">
const { BrowserTestUtils } = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs");
add_task(async function testMozFiveStar() {
const mozFiveStar = document.querySelector("moz-five-star");
ok(mozFiveStar, "moz-five-star is rendered");
const stars = mozFiveStar.starEls;
ok(stars, "moz-five-star has stars");
is(stars.length, 5, "moz-five-star stars count is 5");
const rating = mozFiveStar.rating;
ok(rating, "moz-five-star has a rating");
is(rating, 2.5, "moz-five-star rating is 2.5");
});
add_task(async function testMozFiveStarsDisplay() {
const mozFiveStar = document.querySelector("moz-five-star");
ok(mozFiveStar, "moz-five-star is rendered");
async function testRating(rating, ratingRounded, expectation) {
mozFiveStar.rating = rating;
await mozFiveStar.updateComplete;
if (mozFiveStar.ownerDocument.hasPendingL10nMutations) {
await BrowserTestUtils.waitForEvent(
mozFiveStar.ownerDocument,
"L10nMutationsFinished"
);
}
let starsString = Array.from(mozFiveStar.starEls)
.map(star => star.getAttribute("fill"))
.join(",");
is(starsString, expectation, `Rendering of rating ${rating}`);
is(
mozFiveStar.starsWrapperEl.title,
`Rated ${ratingRounded} out of 5`,
"Rendered title must contain at most one fractional digit"
);
}
await testRating(0.0, "0", "empty,empty,empty,empty,empty");
await testRating(0.249, "0.2", "empty,empty,empty,empty,empty");
await testRating(0.25, "0.3", "half,empty,empty,empty,empty");
await testRating(0.749, "0.7", "half,empty,empty,empty,empty");
await testRating(0.99, "1", "full,empty,empty,empty,empty");
await testRating(1.0, "1", "full,empty,empty,empty,empty");
await testRating(2, "2", "full,full,empty,empty,empty");
await testRating(3.0, "3", "full,full,full,empty,empty");
await testRating(4.001, "4", "full,full,full,full,empty");
await testRating(4.249, "4.2", "full,full,full,full,empty");
await testRating(4.25, "4.3", "full,full,full,full,half");
await testRating(4.749, "4.7", "full,full,full,full,half");
await testRating(4.89, "4.9", "full,full,full,full,full");
await testRating(5.0, "5", "full,full,full,full,full");
});
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -3,6 +3,15 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
:host {
display: flex;
justify-content: space-between;
}
:host([hidden]) {
display: none;
}
.stars {
--rating-star-size: 1em;
--rating-star-spacing: 0.3ch;
@ -12,15 +21,11 @@
align-content: center;
}
:host([hidden]) {
display: none;
}
.rating-star {
display: inline-block;
width: var(--rating-star-size);
height: var(--rating-star-size);
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#empty");
background-image: url("chrome://global/skin/icons/rating-star.svg#empty");
background-position: center;
background-repeat: no-repeat;
background-size: 100%;
@ -30,10 +35,10 @@
}
.rating-star[fill="half"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#half");
background-image: url("chrome://global/skin/icons/rating-star.svg#half");
}
.rating-star[fill="full"] {
background-image: url("chrome://mozapps/skin/extensions/rating-star.svg#full");
background-image: url("chrome://global/skin/icons/rating-star.svg#full");
}
.rating-star[fill="half"]:dir(rtl) {

Просмотреть файл

@ -0,0 +1,70 @@
/* 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 { ifDefined, html } from "../vendor/lit.all.mjs";
import { MozLitElement } from "../lit-utils.mjs";
MozXULElement.insertFTLIfNeeded("browser/components/mozFiveStar.ftl");
/**
* The visual representation is five stars, each of them either empty,
* half-filled or full. The fill state is derived from the rating,
* rounded to the nearest half.
*
* @tagname moz-five-star
* @property {number} rating - The rating out of 5.
* @property {string} title - The title text.
*/
export default class MozFiveStar extends MozLitElement {
static properties = {
rating: { type: Number, reflect: true },
title: { type: String },
};
static get queries() {
return {
starEls: { all: ".rating-star" },
starsWrapperEl: ".stars",
};
}
// Use a relative URL in storybook to get faster reloads on style changes.
static stylesheetUrl = window.IS_STORYBOOK
? "./moz-five-star/moz-five-star.css"
: "chrome://global/content/elements/moz-five-star.css";
getStarsFill() {
let starFill = [];
let roundedRating = Math.round(this.rating * 2) / 2;
for (let i = 1; i <= 5; i++) {
if (i <= roundedRating) {
starFill.push("full");
} else if (i - roundedRating === 0.5) {
starFill.push("half");
} else {
starFill.push("empty");
}
}
return starFill;
}
render() {
let starFill = this.getStarsFill();
return html`
<link rel="stylesheet" href=${this.constructor.stylesheetUrl} />
<div
class="stars"
data-l10n-id=${ifDefined(this.title ? null : "moz-five-star-rating")}
data-l10n-args=${ifDefined(
this.title ? null : JSON.stringify({ rating: this.rating ?? 0 })
)}
>
${starFill.map(
fill => html`<span class="rating-star" fill="${fill}"></span>`
)}
</div>
`;
}
}
customElements.define("moz-five-star", MozFiveStar);

Просмотреть файл

@ -0,0 +1,51 @@
/* 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 "../vendor/lit.all.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "./moz-five-star.mjs";
export default {
title: "UI Widgets/Five Star",
component: "moz-five-star",
parameters: {
status: "in-development",
fluent: `
moz-five-star-title =
.title = This is the title
moz-five-star-aria-label =
.aria-label = This is the aria-label
`,
},
};
const Template = ({ rating, ariaLabel, l10nId }) => html`
<div style="max-width: 400px">
<moz-five-star
rating=${rating}
aria-label=${ifDefined(ariaLabel)}
data-l10n-id=${ifDefined(l10nId)}
data-l10n-attrs="aria-label, title"
>
</moz-five-star>
</div>
`;
export const FiveStar = Template.bind({});
FiveStar.args = {
rating: 5.0,
l10nId: "moz-five-star-aria-label",
};
export const WithTitle = Template.bind({});
WithTitle.args = {
...FiveStar.args,
rating: 0,
l10nId: "moz-five-star-title",
};
export const Default = Template.bind({});
Default.args = {
rating: 3.33,
};

Просмотреть файл

@ -393,12 +393,6 @@ addon-detail-rating-label = Rating
install-postponed-message = This extension will be updated when { -brand-short-name } restarts.
install-postponed-button = Update Now
# The average rating that the add-on has received.
# Variables:
# $rating (number) - A number between 0 and 5. The translation should show at most one digit after the comma.
five-star-rating =
.title = Rated { NUMBER($rating, maximumFractionDigits: 1) } out of 5
# This string is used to show that an add-on is disabled.
# Variables:
# $name (string) - The name of the add-on

Просмотреть файл

@ -68,6 +68,10 @@
type="module"
src="chrome://global/content/elements/moz-support-link.mjs"
></script>
<script
type="module"
src="chrome://global/content/elements/moz-five-star.mjs"
></script>
</head>
<body>
<drag-drop-addon-installer></drag-drop-addon-installer>
@ -448,7 +452,7 @@
<span class="disco-description-main"></span>
</div>
<div class="disco-description-statistics">
<five-star-rating></five-star-rating>
<moz-five-star></moz-five-star>
<span class="disco-user-count"></span>
</div>
</template>
@ -613,7 +617,7 @@
<div class="addon-detail-row addon-detail-row-rating">
<label data-l10n-id="addon-detail-rating-label"></label>
<div class="addon-detail-rating">
<five-star-rating></five-star-rating>
<moz-five-star></moz-five-star>
<a target="_blank"></a>
</div>
</div>
@ -624,18 +628,6 @@
</named-deck>
</template>
<template name="five-star-rating">
<link
rel="stylesheet"
href="chrome://mozapps/content/extensions/rating-star.css"
/>
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
<span class="rating-star"></span>
</template>
<template name="taar-notice">
<message-bar class="discopane-notice" dismissable>
<div class="discopane-notice-content">

Просмотреть файл

@ -1573,67 +1573,6 @@ class PluginOptions extends AddonOptions {
}
customElements.define("plugin-options", PluginOptions);
class FiveStarRating extends HTMLElement {
static get observedAttributes() {
return ["rating"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.append(importTemplate("five-star-rating"));
}
set rating(v) {
this.setAttribute("rating", v);
}
get rating() {
let v = parseFloat(this.getAttribute("rating"), 10);
if (v >= 0 && v <= 5) {
return v;
}
return 0;
}
get ratingBuckets() {
// 0 <= x < 0.25 = empty
// 0.25 <= x < 0.75 = half
// 0.75 <= x <= 1 = full
// ... et cetera, until x <= 5.
let { rating } = this;
return [0, 1, 2, 3, 4].map(ratingStart => {
let distanceToFull = rating - ratingStart;
if (distanceToFull < 0.25) {
return "empty";
}
if (distanceToFull < 0.75) {
return "half";
}
return "full";
});
}
connectedCallback() {
this.renderRating();
}
attributeChangedCallback() {
this.renderRating();
}
renderRating() {
let starElements = this.shadowRoot.querySelectorAll(".rating-star");
for (let [i, part] of this.ratingBuckets.entries()) {
starElements[i].setAttribute("fill", part);
}
document.l10n.setAttributes(this, "five-star-rating", {
rating: this.rating,
});
}
}
customElements.define("five-star-rating", FiveStarRating);
class ProxyContextMenu extends HTMLElement {
openPopupAtScreen(...args) {
// prettier-ignore
@ -2349,7 +2288,7 @@ class AddonDetails extends HTMLElement {
// Rating.
let ratingRow = this.querySelector(".addon-detail-row-rating");
if (addon.averageRating) {
ratingRow.querySelector("five-star-rating").rating = addon.averageRating;
ratingRow.querySelector("moz-five-star").rating = addon.averageRating;
let reviews = ratingRow.querySelector("a");
reviews.href = formatUTMParams(
"addons-manager-reviews-link",
@ -3133,9 +3072,9 @@ class RecommendedAddonCard extends HTMLElement {
let hasStats = false;
if (addon.averageRating) {
hasStats = true;
card.querySelector("five-star-rating").rating = addon.averageRating;
card.querySelector("moz-five-star").rating = addon.averageRating;
} else {
card.querySelector("five-star-rating").hidden = true;
card.querySelector("moz-five-star").hidden = true;
}
if (addon.dailyUsers) {

Просмотреть файл

@ -15,7 +15,6 @@ toolkit.jar:
content/mozapps/extensions/abuse-report-panel.css (content/abuse-report-panel.css)
content/mozapps/extensions/abuse-report-panel.js (content/abuse-report-panel.js)
content/mozapps/extensions/drag-drop-addon-installer.js (content/drag-drop-addon-installer.js)
content/mozapps/extensions/rating-star.css (content/rating-star.css)
content/mozapps/extensions/shortcuts.css (content/shortcuts.css)
content/mozapps/extensions/shortcuts.js (content/shortcuts.js)
content/mozapps/extensions/view-controller.js (content/view-controller.js)

Просмотреть файл

@ -528,9 +528,9 @@ add_task(async function testFullDetails() {
checkLabel(row, "rating");
let rating = row.lastElementChild;
ok(rating.classList.contains("addon-detail-rating"), "Found the rating el");
let starsElem = rating.querySelector("five-star-rating");
is(starsElem.rating, 4.279, "Exact rating used for calculations");
let stars = Array.from(starsElem.shadowRoot.querySelectorAll(".rating-star"));
let mozFiveStar = rating.querySelector("moz-five-star");
is(mozFiveStar.rating, 4.279, "Exact rating used for calculations");
let stars = Array.from(mozFiveStar.starEls);
let fullAttrs = stars.map(star => star.getAttribute("fill")).join(",");
is(fullAttrs, "full,full,full,full,half", "Four and a half stars are full");
link = rating.querySelector("a");
@ -545,16 +545,21 @@ add_task(async function testFullDetails() {
// While we are here, let's test edge cases of star ratings.
async function testRating(rating, ratingRounded, expectation) {
starsElem.rating = rating;
await starsElem.ownerDocument.l10n.translateElements([starsElem]);
is(
starsElem.ratingBuckets.join(","),
expectation,
`Rendering of rating ${rating}`
);
mozFiveStar.rating = rating;
await mozFiveStar.updateComplete;
if (mozFiveStar.ownerDocument.hasPendingL10nMutations) {
await BrowserTestUtils.waitForEvent(
mozFiveStar.ownerDocument,
"L10nMutationsFinished"
);
}
let starsString = Array.from(mozFiveStar.starEls)
.map(star => star.getAttribute("fill"))
.join(",");
is(starsString, expectation, `Rendering of rating ${rating}`);
is(
starsElem.title,
mozFiveStar.starsWrapperEl.title,
`Rated ${ratingRounded} out of 5`,
"Rendered title must contain at most one fractional digit"
);

Просмотреть файл

@ -310,12 +310,12 @@ add_task(async function discopane_with_real_api_data() {
);
checkContent(".disco-description-main", expectations.editorialBody);
let ratingElem = card.querySelector("five-star-rating");
let mozFiveStar = card.querySelector("moz-five-star");
if (expectations.rating) {
is(ratingElem.rating, expectations.rating, "Expected rating value");
ok(ratingElem.offsetWidth, "Rating element is visible");
is(mozFiveStar.rating, expectations.rating, "Expected rating value");
ok(mozFiveStar.offsetWidth, "Rating element is visible");
} else {
is(ratingElem.offsetWidth, 0, "Rating element is not visible");
is(mozFiveStar.offsetWidth, 0, "Rating element is not visible");
}
let userCountElem = card.querySelector(".disco-user-count");

Просмотреть файл

@ -99,6 +99,7 @@
skin/classic/global/icons/print.svg (../../shared/icons/print.svg)
skin/classic/global/icons/undo.svg (../../shared/icons/undo.svg)
skin/classic/global/icons/radio.svg (../../shared/icons/radio.svg)
skin/classic/global/icons/rating-star.svg (../../shared/icons/rating-star.svg)
skin/classic/global/icons/reload.svg (../../shared/icons/reload.svg)
skin/classic/global/icons/search-glass.svg (../../shared/icons/search-glass.svg)
skin/classic/global/icons/security.svg (../../shared/icons/security.svg)

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 2.4 KiB

После

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Просмотреть файл

@ -19,9 +19,6 @@
skin/classic/mozapps/extensions/extension.svg (../../shared/extensions/extension.svg)
skin/classic/mozapps/extensions/recommended.svg (../../shared/extensions/recommended.svg)
skin/classic/mozapps/extensions/line.svg (../../shared/extensions/line.svg)
#ifndef ANDROID
skin/classic/mozapps/extensions/rating-star.svg (../../shared/extensions/rating-star.svg)
#endif
#ifndef ANDROID
skin/classic/mozapps/aboutProfiles.css (../../shared/aboutProfiles.css)
#endif