Source code

Revision control

Copy as Markdown

Other Tools

/* 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, classMap } 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";
/**
* Controls moz-button behavior when menuId property is set.
* Helps to integrate moz-button with panel-list.
*/
class MenuController {
/** @type {HTMLElement} */
host;
/** @type {string} */
#menuId;
/** @type {HTMLElement | null} */
#menuEl;
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.hostUpdated();
}
hostDisconnected() {
this.#menuId = null;
this.#menuEl = null;
this.host.removeEventListener("click", this.openPanelList);
this.host.removeEventListener("mousedown", this.openPanelList);
}
hostUpdated() {
if (this.#menuId === this.host.menuId) {
return;
}
if (this.#menuEl?.localName == "panel-list") {
this.panelListCleanUp();
}
this.#menuId = this.host.menuId;
// Check to see if a menuId has been added to host, or changed
if (this.#menuId) {
this.#menuEl = this.host.getRootNode().querySelector(`#${this.#menuId}`);
if (this.#menuEl?.localName == "panel-list") {
this.panelListSetUp();
}
}
// Check to see if menuId has been removed from host
if (!this.#menuId) {
this.#menuEl = null;
this.host.removeController(this);
}
}
/**
* Handles opening/closing the panel-list when the host is clicked or activated via keyboard.
* @param {MouseEvent|KeyboardEvent} event
*/
openPanelList = event => {
if (
(event.type == "mousedown" && event.button == 0) ||
event.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD ||
!event.detail
) {
this.#menuEl?.toggle(event, this.host);
}
};
/**
* Sets up the host for integration with panel-list,
* adding necessary event listeners and ARIA attributes.
*/
panelListSetUp() {
this.host.addEventListener("click", this.openPanelList);
this.host.addEventListener("mousedown", this.openPanelList);
this.host.ariaHasPopup = "menu";
this.host.ariaExpanded = this.#menuEl?.open ? "true" : "false";
}
/**
* Cleans up panel-list integration,
* removing event listeners and clearing ARIA attributes.
*/
panelListCleanUp() {
this.host.removeEventListener("click", this.openPanelList);
this.host.removeEventListener("mousedown", this.openPanelList);
this.host.ariaHasPopup = null;
this.host.ariaExpanded = null;
}
}
/**
* A button with multiple types and two sizes.
*
* @tagname moz-button
* @property {string} label - The button's label, will be overridden by slotted content.
* @property {string} type - The button type.
* Options: default, primary, destructive, icon, icon ghost, ghost.
* @property {string} size - The button size.
* Options: default, small.
* @property {boolean} disabled - The disabled state.
* @property {string} title - The button's title attribute, used in shadow DOM and therefore not as an attribute on moz-button.
* @property {string} titleAttribute - Internal, map title attribute to the title JS property.
* @property {string} tooltipText - Set the title property, the title attribute will be used first.
* @property {string} ariaLabel - The button's aria-label attribute, used in shadow DOM and therefore not as an attribute on moz-button.
* @property {string} ariaHasPopup - The button's aria-haspopup attribute, that indicates that a popup element can be triggered by the button.
* @property {string} ariaExpanded - The button's aria-expanded attribute, that indicates whether or not the controlled elements are displayed or hidden.
* @property {string} iconSrc - Path to the icon that should be displayed in the button.
* @property {string} ariaLabelAttribute - Internal, map aria-label attribute to the ariaLabel JS property.
* @property {string} ariaHasPopupAttribute - Internal, map aria-haspopup attribute to the ariaHasPopup JS property.
* @property {string} ariaExpandedAttribute - Internal, map aria-expanded attribute to the ariaExpanded JS property.
* @property {string} hasVisibleLabel - Internal, tracks whether or not the button has a visible label.
* @property {boolean} attention - Show a dot notification on the button if true.
* @property {boolean} parentDisabled - When the parent of this component is disabled.
* @property {string} iconPosition - The icon's position relative to the button label.
* Options: start, end.
* @property {string} menuId - A CSS selector string that identifies the associated menu element controlled by the button.
* @property {HTMLButtonElement} buttonEl - The internal button element in the shadow DOM.
* @property {HTMLButtonElement} slotEl - The internal slot element in the shadow DOM.
* @cssproperty [--button-outer-padding-inline] - Used to set the outer inline padding of toolbar style buttons
* @csspropert [--button-outer-padding-block] - Used to set the outer block padding of toolbar style buttons.
* @cssproperty [--button-outer-padding-inline-start] - Used to set the outer inline-start padding of toolbar style buttons
* @cssproperty [--button-outer-padding-inline-end] - Used to set the outer inline-end padding of toolbar style buttons
* @cssproperty [--button-outer-padding-block-start] - Used to set the outer block-start padding of toolbar style buttons
* @cssproperty [--button-outer-padding-block-end] - Used to set the outer block-end padding of toolbar style buttons
* @slot default - The button's content, overrides label property.
* @fires click - The click event.
*/
export default class MozButton extends MozLitElement {
static shadowRootOptions = {
...MozLitElement.shadowRootOptions,
delegatesFocus: true,
};
static properties = {
label: { type: String, reflect: true, fluent: true },
type: { type: String, reflect: true },
size: { type: String, reflect: true },
disabled: { type: Boolean, reflect: true },
title: { type: String, mapped: true },
tooltipText: { type: String, fluent: true },
ariaLabel: { type: String, mapped: true },
ariaHasPopup: { type: String, mapped: true },
ariaExpanded: { type: String, mapped: true },
iconSrc: { type: String },
hasVisibleLabel: { type: Boolean, state: true },
accessKey: { type: String, mapped: true },
attention: { type: Boolean },
iconPosition: { type: String, reflect: true },
menuId: { type: String, reflect: true },
parentDisabled: { type: Boolean },
};
static queries = {
buttonEl: "button",
slotEl: "slot",
backgroundEl: ".button-background",
};
constructor() {
super();
this.type = "default";
this.size = "default";
this.disabled = false;
this.hasVisibleLabel = !!this.label;
this.attention = false;
this.iconPosition = "start";
this.menuId = "";
this.parentDisabled = undefined;
}
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
if (changedProperties.has("menuId")) {
if (this.menuId && !this._menuController) {
this._menuController = new MenuController(this);
}
if (!this.menuId && this._menuController) {
this._menuController = null;
}
}
}
// Delegate clicks on host to the button element.
click() {
this.buttonEl.click();
}
checkForLabelText() {
this.hasVisibleLabel = this.slotEl
?.assignedNodes()
.some(node => node.textContent.trim());
}
labelTemplate() {
if (this.label) {
return this.label;
}
return html`<slot @slotchange=${this.checkForLabelText}></slot>`;
}
iconTemplate(position) {
if (this.iconSrc && position == this.iconPosition) {
return html`<img src=${this.iconSrc} role="presentation" />`;
}
return null;
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://global/content/elements/moz-button.css"
/>
<button
?disabled=${this.disabled || this.parentDisabled}
title=${ifDefined(this.title || this.tooltipText)}
aria-label=${ifDefined(this.ariaLabel)}
aria-expanded=${ifDefined(this.ariaExpanded)}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
accesskey=${ifDefined(this.accessKey)}
>
<span
class=${classMap({
labelled: this.label || this.hasVisibleLabel,
"button-background": true,
badged:
(this.iconSrc || this.type.includes("icon")) && this.attention,
})}
part="button"
type=${this.type}
size=${this.size}
>
${this.iconTemplate("start")}
<label
is="moz-label"
shownaccesskey=${ifDefined(this.accessKey)}
part="moz-button-label"
>
${this.labelTemplate()}
</label>
${this.iconTemplate("end")}
</span>
</button>
`;
}
}
customElements.define("moz-button", MozButton);