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);