summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/moz-toggle
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/moz-toggle')
-rw-r--r--toolkit/content/widgets/moz-toggle/README.stories.md80
-rw-r--r--toolkit/content/widgets/moz-toggle/moz-toggle.css198
-rw-r--r--toolkit/content/widgets/moz-toggle/moz-toggle.mjs133
-rw-r--r--toolkit/content/widgets/moz-toggle/moz-toggle.stories.mjs96
4 files changed, 507 insertions, 0 deletions
diff --git a/toolkit/content/widgets/moz-toggle/README.stories.md b/toolkit/content/widgets/moz-toggle/README.stories.md
new file mode 100644
index 0000000000..8ab289fd92
--- /dev/null
+++ b/toolkit/content/widgets/moz-toggle/README.stories.md
@@ -0,0 +1,80 @@
+# MozToggle
+
+`moz-toggle` is a toggle element that can be used to switch between two states.
+It may be helpful to think of it as a button that can be pressed or unpressed,
+corresponding with "on" and "off" states.
+
+```html story
+<moz-toggle pressed
+ label="Toggle label"
+ description="This is a demo toggle for the docs.">
+</moz-toggle>
+```
+
+## When to use
+
+* Use a toggle for binary controls like on/off or enabled/disabled.
+* Use when the action is performed immediately and doesn't require confirmation
+ or form submission.
+* A toggle is like a switch. If it would be appropriate to use a switch in the
+ physical world for this action, it is likely appropriate to use a toggle in
+ software.
+
+## When not to use
+
+* If another action is required to execute the choice, use a checkbox (i.e. a
+ toggle should not generally be used as part of a form).
+
+## Code
+
+The source for `moz-toggle` can be found under
+[toolkit/content/widgets/moz-toggle](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/moz-toggle/moz-toggle.mjs).
+You can find an examples of `moz-toggle` in use in the Firefox codebase in both
+[about:preferences](https://searchfox.org/mozilla-central/source/browser/components/preferences/privacy.inc.xhtml#696)
+and [about:addons](https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/content/aboutaddons.html#182).
+
+`moz-toggle` can be imported into `.html`/`.xhtml` files:
+
+```html
+<script type="module" src="chrome://global/content/elements/moz-toggle.mjs"></script>
+```
+
+And used as follows:
+
+```html
+<moz-toggle pressed
+ label="Label for the toggle"
+ description="Longer explanation of what the toggle is for"
+ aria-label="Toggle label if label text isn't visible"></moz-toggle>
+```
+
+### Fluent usage
+
+Generally the `label`, `description`, and `aria-label` properties of
+`moz-toggle` will be provided via [Fluent attributes](https://mozilla-l10n.github.io/localizer-documentation/tools/fluent/basic_syntax.html#attributes).
+To get this working you will need to specify a `data-l10n-id` as well as
+`data-l10n-attrs` if you're providing a label and a description:
+
+```html
+<moz-toggle data-l10n-id="with-label-and-description"
+ data-l10n-attrs="label, description"></moz-toggle>
+```
+
+In which case your Fluent messages will look something like this:
+
+```
+with-label-and-description =
+ .label = Label text goes here
+ .description = Description text goes here
+```
+
+You do not have to specify `data-l10n-attrs` if you're only using an `aria-label`:
+
+```html
+<moz-toggle data-l10n-id="with-aria-label-only"></moz-toggle>
+```
+
+```
+with-aria-label-only =
+ .aria-label = aria-label text goes here
+```
diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.css b/toolkit/content/widgets/moz-toggle/moz-toggle.css
new file mode 100644
index 0000000000..8b67a81878
--- /dev/null
+++ b/toolkit/content/widgets/moz-toggle/moz-toggle.css
@@ -0,0 +1,198 @@
+/* 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/design-system/text-and-typography.css");
+
+:host {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+:host([disabled]) {
+ opacity: 0.4
+}
+
+::slotted(a[is="moz-support-link"]) {
+ display: inline-block;
+}
+
+#moz-toggle-label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+}
+
+.description-wrapper,
+.description-wrapper ::slotted([slot="support-link"]) {
+ margin: 0;
+}
+
+.toggle-button {
+ --toggle-background-color: var(--button-background-color);
+ --toggle-background-color-hover: var(--button-background-color-hover);
+ --toggle-background-color-active: var(--button-background-color-active);
+ --toggle-background-color-pressed: var(--color-accent-primary);
+ --toggle-background-color-pressed-hover: var(--color-accent-primary-hover);
+ --toggle-background-color-pressed-active: var(--color-accent-primary-active);
+ --toggle-border-color: var(--border-interactive-color);
+ --toggle-border-radius: var(--border-radius-circle);
+ --toggle-border-width: var(--border-width);
+ --toggle-height: var(--size-item-small);
+ --toggle-width: var(--size-item-large);
+ --toggle-dot-background-color: var(--toggle-border-color);
+ --toggle-dot-background-color-on-pressed: var(--color-canvas);
+ --toggle-dot-margin: 1px;
+ --toggle-dot-height: calc(var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width));
+ --toggle-dot-width: var(--toggle-dot-height);
+ --toggle-dot-transform-x: calc(var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width));
+}
+
+.toggle-button {
+ appearance: none;
+ padding: 0;
+ margin: 0;
+ border: var(--toggle-border-width) solid var(--toggle-border-color);
+ height: var(--toggle-height);
+ width: var(--toggle-width);
+ border-radius: var(--toggle-border-radius);
+ background: var(--toggle-background-color);
+ box-sizing: border-box;
+ flex-shrink: 0;
+}
+
+.toggle-button:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: var(--focus-outline-offset);
+}
+
+.toggle-button:enabled:hover {
+ background: var(--toggle-background-color-hover);
+ border-color: var(--toggle-border-color);
+}
+
+.toggle-button:enabled:active {
+ background: var(--toggle-background-color-active);
+ border-color: var(--toggle-border-color);
+}
+
+.toggle-button[aria-pressed="true"] {
+ background: var(--toggle-background-color-pressed);
+ border-color: transparent;
+}
+
+.toggle-button[aria-pressed="true"]:enabled:hover {
+ background: var(--toggle-background-color-pressed-hover);
+ border-color: transparent;
+}
+
+.toggle-button[aria-pressed="true"]:enabled:active {
+ background: var(--toggle-background-color-pressed-active);
+ border-color: transparent;
+}
+
+.toggle-button::before {
+ display: block;
+ content: "";
+ background-color: var(--toggle-dot-background-color);
+ height: var(--toggle-dot-height);
+ width: var(--toggle-dot-width);
+ margin: var(--toggle-dot-margin);
+ border-radius: var(--toggle-border-radius);
+ translate: 0;
+}
+
+.toggle-button[aria-pressed="true"]::before {
+ translate: var(--toggle-dot-transform-x);
+ background-color: var(--toggle-dot-background-color-on-pressed);
+}
+
+.toggle-button[aria-pressed="true"]:enabled:hover::before,
+.toggle-button[aria-pressed="true"]:enabled:active::before {
+ background-color: var(--toggle-dot-background-color-on-pressed);
+}
+
+.toggle-button[aria-pressed="true"]:-moz-locale-dir(rtl)::before,
+.toggle-button[aria-pressed="true"]:dir(rtl)::before {
+ translate: calc(-1 * var(--toggle-dot-transform-x));
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .toggle-button::before {
+ transition: translate 100ms;
+ }
+}
+
+@media (prefers-contrast) {
+ :host([disabled]) {
+ opacity: 1;
+ }
+
+ :host([disabled]) > .toggle-button[aria-pressed="true"],
+ :host([disabled]) > .toggle-button {
+ background-color: var(--toggle-background-color-disabled);
+ border-color: var(--toggle-border-color-disabled);
+ }
+
+ :host([disabled]) > .toggle-button[aria-pressed="false"]::before,
+ :host([disabled]) > .toggle-button[aria-pressed="true"]::before {
+ background-color: var(--toggle-background-color-disabled);
+ }
+
+ .toggle-button {
+ --toggle-dot-background-color: var(--color-accent-primary);
+ --toggle-dot-background-color-hover: var(--color-accent-primary-hover);
+ --toggle-dot-background-color-active: var(--color-accent-primary-active);
+ --toggle-dot-background-color-on-pressed: var(--button-background-color);
+ --toggle-background-color-disabled: var(--button-background-color-disabled);
+ --toggle-border-color-hover: var(--border-interactive-color-hover);
+ --toggle-border-color-active: var(--border-interactive-color-active);
+ --toggle-border-color-disabled: var(--border-interactive-color-disabled);
+ }
+
+ .toggle-button:enabled:hover {
+ border-color: var(--toggle-border-color-hover);
+ }
+
+ .toggle-button:enabled:active {
+ border-color: var(--toggle-border-color-active);
+ }
+
+ .toggle-button[aria-pressed="true"]:enabled {
+ border-color: var(--toggle-border-color);
+ position: relative;
+ }
+
+ .toggle-button[aria-pressed="true"]:enabled:hover,
+ .toggle-button[aria-pressed="true"]:enabled:hover:active {
+ border-color: var(--toggle-border-color-hover);
+ }
+
+ .toggle-button[aria-pressed="true"]:enabled:active {
+ background-color: var(--toggle-dot-background-color-active);
+ border-color: var(--toggle-dot-background-color-hover);
+ }
+
+ .toggle-button[aria-pressed="true"]:enabled::after {
+ border: 1px solid var(--button-background-color);
+ content: '';
+ position: absolute;
+ height: var(--toggle-height);
+ width: var(--toggle-width);
+ display: block;
+ border-radius: var(--toggle-border-radius);
+ inset: -2px;
+ }
+
+ .toggle-button[aria-pressed="true"]:enabled:active::after {
+ border-color: var(--toggle-border-color-active);
+ }
+
+ .toggle-button:hover::before,
+ .toggle-button:hover:active::before,
+ .toggle-button:active::before {
+ background-color: var(--toggle-dot-background-color-hover);
+ }
+}
diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.mjs b/toolkit/content/widgets/moz-toggle/moz-toggle.mjs
new file mode 100644
index 0000000000..be7ee98f34
--- /dev/null
+++ b/toolkit/content/widgets/moz-toggle/moz-toggle.mjs
@@ -0,0 +1,133 @@
+/* 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 htp://mozilla.org/MPL/2.0/. */
+
+import { html, ifDefined } from "../vendor/lit.all.mjs";
+import { MozLitElement } from "../lit-utils.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-label.mjs";
+
+/**
+ * A simple toggle element that can be used to switch between two states.
+ *
+ * @tagname moz-toggle
+ * @property {boolean} pressed - Whether or not the element is pressed.
+ * @property {boolean} disabled - Whether or not the element is disabled.
+ * @property {string} label - The label text.
+ * @property {string} description - The description text.
+ * @property {string} ariaLabel
+ * The aria-label text for cases where there is no visible label.
+ * @slot support-link - Used to append a moz-support-link to the description.
+ * @fires toggle
+ * Custom event indicating that the toggle's pressed state has changed.
+ */
+export default class MozToggle extends MozLitElement {
+ static shadowRootOptions = {
+ ...MozLitElement.shadowRootOptions,
+ delegatesFocus: true,
+ };
+
+ static properties = {
+ pressed: { type: Boolean, reflect: true },
+ disabled: { type: Boolean, reflect: true },
+ label: { type: String },
+ description: { type: String },
+ ariaLabel: { type: String, attribute: "aria-label" },
+ accessKey: { type: String, attribute: "accesskey" },
+ };
+
+ static get queries() {
+ return {
+ buttonEl: "#moz-toggle-button",
+ labelEl: "#moz-toggle-label",
+ descriptionEl: "#moz-toggle-description",
+ };
+ }
+
+ constructor() {
+ super();
+ this.pressed = false;
+ this.disabled = false;
+ }
+
+ handleClick() {
+ this.pressed = !this.pressed;
+ this.dispatchOnUpdateComplete(
+ new CustomEvent("toggle", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ // Delegate clicks on the host to the input element
+ click() {
+ this.buttonEl.click();
+ }
+
+ descriptionTemplate() {
+ if (this.description) {
+ return html`
+ <p
+ id="moz-toggle-description"
+ class="description-wrapper text-deemphasized"
+ part="description"
+ >
+ ${this.description} ${this.supportLinkTemplate()}
+ </p>
+ `;
+ }
+ return "";
+ }
+
+ supportLinkTemplate() {
+ return html` <slot name="support-link"></slot> `;
+ }
+
+ buttonTemplate() {
+ const { pressed, disabled, description, ariaLabel, handleClick } = this;
+ return html`
+ <button
+ id="moz-toggle-button"
+ part="button"
+ type="button"
+ class="toggle-button"
+ ?disabled=${disabled}
+ aria-pressed=${pressed}
+ aria-label=${ifDefined(ariaLabel ?? undefined)}
+ aria-describedby=${ifDefined(
+ description ? "moz-toggle-description" : undefined
+ )}
+ @click=${handleClick}
+ ></button>
+ `;
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/elements/moz-toggle.css"
+ />
+ ${this.label
+ ? html`
+ <label
+ is="moz-label"
+ id="moz-toggle-label"
+ part="label"
+ for="moz-toggle-button"
+ accesskey=${ifDefined(this.accessKey)}
+ >
+ <span>
+ ${this.label}
+ ${!this.description ? this.supportLinkTemplate() : ""}
+ </span>
+ ${this.buttonTemplate()}
+ </label>
+ `
+ : this.buttonTemplate()}
+ ${this.descriptionTemplate()}
+ `;
+ }
+}
+customElements.define("moz-toggle", MozToggle);
diff --git a/toolkit/content/widgets/moz-toggle/moz-toggle.stories.mjs b/toolkit/content/widgets/moz-toggle/moz-toggle.stories.mjs
new file mode 100644
index 0000000000..fa41e7c888
--- /dev/null
+++ b/toolkit/content/widgets/moz-toggle/moz-toggle.stories.mjs
@@ -0,0 +1,96 @@
+/* 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 import/no-unassigned-import */
+
+import { html, ifDefined } from "../vendor/lit.all.mjs";
+import "./moz-toggle.mjs";
+import "../moz-support-link/moz-support-link.mjs";
+
+export default {
+ title: "UI Widgets/Toggle",
+ component: "moz-toggle",
+ parameters: {
+ status: "in-development",
+ actions: {
+ handles: ["toggle"],
+ },
+ fluent: `
+moz-toggle-aria-label =
+ .aria-label = This is the aria-label
+moz-toggle-label =
+ .label = This is the label
+moz-toggle-description =
+ .label = This is the label
+ .description = This is the description.
+ `,
+ },
+};
+
+const Template = ({
+ pressed,
+ disabled,
+ label,
+ description,
+ ariaLabel,
+ l10nId,
+ hasSupportLink,
+ accessKey,
+}) => html`
+ <div style="max-width: 400px">
+ <moz-toggle
+ ?pressed=${pressed}
+ ?disabled=${disabled}
+ label=${ifDefined(label)}
+ description=${ifDefined(description)}
+ aria-label=${ifDefined(ariaLabel)}
+ data-l10n-id=${ifDefined(l10nId)}
+ data-l10n-attrs="aria-label, description, label"
+ accesskey=${ifDefined(accessKey)}
+ >
+ ${hasSupportLink
+ ? html`
+ <a
+ is="moz-support-link"
+ support-page="addons"
+ slot="support-link"
+ ></a>
+ `
+ : ""}
+ </moz-toggle>
+ </div>
+`;
+
+export const Toggle = Template.bind({});
+Toggle.args = {
+ pressed: true,
+ disabled: false,
+ l10nId: "moz-toggle-aria-label",
+};
+
+export const ToggleDisabled = Template.bind({});
+ToggleDisabled.args = {
+ ...Toggle.args,
+ disabled: true,
+};
+
+export const WithLabel = Template.bind({});
+WithLabel.args = {
+ pressed: true,
+ disabled: false,
+ l10nId: "moz-toggle-label",
+ hasSupportLink: false,
+ accessKey: "h",
+};
+
+export const WithDescription = Template.bind({});
+WithDescription.args = {
+ ...WithLabel.args,
+ l10nId: "moz-toggle-description",
+};
+
+export const WithSupportLink = Template.bind({});
+WithSupportLink.args = {
+ ...WithDescription.args,
+ hasSupportLink: true,
+};