summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/moz-card
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/content/widgets/moz-card
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/content/widgets/moz-card')
-rw-r--r--toolkit/content/widgets/moz-card/moz-card.css180
-rw-r--r--toolkit/content/widgets/moz-card/moz-card.mjs112
-rw-r--r--toolkit/content/widgets/moz-card/moz-card.stories.mjs88
3 files changed, 380 insertions, 0 deletions
diff --git a/toolkit/content/widgets/moz-card/moz-card.css b/toolkit/content/widgets/moz-card/moz-card.css
new file mode 100644
index 0000000000..52c0ac0980
--- /dev/null
+++ b/toolkit/content/widgets/moz-card/moz-card.css
@@ -0,0 +1,180 @@
+/* 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 {
+ --card-border-color: color-mix(in srgb, currentColor 10%, transparent);
+ --card-border-radius: var(--border-radius-medium);
+ --card-border-width: var(--border-width);
+ --card-border: var(--card-border-width) solid var(--card-border-color);
+ --card-background-color: var(--box-background-color);
+ --card-focus-outline: var(--focus-outline);
+ --card-box-shadow: var(--box-shadow-10);
+ /* Bug 1791816, 1839523: replace with spacing tokens */
+ --card-padding: 1em;
+ --card-gap: var(--card-padding);
+ --card-article-gap: 0.45em;
+
+ /* Bug 1791816: replace with button tokens */
+ @media (prefers-contrast) {
+ --button-border-color: var(--border-interactive-color);
+ --button-border-color-hover: var(--border-interactive-color-hover);
+ --button-border-color-active: var(--border-interactive-color-active);
+ --card-border-color: color-mix(in srgb, currentColor 41%, transparent);
+ }
+ /* Bug 1791816: replace with button tokens */
+ @media (forced-colors) {
+ --button-background-color: ButtonFace;
+ --button-background-color-hover: SelectedItemText;
+ --button-background-color-active: SelectedItemText;
+ --button-border-color: var(--border-interactive-color);
+ --button-border-color-hover: var(--border-interactive-color-hover);
+ --button-border-color-active: var(--border-interactive-color-active);
+ --button-text-color: ButtonText;
+ --button-text-color-hover: SelectedItem;
+ --button-text-color-active: SelectedItem;
+ }
+}
+
+:host {
+ display: block;
+ border: var(--card-border);
+ border-radius: var(--card-border-radius);
+ background-color: var(--card-background-color);
+ box-shadow: var(--card-box-shadow);
+ box-sizing: border-box;
+}
+
+:host([type=accordion]) {
+ summary {
+ padding-block: var(--card-padding);
+ }
+ #content {
+ padding-block-end: var(--card-padding);
+ }
+}
+:host(:not([type=accordion])) {
+ .moz-card {
+ padding-block: var(--card-padding);
+ }
+}
+
+.moz-card {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--card-article-gap);
+}
+
+#moz-card-details {
+ width: 100%;
+}
+
+summary {
+ cursor: pointer;
+}
+
+#heading-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: var(--card-gap);
+ padding-inline: var(--card-padding);
+ border-radius: var(--card-border-radius);
+}
+
+#heading {
+ font-size: var(--font-size-root);
+ font-weight: var(--font-weight-bold);
+}
+
+#content {
+ align-self: stretch;
+ padding-inline: var(--card-padding);
+ border-end-start-radius: var(--card-border-radius);
+ border-end-end-radius: var(--card-border-radius);
+
+ @media (prefers-contrast) {
+ :host([type=accordion]) & {
+ border-block-start: 0;
+ padding-block-start: var(--card-padding);
+ }
+ }
+}
+
+details {
+ > summary {
+ list-style: none;
+ border-radius: var(--card-border-radius);
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--button-background-color-hover);
+ }
+ @media (prefers-contrast) {
+ outline: var(--button-border-color) solid var(--border-width);
+
+ &:hover {
+ outline-color: var(--button-border-color-hover);
+ }
+
+ &:active {
+ outline-color: var(--button-border-color-active);
+ }
+ }
+
+ @media (forced-colors) {
+ color: var(--button-text-color);
+ background-color: var(--button-background-color);
+
+ &:hover {
+ background-color: var(--button-background-color-hover);
+ color: var(--button-text-color-hover);
+ }
+
+ &:active {
+ background-color: var(--button-background-color-active);
+ color: var(--button-text-color-active);
+ }
+ }
+ }
+
+ &[open] {
+ summary {
+ border-end-start-radius: 0;
+ border-end-end-radius: 0;
+ }
+ @media not (prefers-contrast) {
+ #content {
+ /*
+ There is a border shown above this element in prefers-contrast.
+ When there isn't a border, there's no need for the extra space.
+ */
+ padding-block-start: 0;
+ }
+ }
+ }
+
+ &:focus-visible {
+ outline: var(--card-focus-outline);
+ }
+}
+
+.chevron-icon {
+ background-image: url("chrome://global/skin/icons/arrow-down.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ width: 24px;
+ height: 24px;
+ min-width: 24px;
+ min-height: 24px;
+ padding: 0;
+ flex-shrink: 0;
+ align-self: flex-start;
+
+ details[open] & {
+ background-image: url("chrome://global/skin/icons/arrow-up.svg");
+ }
+}
diff --git a/toolkit/content/widgets/moz-card/moz-card.mjs b/toolkit/content/widgets/moz-card/moz-card.mjs
new file mode 100644
index 0000000000..2bd6e0f987
--- /dev/null
+++ b/toolkit/content/widgets/moz-card/moz-card.mjs
@@ -0,0 +1,112 @@
+/* -*- 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";
+
+/**
+ * Cards contain content and actions about a single subject.
+ * There are two 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 an arrow to expand the card so that all of the content
+ * is visible.
+ *
+ *
+ * @property {string} heading - The heading text that will be used for the card.
+ * @property {string} type - (optional) The type of card. No type specified
+ * will be the default card. The other available type is "accordion"
+ * @slot content - The content to show inside of the card.
+ */
+export default class MozCard extends MozLitElement {
+ static queries = {
+ detailsEl: "#moz-card-details",
+ headingEl: "#heading",
+ contentSlotEl: "#content",
+ };
+
+ static properties = {
+ heading: { type: String },
+ type: { type: String, reflect: true },
+ expanded: { type: Boolean },
+ };
+
+ constructor() {
+ super();
+ this.expanded = false;
+ this.type = "default";
+ }
+
+ headingTemplate() {
+ if (!this.heading) {
+ return "";
+ }
+ return html`
+ <div id="heading-wrapper">
+ ${this.type == "accordion"
+ ? html` <div class="chevron-icon"></div>`
+ : ""}
+ <span id="heading">${this.heading}</span>
+ </div>
+ `;
+ }
+
+ cardTemplate() {
+ if (this.type === "accordion") {
+ return html`
+ <details id="moz-card-details">
+ <summary>${this.headingTemplate()}</summary>
+ <div id="content"><slot></slot></div>
+ </details>
+ `;
+ }
+
+ return html`
+ ${this.headingTemplate()}
+ <div id="content" aria-describedby="content">
+ <slot></slot>
+ </div>
+ `;
+ }
+ /**
+ * Handles the click event on the chevron icon.
+ *
+ * Without this, the click event would be passed to
+ * toggleDetails which would force the details element
+ * to stay open.
+ *
+ * @memberof MozCard
+ */
+ onDetailsClick() {
+ this.toggleDetails();
+ }
+
+ /**
+ * @param {boolean} force - Used to force open or force close the
+ * details element.
+ * @memberof MozCard
+ */
+ toggleDetails(force) {
+ this.detailsEl.open = force ?? !this.detailsEl.open;
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/elements/moz-card.css"
+ />
+ <article
+ class="moz-card"
+ aria-labelledby=${ifDefined(this.heading ? "heading" : undefined)}
+ >
+ ${this.cardTemplate()}
+ </article>
+ `;
+ }
+}
+customElements.define("moz-card", MozCard);
diff --git a/toolkit/content/widgets/moz-card/moz-card.stories.mjs b/toolkit/content/widgets/moz-card/moz-card.stories.mjs
new file mode 100644
index 0000000000..da3279b2a4
--- /dev/null
+++ b/toolkit/content/widgets/moz-card/moz-card.stories.mjs
@@ -0,0 +1,88 @@
+/* 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-disable-next-line import/no-unresolved
+import { html, ifDefined } from "lit.all.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "./moz-card.mjs";
+
+export default {
+ title: "UI Widgets/Card",
+ component: "moz-card",
+ parameters: {
+ status: "in-development",
+ fluent: `
+moz-card-heading =
+ .heading = This is the label
+ `,
+ },
+ argTypes: {
+ type: {
+ options: ["default", "accordion"],
+ control: { type: "select" },
+ },
+ },
+};
+
+const Template = ({ l10nId, content, type }) => html`
+ <main style="max-width: 400px">
+ <moz-card
+ type=${ifDefined(type)}
+ data-l10n-id=${ifDefined(l10nId)}
+ data-l10n-attrs="heading"
+ >
+ <div>${content}</div>
+ </moz-card>
+ </main>
+`;
+
+export const DefaultCard = Template.bind({});
+DefaultCard.args = {
+ l10nId: "moz-card-heading",
+ content: "This is the content",
+};
+
+export const CardOnlyContent = Template.bind({});
+CardOnlyContent.args = {
+ content: "This card only contains content",
+};
+
+export const CardTypeAccordion = Template.bind({});
+CardTypeAccordion.args = {
+ ...DefaultCard.args,
+ content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Nunc velit turpis, mollis a ultricies vitae, accumsan ut augue.
+ In a eros ac dolor hendrerit varius et at mauris.`,
+ type: "accordion",
+};
+CardTypeAccordion.parameters = {
+ a11y: {
+ config: {
+ rules: [
+ /*
+ The accordion card can be expanded either by the chevron icon
+ button or by activating the details element. Mouse users can
+ click on the chevron button or the details element, while
+ keyboard users can tab to the details element and have a
+ focus ring around the details element in the card.
+ Additionally, the details element is announced as a button
+ so I don't believe we are providing a degraded experience
+ to non-mouse users.
+
+ Bug 1854008: We should probably make the accordion button a
+ clickable div or something that isn't announced to screen
+ readers.
+ */
+ {
+ id: "button-name",
+ reviewOnFail: true,
+ },
+ {
+ id: "nested-interactive",
+ reviewOnFail: true,
+ },
+ ],
+ },
+ },
+};