summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs')
-rw-r--r--comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs240
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",
+});