Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
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`
<div id="label-wrapper">
<h2 id="header">${this.label}</h2>
<button
tabindex="-1"
class="icon chevron-icon ghost-button"
aria-labelledby="header"
@click=${this.handleChevronButtonClick}
></button>
</div>
`;
}
return html`
<div id="label-wrapper">
<h2 id="header">${this.label}</h2>
<slot name="rating"></slot>
</div>
`;
}
return "";
}
cardTemplate() {
if (this.type === "accordion") {
return html`
<details id="shopping-details" @toggle=${this.onCardToggle}>
<summary>${this.labelTemplate()}</summary>
<div id="content"><slot name="content"></slot></div>
</details>
`;
} else if (this.type === "show-more") {
return html`
${this.labelTemplate()}
<article
id="content"
class="show-more"
aria-describedby="content"
expanded="false"
>
<slot name="content"></slot>
<footer>
<button
aria-controls="content"
class="small-button shopping-button"
data-l10n-id="shopping-show-more-button"
@click=${this.handleShowMoreButtonClick}
></button>
</footer>
</article>
`;
}
return html`
${this.labelTemplate()}
<div id="content" aria-describedby="content">
<slot name="content"></slot>
</div>
`;
}
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`
<link
rel="stylesheet"
href="chrome://browser/content/shopping/shopping-card.css"
/>
<link
rel="stylesheet"
href="chrome://browser/content/shopping/shopping-page.css"
/>
<article
class="shopping-card"
aria-labelledby="header"
aria-label=${ifDefined(this.label)}
>
${this.cardTemplate()}
</article>
`;
}
}
customElements.define("shopping-card", ShoppingCard);