diff options
Diffstat (limited to 'comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs')
-rw-r--r-- | comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs b/comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs new file mode 100644 index 0000000000..466a83f0c1 --- /dev/null +++ b/comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs @@ -0,0 +1,240 @@ +/* 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/. */ + +//TODO keyboard handling, keyboard + commands + +/* import-globals-from ../../../base/content/globalOverlay.js */ + +/** + * Toolbar button implementation for the unified toolbar. + * Template ID: unifiedToolbarButtonTemplate + * Attributes: + * - command: ID string of the command to execute when the button is pressed. + * - observes: ID of command to observe for disabled state. Defaults to value of + * command attribute. + * - popup: ID of the popup to open when the button is pressed. The popup is + * anchored to the button. Overrides any other click handling. + * - disabled: When set the button is disabled. + * - title: Tooltip to show on the button. + * - label: Label text of the button. Observed for changes. + * - label-id: A fluent ID for the label instead of the label attribute. + * Observed for changes. + * - badge: When set, the value of the attribute is shown as badge. + * - aria-pressed: set to "false" to make the button behave like a toggle. + * Events: + * - buttondisabled: Fired when the button gets disabled while it is keyboard + * navigable. + * - buttonenabled: Fired when the button gets enabled again but isn't marked to + * be keyboard navigable. + */ +export class UnifiedToolbarButton extends HTMLButtonElement { + static get observedAttributes() { + return ["label", "label-id", "disabled"]; + } + + /** + * Container for the button label. + * + * @type {?HTMLSpanElement} + */ + label = null; + + /** + * Name of the command this button follows the disabled (and if it is a toggle + * button the checked) state of. + * + * @type {string?} + */ + observedCommand; + + /** + * The mutation observer observing the command this button follows the state + * of. + * + * @type {MutationObserver?} + */ + #observer = null; + + connectedCallback() { + // We remove the mutation overserver when the element is disconnected, thus + // we have to add it every time the element is connected. + this.observedCommand = + this.getAttribute("observes") || this.getAttribute("command"); + if (this.observedCommand) { + const command = document.getElementById(this.observedCommand); + if (command) { + if (!this.#observer) { + this.#observer = new MutationObserver(this.#handleCommandMutation); + } + const observedAttributes = ["disabled"]; + if (this.hasAttribute("aria-pressed")) { + observedAttributes.push("checked"); + + // Update the pressed state from the command + this.setAttribute( + "aria-pressed", + command.getAttribute("checked") ?? "false" + ); + } + this.#observer.observe(command, { + attributes: true, + attributeFilter: observedAttributes, + }); + } + // Update the disabled state to match the current state of the command. + try { + this.disabled = !getEnabledControllerForCommand(this.observedCommand); + } catch { + this.disabled = true; + } + } + if (this.hasConnected) { + return; + } + this.hasConnected = true; + this.classList.add("unified-toolbar-button", "button"); + + const template = document + .getElementById("unifiedToolbarButtonTemplate") + .content.cloneNode(true); + this.label = template.querySelector("span"); + this.#updateLabel(); + this.appendChild(template); + this.addEventListener("click", event => this.handleClick(event)); + } + + disconnectedCallback() { + if (this.#observer) { + this.#observer.disconnect(); + } + } + + attributeChangedCallback(attribute) { + switch (attribute) { + case "label": + case "label-id": + this.#updateLabel(); + break; + case "disabled": + if (!this.hasConnected) { + return; + } + if (this.disabled && this.tabIndex !== -1) { + this.tabIndex = -1; + this.dispatchEvent(new CustomEvent("buttondisabled")); + } else if (!this.disabled && this.tabIndex === -1) { + this.dispatchEvent(new CustomEvent("buttonenabled")); + } + break; + } + } + + /** + * Default handling for clicks on the button. Shows the associated popup, + * executes the given command and toggles the button state. + * + * @param {MouseEvent} event - Click event. + */ + handleClick(event) { + if (this.hasAttribute("popup")) { + event.preventDefault(); + event.stopPropagation(); + const popup = document.getElementById(this.getAttribute("popup")); + popup.openPopup(this, { + position: "after_start", + triggerEvent: event, + }); + this.setAttribute("aria-pressed", "true"); + const hideListener = () => { + if (popup.state === "open") { + return; + } + this.removeAttribute("aria-pressed"); + popup.removeEventListener("popuphiding", hideListener); + }; + popup.addEventListener("popuphiding", hideListener); + return; + } + if (this.hasAttribute("aria-pressed")) { + const isPressed = this.getAttribute("aria-pressed") === "true"; + this.setAttribute("aria-pressed", (!isPressed).toString()); + } + if (this.hasAttribute("command")) { + const command = this.getAttribute("command"); + let controller = getEnabledControllerForCommand(command); + if (controller) { + event.preventDefault(); + event.stopPropagation(); + controller = controller.wrappedJSObject ?? controller; + controller.doCommand(command, event); + return; + } + const commandElement = document.getElementById(command); + if (!commandElement) { + return; + } + event.preventDefault(); + event.stopPropagation(); + commandElement.doCommand(); + } + } + + /** + * Callback for the mutation observer on the command this button follows. + * + * @param {Mutation[]} mutationList - List of mutations the observer saw. + */ + #handleCommandMutation = mutationList => { + for (const mutation of mutationList) { + if (mutation.type !== "attributes") { + continue; + } + if (mutation.attributeName === "disabled") { + this.disabled = mutation.target.getAttribute("disabled") === "true"; + } else if (mutation.attributeName === "checked") { + this.setAttribute( + "aria-pressed", + mutation.target.getAttribute("checked") + ); + } + } + }; + + /** + * Update the contents of the label from the attributes of this element. + */ + #updateLabel() { + if (!this.label) { + return; + } + if (this.hasAttribute("label")) { + this.label.textContent = this.getAttribute("label"); + return; + } + if (this.hasAttribute("label-id")) { + document.l10n.setAttributes(this.label, this.getAttribute("label-id")); + } + } + + /** + * Badge displayed on the button. To clear the badge, set to empty string or + * nullish value. + * + * @type {string} + */ + set badge(badgeText) { + if (badgeText === "" || badgeText == null) { + this.removeAttribute("badge"); + return; + } + this.setAttribute("badge", badgeText); + } + + get badge() { + return this.getAttribute("badge"); + } +} +customElements.define("unified-toolbar-button", UnifiedToolbarButton, { + extends: "button", +}); |