Skip to content

Commit

Permalink
feat(foxy-copy-to-clipboard): add support for text layout
Browse files Browse the repository at this point in the history
  • Loading branch information
pheekus committed Sep 21, 2024
1 parent 240f0d9 commit 43ce7ae
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 44 deletions.
155 changes: 147 additions & 8 deletions src/elements/public/CopyToClipboard/CopyToClipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ describe('CopyToClipboard', () => {
expect(new CopyToClipboard()).to.be.instanceOf(LitElement);
});

it('has a reactive property/attribite named "layout" (String)', () => {
expect(CopyToClipboard).to.have.deep.nested.property('properties.layout', {});
expect(new CopyToClipboard()).to.have.property('layout', null);
});

it('has a reactive property/attribite named "theme" (String)', () => {
expect(CopyToClipboard).to.have.deep.nested.property('properties.theme', {});
expect(new CopyToClipboard()).to.have.property('theme', null);
});

it('has a reactive property/attribite named "text" (String)', () => {
expect(CopyToClipboard).to.have.nested.property('properties.text.type', String);
});
Expand All @@ -37,7 +47,7 @@ describe('CopyToClipboard', () => {
expect(new CopyToClipboard()).to.have.property('ns', 'copy-to-clipboard');
});

it('renders in the idle state by default', async () => {
it('renders in the idle state by default (icon layout)', async () => {
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const tooltip = element.renderRoot.querySelector('vcf-tooltip foxy-i18n') as HTMLElement;
Expand All @@ -46,23 +56,32 @@ describe('CopyToClipboard', () => {
expect(tooltip).to.have.property('key', 'click_to_copy');
});

it('renders default icon when icon attribute is not set', async () => {
it('renders in the idle state by default (text layout)', async () => {
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const tooltip = element.renderRoot.querySelector('vaadin-button foxy-i18n') as HTMLElement;

expect(tooltip).to.have.property('infer', '');
expect(tooltip).to.have.property('key', 'click_to_copy');
});

it('renders default icon in icon layout when icon attribute is not set', async () => {
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const icon = element.renderRoot.querySelector('iron-icon') as HTMLElement;

expect(icon).to.have.property('icon', 'icons:content-copy');
});

it('renders custom icon when icon attribute is set', async () => {
it('renders custom icon in icon layout when icon attribute is set', async () => {
const layout = html`<foxy-copy-to-clipboard icon="icons:foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const icon = element.renderRoot.querySelector('iron-icon') as HTMLElement;

expect(icon).to.have.property('icon', 'icons:foo');
});

it('copies text on click', async () => {
it('copies text on click in icon layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
const layout = html`<foxy-copy-to-clipboard text="Foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
Expand All @@ -86,7 +105,31 @@ describe('CopyToClipboard', () => {
writeTextMethod.restore();
});

it('switches to the busy state when copying text', async () => {
it('copies text on click in text layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const button = element.renderRoot.querySelector('vaadin-button');

button?.click();
await waitUntil(
() => {
try {
expect(writeTextMethod).to.have.been.calledOnceWith('Foo');
return true;
} catch {
return false;
}
},
undefined,
{ timeout: 5000 }
);

expect(writeTextMethod).to.have.been.calledOnceWith('Foo');
writeTextMethod.restore();
});

it('switches to the busy state when copying text in icon layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').returns(
new Promise(() => void 0)
);
Expand All @@ -104,7 +147,25 @@ describe('CopyToClipboard', () => {
writeTextMethod.restore();
});

it('switches to the idle state ~2s after copying text successfully', async () => {
it('switches to the busy state when copying text in text layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').returns(
new Promise(() => void 0)
);

const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const button = element.renderRoot.querySelector('vaadin-button');
const tooltip = button?.querySelector('foxy-i18n');

button?.click();
await element.requestUpdate();

expect(tooltip).to.have.property('infer', '');
expect(tooltip).to.have.property('key', 'copying');
writeTextMethod.restore();
});

it('switches to the idle state ~2s after copying text successfully in icon layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
Expand All @@ -127,7 +188,30 @@ describe('CopyToClipboard', () => {
writeTextMethod.restore();
});

it('switches to the error state when copying text fails', async () => {
it('switches to the idle state ~2s after copying text successfully in text layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const button = element.renderRoot.querySelector('vaadin-button');
const tooltip = button?.querySelector('foxy-i18n');

button?.click();

await waitUntil(
async () => {
await element.requestUpdate();
return tooltip?.getAttribute('key') === 'click_to_copy';
},
undefined,
{ timeout: 5000 }
);

expect(tooltip).to.have.property('infer', '');
expect(tooltip).to.have.property('key', 'click_to_copy');
writeTextMethod.restore();
});

it('switches to the error state when copying text fails in icon layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
const layout = html`<foxy-copy-to-clipboard text="Foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
Expand All @@ -150,7 +234,30 @@ describe('CopyToClipboard', () => {
writeTextMethod.restore();
});

it('switches to the idle state ~2s after copying text fails', async () => {
it('switches to the error state when copying text fails in text layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const button = element.renderRoot.querySelector('vaadin-button');
const tooltip = button?.querySelector('foxy-i18n');

button?.click();

await waitUntil(
async () => {
await element.requestUpdate();
return tooltip?.getAttribute('key') === 'failed_to_copy';
},
undefined,
{ timeout: 5000 }
);

expect(tooltip).to.have.property('infer', '');
expect(tooltip).to.have.property('key', 'failed_to_copy');
writeTextMethod.restore();
});

it('switches to the idle state ~2s after copying text fails in icon layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
Expand All @@ -172,4 +279,36 @@ describe('CopyToClipboard', () => {
expect(tooltip).to.have.property('key', 'click_to_copy');
writeTextMethod.restore();
});

it('switches to the idle state ~2s after copying text fails in text layout', async () => {
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
const element = await fixture<CopyToClipboard>(layout);
const button = element.renderRoot.querySelector('vaadin-button');
const tooltip = button?.querySelector('foxy-i18n');

button?.click();

await waitUntil(
async () => {
await element.requestUpdate();
return tooltip?.getAttribute('key') === 'click_to_copy';
},
undefined,
{ timeout: 5000 }
);

expect(tooltip).to.have.property('infer', '');
expect(tooltip).to.have.property('key', 'click_to_copy');
writeTextMethod.restore();
});

it('propagates theme attribute to vaadin-button in text layout', async () => {
const element = await fixture<CopyToClipboard>(html`
<foxy-copy-to-clipboard layout="text" theme="foo"></foxy-copy-to-clipboard>
`);

const button = element.renderRoot.querySelector('vaadin-button');
expect(button).to.have.attribute('theme', 'foo');
});
});
99 changes: 63 additions & 36 deletions src/elements/public/CopyToClipboard/CopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import {
CSSResult,
LitElement,
PropertyDeclarations,
TemplateResult,
css,
html,
} from 'lit-element';
import type { CSSResult, PropertyDeclarations, TemplateResult } from 'lit-element';

import { LitElement, css, html } from 'lit-element';
import { TranslatableMixin } from '../../../mixins/translatable';
import { ConfigurableMixin } from '../../../mixins/configurable';
import { InferrableMixin } from '../../../mixins/inferrable';
import { TranslatableMixin } from '../../../mixins/translatable';
import { ifDefined } from 'lit-html/directives/if-defined';

const Base = ConfigurableMixin(TranslatableMixin(InferrableMixin(LitElement), 'copy-to-clipboard'));
const NS = 'copy-to-clipboard';
const Base = ConfigurableMixin(TranslatableMixin(InferrableMixin(LitElement), NS));

/**
* A simple "click to copy" button that takes the size of the font
Expand All @@ -24,6 +20,8 @@ export class CopyToClipboard extends Base {
static get properties(): PropertyDeclarations {
return {
...super.properties,
layout: {},
theme: {},
text: { type: String },
icon: { type: String },
__state: { attribute: false },
Expand All @@ -32,7 +30,7 @@ export class CopyToClipboard extends Base {

static get styles(): CSSResult {
return css`
button {
.icon-button {
position: relative;
appearance: none;
background: none;
Expand All @@ -48,7 +46,7 @@ export class CopyToClipboard extends Base {
align-items: center;
}
button::before {
.icon-button::before {
position: absolute;
inset: 0;
content: ' ';
Expand All @@ -59,22 +57,22 @@ export class CopyToClipboard extends Base {
border-radius: var(--lumo-border-radius-s);
}
button:focus {
.icon-button:focus {
outline: none;
box-shadow: 0 0 0 2px currentColor;
}
button:disabled {
.icon-button:disabled {
opacity: 0.5;
cursor: default;
}
@media (hover: hover) {
button:not(:disabled):hover {
.icon-button:not(:disabled):hover {
cursor: pointer;
}
button:not(:disabled):hover::before {
.icon-button:not(:disabled):hover::before {
opacity: 0.16;
}
}
Expand All @@ -86,6 +84,12 @@ export class CopyToClipboard extends Base {
`;
}

/** Icon or text UI. Icon UI by default. */
layout: 'text' | 'icon' | null = null;

/** VaadinButton theme for text layout. */
theme: string | null = null;

/** Default icon. */
icon: string | null = null;

Expand All @@ -95,6 +99,7 @@ export class CopyToClipboard extends Base {
private __state: 'idle' | 'busy' | 'fail' | 'done' = 'idle';

render(): TemplateResult {
const layout = this.layout === 'text' ? 'text' : 'icon';
let label = '';
let icon = '';

Expand All @@ -113,26 +118,48 @@ export class CopyToClipboard extends Base {
}

return html`
<button
id="trigger"
?disabled=${this.disabled}
@click=${() => {
if (this.__state === 'idle') {
this.__state = 'busy';
navigator.clipboard
.writeText(this.text ?? '')
.then(() => (this.__state = 'done'))
.catch(() => (this.__state = 'fail'))
.then(() => setTimeout(() => (this.__state = 'idle'), 2000));
}
}}
>
<iron-icon icon=${icon}></iron-icon>
</button>
<vcf-tooltip for="trigger" position="bottom">
<span class="text-s"><foxy-i18n infer="" class="text-s" key=${label}></foxy-i18n></span>
</vcf-tooltip>
${layout === 'icon'
? html`
<button
id="trigger"
class="icon-button"
?disabled=${this.disabled}
@click=${this.__copy}
>
<iron-icon icon=${icon}></iron-icon>
</button>
<vcf-tooltip
position="bottom"
style="--lumo-base-color: black"
theme="light"
for="trigger"
>
<span class="text-s" style="color: white">
<foxy-i18n infer="" key=${label}></foxy-i18n>
</span>
</vcf-tooltip>
`
: html`
<vaadin-button
theme=${ifDefined(this.theme ?? void 0)}
?disabled=${this.disabled}
@click=${this.__copy}
>
<foxy-i18n infer="" key=${label}></foxy-i18n>
</vaadin-button>
`}
`;
}

private __copy() {
if (this.__state === 'idle') {
this.__state = 'busy';

navigator.clipboard
.writeText(this.text ?? '')
.then(() => (this.__state = 'done'))
.catch(() => (this.__state = 'fail'))
.then(() => setTimeout(() => (this.__state = 'idle'), 2000));
}
}
}

0 comments on commit 43ce7ae

Please sign in to comment.