diff options
Diffstat (limited to 'toolkit/content/widgets/moz-toggle')
-rw-r--r-- | toolkit/content/widgets/moz-toggle/README.stories.md | 80 | ||||
-rw-r--r-- | toolkit/content/widgets/moz-toggle/moz-toggle.css | 198 | ||||
-rw-r--r-- | toolkit/content/widgets/moz-toggle/moz-toggle.mjs | 133 | ||||
-rw-r--r-- | toolkit/content/widgets/moz-toggle/moz-toggle.stories.mjs | 96 |
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, +}; |