blob: 4858a31f5a31798cda65f8129aa7066c7c018b4b [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-dropdown/iron-dropdown';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import {LitElement, html, css, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {strToClassName} from '../../../utils/dom-util';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {copyToClipbard, queryAndAssert} from '../../../utils/common-util';
import {ValueChangedEvent} from '../../../types/events';
export interface CopyLink {
label: string;
shortcut: string;
value: string;
}
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@customElement('gr-copy-links')
export class GrCopyLinks extends LitElement {
@property({type: Array})
copyLinks: CopyLink[] = [];
@state() isDropdownOpen = false;
@query('iron-dropdown') private dropdown?: IronDropdownElement;
static override get styles() {
return [
css`
iron-dropdown {
box-shadow: var(--elevation-level-2);
width: min(90vw, 640px);
background-color: var(--dialog-background-color);
border-radius: var(--border-radius);
}
[slot='dropdown-content'] {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-m);
}
.copy-link-row {
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
}
.copy-link-row label {
flex: 0 0 120px;
color: var(--deemphasized-text-color);
}
.copy-link-row input {
flex: 1 1 420px;
}
.copy-link-row .shortcut {
width: 27px;
margin: 0 var(--spacing-m);
color: var(--deemphasized-text-color);
}
.copy-link-row gr-copy-clipboard {
flex: 0 0 20px;
}
/* TODO(milutin): It's from shared styles, move it to input styles */
input {
background-color: var(--background-color-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-sizing: border-box;
color: var(--primary-text-color);
margin: 0;
padding: var(--spacing-s);
font: inherit;
}
`,
];
}
override render() {
if (!this.copyLinks) return nothing;
return html`<iron-dropdown
.horizontalAlign=${'left'}
.verticalAlign=${'top'}
.verticalOffset=${20}
@keydown=${this.handleKeydown}
@opened-changed=${(e: ValueChangedEvent<boolean>) =>
(this.isDropdownOpen = e.detail.value)}
>
${this.renderCopyLinks()}
</iron-dropdown>`;
}
private renderCopyLinks() {
return html`<div slot="dropdown-content">
${this.copyLinks?.map(link => this.renderCopyLinkRow(link))}
</div>`;
}
private renderCopyLinkRow(copyLink: CopyLink) {
const {label, shortcut, value} = copyLink;
const id = `${strToClassName(label, '')}-field`;
// TODO(milutin): Use input in gr-copy-clipboard instead of creating new
// one. Move shorcut to gr-copy-clipboard.
return html`<div class="copy-link-row">
<label for=${id}>${label}</label
><input type="text" readonly="" id=${id} class="input" .value=${value} />
<span class="shortcut">${`l - ${shortcut}`}</span>
<gr-copy-clipboard
hideInput=""
text=${value}
id=${`${id}-copy-clipboard`}
></gr-copy-clipboard>
</div>`;
}
private async handleKeydown(e: KeyboardEvent) {
const copyLink = this.copyLinks?.find(link => link.shortcut === e.key);
if (!copyLink) return;
await copyToClipbard(copyLink.value, copyLink.label);
this.closeDropdown();
}
toggleDropdown() {
this.isDropdownOpen ? this.closeDropdown() : this.openDropdown();
}
private closeDropdown() {
this.dropdown?.close();
}
openDropdown() {
this.dropdown?.open();
this.awaitOpen(() => {
queryAndAssert<HTMLInputElement>(this.dropdown, 'input')?.select();
});
}
/**
* NOTE: (milutin) Slightly hacky way to listen to the overlay actually
* opening. It's from gr-editable-label. It will be removed when we
* migrate out of iron-* components.
*/
private awaitOpen(fn: () => void) {
let iters = 0;
const step = () => {
setTimeout(() => {
if (this.dropdown?.style.display !== 'none') {
fn.call(this);
} else if (iters++ < AWAIT_MAX_ITERS) {
step.call(this);
}
}, AWAIT_STEP);
};
step.call(this);
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-copy-links': GrCopyLinks;
}
}