blob: c9c30d5e7e0e8f24db9d1718594ee5afecfb712b [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {createRef, ref, Ref} from 'lit/directives/ref.js';
import {customElement, property, query, state} from 'lit/decorators.js';
import {strToClassName} from '../../../utils/dom-util';
import {copyToClipboard, queryAndAssert} from '../../../utils/common-util';
import {formStyles} from '../../../styles/form-styles';
import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '@material/web/menu/menu';
import {MdMenu} from '@material/web/menu/menu';
import {MdOutlinedTextField} from '@material/web/textfield/outlined-text-field';
export interface CopyLink {
label: string;
shortcut: string;
value: string;
multiline?: boolean;
}
const AWAIT_MAX_ITERS = 10;
const AWAIT_STEP = 5;
@customElement('gr-copy-links')
export class GrCopyLinks extends LitElement {
copyClipboardRef: Ref<GrCopyClipboard> = createRef();
@property({type: Array})
copyLinks: CopyLink[] = [];
@property({type: String})
horizontalAlign: 'left' | 'right' = 'left';
@property({type: String})
shortcutPrefix = 'l - ';
@property({type: Number})
verticalOffset = 10;
@state() isDropdownOpen = false;
// private but used in screenshot tests
@query('md-menu') dropdown?: MdMenu;
static override get styles() {
return [
formStyles,
css`
md-menu {
white-space: nowrap;
--md-menu-container-color: var(--dialog-background-color);
--md-menu-top-space: 0px;
--md-menu-bottom-space: 0px;
}
.dropdown-content {
width: min(90vw, 640px);
padding: var(--spacing-m) var(--spacing-l) var(--spacing-m);
box-shadow: var(--elevation-level-2);
border-radius: var(--border-radius);
}
.copy-link-row {
margin-bottom: var(--spacing-m);
}
gr-copy-clipboard::part(text-container-wrapper-style) {
flex: 1 1 420px;
}
`,
];
}
override connectedCallback() {
super.connectedCallback();
if (this.isDropdownOpen) {
this.setUpGlobalEventListeners();
}
}
override updated(changedProperties: PropertyValues) {
if (changedProperties.has('isDropdownOpen')) {
if (this.isDropdownOpen) {
this.setUpGlobalEventListeners();
} else {
this.cleanUpGlobalEventListeners();
}
}
}
private setUpGlobalEventListeners() {
const passiveOptions: AddEventListenerOptions = {passive: true};
window.addEventListener('resize', this.onWindowResize, passiveOptions);
window.addEventListener('scroll', this.onWindowResize, passiveOptions);
}
private cleanUpGlobalEventListeners() {
const passiveOptions: AddEventListenerOptions = {passive: true};
window.removeEventListener('resize', this.onWindowResize, passiveOptions);
window.removeEventListener('scroll', this.onWindowResize, passiveOptions);
}
private readonly onWindowResize = () => {
this.dropdown?.reposition();
};
override render() {
if (!this.copyLinks) return nothing;
return html`<md-menu
default-focus="none"
tabindex="-1"
.menuCorner=${this.horizontalAlign === 'left'
? 'start-start'
: 'end-start'}
?quick=${true}
.yOffset=${this.verticalOffset}
@opened=${() => {
this.isDropdownOpen = true;
}}
@closed=${() => {
this.isDropdownOpen = false;
}}
@keydown=${this.handleKeydown}
>
${this.renderCopyLinks()}
</md-menu> `;
}
private renderCopyLinks() {
return html`<div class="dropdown-content">
${this.copyLinks?.map((link, index) =>
this.renderCopyLinkRow(link, index)
)}
</div>`;
}
private renderCopyLinkRow(copyLink: CopyLink, index?: number) {
const {label, shortcut, value, multiline} = copyLink;
const id = `${strToClassName(label, '')}-field`;
return html`<div class="copy-link-row">
<gr-copy-clipboard
text=${value}
label=${label}
shortcut=${`${this.shortcutPrefix}${shortcut}`}
id=${`${id}-copy-clipboard`}
nowrap
?multiline=${!!multiline}
${index === 0 && ref(this.copyClipboardRef)}
></gr-copy-clipboard>
</div>`;
}
private async handleKeydown(e: KeyboardEvent) {
const copyLink = this.copyLinks?.find(link => link.shortcut === e.key);
if (!copyLink) return;
await copyToClipboard(copyLink.value, copyLink.label);
this.closeDropdown();
}
toggleDropdown(button?: HTMLElement) {
if (button) {
this.dropdown!.anchorElement = button;
}
this.isDropdownOpen ? this.closeDropdown() : this.openDropdown();
}
private closeDropdown() {
this.dropdown?.close();
}
openDropdown(button?: HTMLElement) {
if (button) {
this.dropdown!.anchorElement = button;
}
this.dropdown?.show();
this.awaitOpen(() => {
if (!this.copyClipboardRef?.value) return;
queryAndAssert<MdOutlinedTextField>(
this.copyClipboardRef.value,
'md-outlined-text-field'
)?.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;
}
}