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