summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/content/components/menu-button.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutlogins/content/components/menu-button.mjs')
-rw-r--r--browser/components/aboutlogins/content/components/menu-button.mjs183
1 files changed, 183 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/content/components/menu-button.mjs b/browser/components/aboutlogins/content/components/menu-button.mjs
new file mode 100644
index 0000000000..bb69b711c9
--- /dev/null
+++ b/browser/components/aboutlogins/content/components/menu-button.mjs
@@ -0,0 +1,183 @@
+/* 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/. */
+
+export default class MenuButton extends HTMLElement {
+ connectedCallback() {
+ if (this.shadowRoot) {
+ return;
+ }
+
+ let MenuButtonTemplate = document.querySelector("#menu-button-template");
+ let shadowRoot = this.attachShadow({ mode: "open" });
+ document.l10n.connectRoot(shadowRoot);
+ shadowRoot.appendChild(MenuButtonTemplate.content.cloneNode(true));
+
+ for (let menuitem of this.shadowRoot.querySelectorAll(
+ ".menuitem-button[data-supported-platforms]"
+ )) {
+ let supportedPlatforms = menuitem.dataset.supportedPlatforms
+ .split(",")
+ .map(platform => platform.trim());
+ if (supportedPlatforms.includes(navigator.platform)) {
+ menuitem.hidden = false;
+ }
+ }
+
+ this._menu = this.shadowRoot.querySelector(".menu");
+ this._menuButton = this.shadowRoot.querySelector(".menu-button");
+
+ this._menuButton.addEventListener("click", this);
+ document.addEventListener("keydown", this, true);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "blur": {
+ if (event.explicitOriginalTarget) {
+ let node = event.explicitOriginalTarget;
+ if (node.nodeType == Node.TEXT_NODE) {
+ node = node.parentElement;
+ }
+ if (node.closest(".menu") == this._menu) {
+ // Only hide the menu if focus has left the menu-button.
+ return;
+ }
+ }
+ this._hideMenu();
+ break;
+ }
+ case "click": {
+ // Skip the catch-all event listener if it was the menu-button
+ // that was clicked on.
+ if (
+ event.currentTarget == document.documentElement &&
+ event.target == this &&
+ event.originalTarget == this._menuButton
+ ) {
+ return;
+ }
+
+ if (event.originalTarget == this._menuButton) {
+ this._toggleMenu();
+ if (!this._menu.hidden) {
+ this._menuButton.focus();
+ }
+ return;
+ }
+
+ let classList = event.originalTarget.classList;
+ if (classList.contains("menuitem-button")) {
+ let eventName = event.originalTarget.dataset.eventName;
+ const linkTrackingSource = "Elipsis_Menu";
+ document.dispatchEvent(
+ new CustomEvent(eventName, {
+ bubbles: true,
+ detail: linkTrackingSource,
+ })
+ );
+
+ // Bug 1645365: Only hide the menu when the buttons are clicked
+ // So that the menu isn't closed when non-buttons (e.g. separators, paddings) are clicked
+ this._hideMenu();
+ }
+
+ // Explicitly close menu at the catch-all click event (i.e. a click outside of the menu)
+ if (
+ !this._menu.contains(event.originalTarget) &&
+ !this._menuButton.contains(event.originalTarget)
+ ) {
+ this._hideMenu();
+ }
+
+ break;
+ }
+ case "keydown": {
+ this._handleKeyDown(event);
+ }
+ }
+ }
+
+ _handleKeyDown(event) {
+ if (event.key == "Enter" && event.originalTarget == this._menuButton) {
+ event.preventDefault();
+ this._toggleMenu();
+ this._focusSuccessor(true);
+ } else if (event.key == "Escape" && !this._menu.hidden) {
+ this._hideMenu();
+ this._menuButton.focus();
+ } else if (
+ (event.key == "ArrowDown" || event.key == "ArrowUp") &&
+ !this._menu.hidden
+ ) {
+ event.preventDefault();
+ this._focusSuccessor(event.key == "ArrowDown");
+ }
+ }
+
+ _focusSuccessor(next = true) {
+ let items = this._menu.querySelectorAll(".menuitem-button:not([hidden])");
+ let firstItem = items[0];
+ let lastItem = items[items.length - 1];
+
+ let activeItem = this.shadowRoot.activeElement;
+ let activeItemIndex = [...items].indexOf(activeItem);
+
+ let successor = null;
+
+ if (next) {
+ if (!activeItem || activeItem === lastItem) {
+ successor = firstItem;
+ } else {
+ successor = items[activeItemIndex + 1];
+ }
+ } else if (activeItem === this._menuButton || activeItem === firstItem) {
+ successor = lastItem;
+ } else {
+ successor = items[activeItemIndex - 1];
+ }
+
+ if (this._menu.hidden) {
+ this._showMenu();
+ }
+ if (successor.disabled) {
+ if (next) {
+ successor = items[activeItemIndex + 2];
+ } else {
+ successor = items[activeItemIndex - 2];
+ }
+ }
+ window.AboutLoginsUtils.setFocus(successor);
+ }
+
+ _hideMenu() {
+ this._menu.hidden = true;
+
+ this.removeEventListener("blur", this);
+ document.documentElement.removeEventListener("click", this, true);
+ }
+
+ _showMenu() {
+ this._menu.querySelector(".menuitem-import-file").hidden =
+ !window.AboutLoginsUtils.fileImportEnabled;
+
+ this._menu.hidden = false;
+
+ // Event listeners to close the menu
+ this.addEventListener("blur", this);
+ document.documentElement.addEventListener("click", this, true);
+ }
+
+ /**
+ * Toggles the visibility of the menu.
+ */
+ _toggleMenu() {
+ let wasHidden = this._menu.hidden;
+ if (wasHidden) {
+ this._showMenu();
+ } else {
+ this._hideMenu();
+ }
+ }
+}
+customElements.define("menu-button", MenuButton);