Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/framework/mr-dropdown/mr-dropdown.js b/static_src/elements/framework/mr-dropdown/mr-dropdown.js
new file mode 100644
index 0000000..4564ab0
--- /dev/null
+++ b/static_src/elements/framework/mr-dropdown/mr-dropdown.js
@@ -0,0 +1,367 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+import {ifDefined} from 'lit-html/directives/if-defined';
+import {SHARED_STYLES} from 'shared/shared-styles.js';
+import 'shared/typedef.js';
+
+export const SCREENREADER_ATTRIBUTE_ERROR = `For screenreader support,
+  mr-dropdown must always have either a label or a text property defined.`;
+
+/**
+ * `<mr-dropdown>`
+ *
+ * Dropdown menu for Monorail.
+ *
+ */
+export class MrDropdown extends LitElement {
+  /** @override */
+  static get styles() {
+    return [
+      SHARED_STYLES,
+      css`
+        :host {
+          position: relative;
+          display: inline-block;
+          height: 100%;
+          font-size: inherit;
+          font-family: var(--chops-font-family);
+          --mr-dropdown-icon-color: var(--chops-primary-icon-color);
+          --mr-dropdown-icon-font-size: var(--chops-icon-font-size);
+          --mr-dropdown-anchor-font-weight: var(--chops-link-font-weight);
+          --mr-dropdown-anchor-padding: 4px 0.25em;
+          --mr-dropdown-anchor-justify-content: center;
+          --mr-dropdown-menu-max-height: initial;
+          --mr-dropdown-menu-overflow: initial;
+          --mr-dropdown-menu-min-width: 120%;
+          --mr-dropdown-menu-font-size: var(--chops-large-font-size);
+          --mr-dropdown-menu-icon-size: var(--chops-icon-font-size);
+        }
+        :host([hidden]) {
+          display: none;
+          visibility: hidden;
+        }
+        :host(:not([opened])) .menu {
+          display: none;
+          visibility: hidden;
+        }
+        strong {
+          font-size: var(--chops-large-font-size);
+        }
+        i.material-icons {
+          font-size: var(--mr-dropdown-icon-font-size);
+          display: inline-block;
+          color: var(--mr-dropdown-icon-color);
+          padding: 0 2px;
+          box-sizing: border-box;
+        }
+        i.material-icons[hidden],
+        .menu-item > i.material-icons[hidden] {
+          display: none;
+        }
+        .menu-item > i.material-icons {
+          display: block;
+          font-size: var(--mr-dropdown-menu-icon-size);
+          width: var(--mr-dropdown-menu-icon-size);
+          height: var(--mr-dropdown-menu-icon-size);
+          margin-right: 8px;
+        }
+        .anchor:disabled {
+          color: var(--chops-button-disabled-color);
+        }
+        button.anchor {
+          box-sizing: border-box;
+          background: none;
+          border: none;
+          font-size: inherit;
+          width: 100%;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          justify-content: var(--mr-dropdown-anchor-justify-content);
+          cursor: pointer;
+          padding: var(--mr-dropdown-anchor-padding);
+          color: var(--chops-link-color);
+          font-weight: var(--mr-dropdown-anchor-font-weight);
+          font-family: inherit;
+        }
+        /* menuAlignment options: right, left, side. */
+        .menu.right {
+          right: 0px;
+        }
+        .menu.left {
+          left: 0px;
+        }
+        .menu.side {
+          left: 100%;
+          top: 0;
+        }
+        .menu {
+          font-size: var(--mr-dropdown-menu-font-size);
+          position: absolute;
+          min-width: var(--mr-dropdown-menu-min-width);
+          max-height: var(--mr-dropdown-menu-max-height);
+          overflow: var(--mr-dropdown-menu-overflow);
+          top: 90%;
+          display: block;
+          background: var(--chops-white);
+          border: var(--chops-accessible-border);
+          z-index: 990;
+          box-shadow: 2px 3px 8px 0px hsla(0, 0%, 0%, 0.3);
+          font-family: inherit;
+        }
+        .menu-item {
+          background: none;
+          margin: 0;
+          border: 0;
+          box-sizing: border-box;
+          text-decoration: none;
+          white-space: nowrap;
+          display: flex;
+          align-items: center;
+          justify-content: left;
+          width: 100%;
+          padding: 0.25em 8px;
+          transition: 0.2s background ease-in-out;
+
+        }
+        .menu-item[hidden] {
+          display: none;
+        }
+        mr-dropdown.menu-item {
+          width: 100%;
+          padding: 0;
+          --mr-dropdown-anchor-padding: 0.25em 8px;
+          --mr-dropdown-anchor-justify-content: space-between;
+        }
+        .menu hr {
+          width: 96%;
+          margin: 0 2%;
+          border: 0;
+          height: 1px;
+          background: hsl(0, 0%, 80%);
+        }
+        .menu a {
+          cursor: pointer;
+          color: var(--chops-link-color);
+        }
+        .menu a:hover, .menu a:focus {
+          background: var(--chops-active-choice-bg);
+        }
+      `,
+    ];
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
+      <button class="anchor"
+        @click=${this.toggle}
+        @keydown=${this._exitMenuOnEsc}
+        ?disabled=${this.disabled}
+        title=${this.title || this.label}
+        aria-label=${this.label}
+        aria-expanded=${this.opened}
+      >
+        ${this.text}
+        <i class="material-icons" aria-hidden="true">${this.icon}</i>
+      </button>
+      <div class="menu ${this.menuAlignment}">
+        ${this.items.map((item, index) => this._renderItem(item, index))}
+        <slot></slot>
+      </div>
+    `;
+  }
+
+  /**
+   * Render a single dropdown menu item.
+   * @param {MenuItem} item
+   * @param {number} index The item's position in the list of items.
+   * @return {TemplateResult}
+   */
+  _renderItem(item, index) {
+    if (item.separator) {
+      // The menu item is a no-op divider between sections.
+      return html`
+        <strong ?hidden=${!item.text} class="menu-item">
+          ${item.text}
+        </strong>
+        <hr />
+      `;
+    }
+    if (item.items && item.items.length) {
+      // The menu contains a sub-menu.
+      return html`
+        <mr-dropdown
+          .text=${item.text}
+          .items=${item.items}
+          menuAlignment="side"
+          icon="arrow_right"
+          data-idx=${index}
+          class="menu-item"
+        ></mr-dropdown>
+      `;
+    }
+
+    return html`
+      <a
+        href=${ifDefined(item.url)}
+        @click=${this._runItemHandler}
+        @keydown=${this._onItemKeydown}
+        data-idx=${index}
+        tabindex="0"
+        class="menu-item"
+      >
+        <i
+          class="material-icons"
+          ?hidden=${item.icon === undefined}
+        >${item.icon}</i>
+        ${item.text}
+      </a>
+    `;
+  }
+
+  /** @override */
+  constructor() {
+    super();
+
+    this.label = '';
+    this.text = '';
+    this.items = [];
+    this.icon = 'arrow_drop_down';
+    this.menuAlignment = 'right';
+    this.opened = false;
+    this.disabled = false;
+
+    this._boundCloseOnOutsideClick = this._closeOnOutsideClick.bind(this);
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      title: {type: String},
+      label: {type: String},
+      text: {type: String},
+      items: {type: Array},
+      icon: {type: String},
+      menuAlignment: {type: String},
+      opened: {type: Boolean, reflect: true},
+      disabled: {type: Boolean},
+    };
+  }
+
+  /**
+   * Either runs the click handler attached to the clicked item and closes the
+   * menu.
+   * @param {MouseEvent|KeyboardEvent} e
+   */
+  _runItemHandler(e) {
+    if (e instanceof MouseEvent || e.code === 'Enter') {
+      const idx = e.target.dataset.idx;
+      if (idx !== undefined && this.items[idx].handler) {
+        this.items[idx].handler();
+      }
+      this.close();
+    }
+  }
+
+  /**
+   * Runs multiple event handlers when a user types a key while
+   * focusing a menu item.
+   * @param {KeyboardEvent} e
+   */
+  _onItemKeydown(e) {
+    this._runItemHandler(e);
+    this._exitMenuOnEsc(e);
+  }
+
+  /**
+   * If the user types Esc while focusing any dropdown item, then
+   * exit the dropdown.
+   * @param {KeyboardEvent} e
+   */
+  _exitMenuOnEsc(e) {
+    if (e.key === 'Escape') {
+      this.close();
+
+      // Return focus to the anchor of the dropdown on closing, so that
+      // users don't lose their overall focus position within the page.
+      const anchor = this.shadowRoot.querySelector('.anchor');
+      anchor.focus();
+    }
+  }
+
+  /** @override */
+  connectedCallback() {
+    super.connectedCallback();
+    window.addEventListener('click', this._boundCloseOnOutsideClick, true);
+  }
+
+  /** @override */
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    window.removeEventListener('click', this._boundCloseOnOutsideClick, true);
+  }
+
+  /** @override */
+  updated(changedProperties) {
+    if (changedProperties.has('label') || changedProperties.has('text')) {
+      if (!this.label && !this.text) {
+        console.error(SCREENREADER_ATTRIBUTE_ERROR);
+      }
+    }
+  }
+
+  /**
+   * Closes and opens the dropdown menu.
+   */
+  toggle() {
+    this.opened = !this.opened;
+  }
+
+  /**
+   * Opens the dropdown menu.
+   */
+  open() {
+    this.opened = true;
+  }
+
+  /**
+   * Closes the dropdown menu.
+   */
+  close() {
+    this.opened = false;
+  }
+
+  /**
+   * Click a specific item in mr-dropdown, using JavaScript. Useful for testing.
+   *
+   * @param {number} i index of the item to click.
+   */
+  clickItem(i) {
+    const items = this.shadowRoot.querySelectorAll('.menu-item');
+    items[i].click();
+  }
+
+  /**
+   * @param {MouseEvent} evt
+   * @private
+   */
+  _closeOnOutsideClick(evt) {
+    if (!this.opened) return;
+
+    const hasMenu = evt.composedPath().find(
+        (node) => {
+          return node === this;
+        },
+    );
+    if (hasMenu) return;
+
+    this.close();
+  }
+}
+
+customElements.define('mr-dropdown', MrDropdown);