Skip to content

Instantly share code, notes, and snippets.

@paladox
Created July 9, 2025 13:45
Show Gist options
  • Save paladox/62d2efa87f35db54cbc94527303f067e to your computer and use it in GitHub Desktop.
Save paladox/62d2efa87f35db54cbc94527303f067e to your computer and use it in GitHub Desktop.
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import {css, html, LitElement, nothing} 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';
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 {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-m);
width: min(90vw, 640px);
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 render() {
if (!this.copyLinks) return nothing;
return html`<md-menu
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() {
this.dropdown?.show();
this.awaitOpen(() => {
if (!this.copyClipboardRef?.value) return;
queryAndAssert<HTMLInputElement>(
this.copyClipboardRef.value,
'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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment