Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/chops/chops-dialog/chops-dialog.js b/static_src/elements/chops/chops-dialog/chops-dialog.js
new file mode 100644
index 0000000..0d40aa2
--- /dev/null
+++ b/static_src/elements/chops/chops-dialog/chops-dialog.js
@@ -0,0 +1,254 @@
+// 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';
+
+/**
+ * `<chops-dialog>` displays a modal/dialog overlay.
+ *
+ * @customElement
+ */
+export class ChopsDialog extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      :host {
+        position: fixed;
+        z-index: 9999;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        overflow: auto;
+        background-color: rgba(0,0,0,0.4);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+      :host(:not([opened])), [hidden] {
+        display: none;
+        visibility: hidden;
+      }
+      :host([closeOnOutsideClick]),
+      :host([closeOnOutsideClick]) .dialog::backdrop {
+        /* TODO(zhangtiff): Deprecate custom backdrop in favor of native
+        * browser backdrop.
+        */
+        cursor: pointer;
+      }
+      .dialog {
+        background: none;
+        border: 0;
+        max-width: 90%;
+      }
+      .dialog-content {
+        /* This extra div is here because otherwise the browser can't
+        * differentiate between a click event that hits the dialog element or
+        * its backdrop pseudoelement.
+        */
+        box-sizing: border-box;
+        background: var(--chops-white);
+        padding: 1em 16px;
+        cursor: default;
+        box-shadow: 0px 3px 20px 0px hsla(0, 0%, 0%, 0.4);
+        width: var(--chops-dialog-width);
+        max-width: var(--chops-dialog-max-width, 100%);
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <dialog class="dialog" role="dialog" @cancel=${this._cancelHandler}>
+        <div class="dialog-content">
+          <slot></slot>
+        </div>
+      </dialog>
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      /**
+       * Whether the dialog should currently be displayed or not.
+       */
+      opened: {
+        type: Boolean,
+        reflect: true,
+      },
+      /**
+       * A boolean that determines whether clicking outside of the dialog
+       * window should close it.
+       */
+      closeOnOutsideClick: {
+        type: Boolean,
+      },
+      /**
+       * A function fired when the element tries to change its own opened
+       * state. This is useful if you want the dialog state managed outside
+       * of the dialog instead of with internal state. (ie: with Redux)
+       */
+      onOpenedChange: {
+        type: Object,
+      },
+      /**
+       * When True, disables exiting keys and closing on outside clicks.
+       * Forces the user to interact with the dialog rather than just dismissing
+       * it.
+       */
+      forced: {
+        type: Boolean,
+      },
+      _boundKeydownHandler: {
+        type: Object,
+      },
+      _previousFocusedElement: {
+        type: Object,
+      },
+    };
+  }
+
+  /** @override */
+  constructor() {
+    super();
+
+    this.opened = false;
+    this.closeOnOutsideClick = false;
+    this.forced = false;
+    this._boundKeydownHandler = this._keydownHandler.bind(this);
+  }
+
+  /** @override */
+  connectedCallback() {
+    super.connectedCallback();
+
+    this.addEventListener('click', (evt) => {
+      if (!this.opened || !this.closeOnOutsideClick || this.forced) return;
+
+      const hasDialog = evt.composedPath().find(
+          (node) => {
+            return node.classList && node.classList.contains('dialog-content');
+          }
+      );
+      if (hasDialog) return;
+
+      this.close();
+    });
+
+    window.addEventListener('keydown', this._boundKeydownHandler, true);
+  }
+
+  /** @override */
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    window.removeEventListener('keydown', this._boundKeydownHandler,
+        true);
+  }
+
+  /** @override */
+  updated(changedProperties) {
+    if (changedProperties.has('opened')) {
+      this._openedChanged(this.opened);
+    }
+  }
+
+  _keydownHandler(event) {
+    if (!this.opened) return;
+    if (event.key === 'Escape' && this.forced) {
+      // Stop users from using the Escape key in a forced dialog.
+      e.preventDefault();
+    }
+  }
+
+  /**
+   * Closes the dialog.
+   * May have its logic overridden by a custom onOpenChanged function.
+   */
+  close() {
+    if (this.onOpenedChange) {
+      this.onOpenedChange(false);
+    } else {
+      this.opened = false;
+    }
+  }
+
+  /**
+   * Opens the dialog.
+   * May have its logic overridden by a custom onOpenChanged function.
+   */
+  open() {
+    if (this.onOpenedChange) {
+      this.onOpenedChange(true);
+    } else {
+      this.opened = true;
+    }
+  }
+
+  /**
+   * Switches the dialog from open to closed or vice versa.
+   */
+  toggle() {
+    this.opened = !this.opened;
+  }
+
+  _cancelHandler(evt) {
+    if (!this.forced) {
+      this.close();
+    } else {
+      evt.preventDefault();
+    }
+  }
+
+  _getActiveElement() {
+    // document.activeElement alone isn't sufficient to find the active
+    // element within shadow dom.
+    let active = document.activeElement || document.body;
+    let activeRoot = active.shadowRoot || active.root;
+    while (activeRoot && activeRoot.activeElement) {
+      active = activeRoot.activeElement;
+      activeRoot = active.shadowRoot || active.root;
+    }
+    return active;
+  }
+
+  _openedChanged(opened) {
+    const dialog = this.shadowRoot.querySelector('dialog');
+    if (opened) {
+      // For accessibility, we want to ensure we remember the element that was
+      // focused before this dialog opened.
+      this._previousFocusedElement = this._getActiveElement();
+
+      if (dialog.showModal) {
+        dialog.showModal();
+      } else {
+        dialog.setAttribute('open', 'true');
+      }
+      if (this._previousFocusedElement) {
+        this._previousFocusedElement.blur();
+      }
+    } else {
+      if (dialog.close) {
+        dialog.close();
+      } else {
+        dialog.setAttribute('open', undefined);
+      }
+
+      if (this._previousFocusedElement) {
+        const element = this._previousFocusedElement;
+        requestAnimationFrame(() => {
+          // HACK. This is to prevent a possible accessibility bug where
+          // using a keypress to trigger a button that exits a modal causes
+          // the modal to immediately re-open because the button that
+          // originally opened the modal refocuses, and the keypress
+          // propagates.
+          element.focus();
+        });
+      }
+    }
+  }
+}
+
+customElements.define('chops-dialog', ChopsDialog);