Skip to content

Commit 43ce7ae

Browse files
committed
feat(foxy-copy-to-clipboard): add support for text layout
1 parent 240f0d9 commit 43ce7ae

File tree

2 files changed

+210
-44
lines changed

2 files changed

+210
-44
lines changed

src/elements/public/CopyToClipboard/CopyToClipboard.test.ts

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ describe('CopyToClipboard', () => {
2424
expect(new CopyToClipboard()).to.be.instanceOf(LitElement);
2525
});
2626

27+
it('has a reactive property/attribite named "layout" (String)', () => {
28+
expect(CopyToClipboard).to.have.deep.nested.property('properties.layout', {});
29+
expect(new CopyToClipboard()).to.have.property('layout', null);
30+
});
31+
32+
it('has a reactive property/attribite named "theme" (String)', () => {
33+
expect(CopyToClipboard).to.have.deep.nested.property('properties.theme', {});
34+
expect(new CopyToClipboard()).to.have.property('theme', null);
35+
});
36+
2737
it('has a reactive property/attribite named "text" (String)', () => {
2838
expect(CopyToClipboard).to.have.nested.property('properties.text.type', String);
2939
});
@@ -37,7 +47,7 @@ describe('CopyToClipboard', () => {
3747
expect(new CopyToClipboard()).to.have.property('ns', 'copy-to-clipboard');
3848
});
3949

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

49-
it('renders default icon when icon attribute is not set', async () => {
59+
it('renders in the idle state by default (text layout)', async () => {
60+
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
61+
const element = await fixture<CopyToClipboard>(layout);
62+
const tooltip = element.renderRoot.querySelector('vaadin-button foxy-i18n') as HTMLElement;
63+
64+
expect(tooltip).to.have.property('infer', '');
65+
expect(tooltip).to.have.property('key', 'click_to_copy');
66+
});
67+
68+
it('renders default icon in icon layout when icon attribute is not set', async () => {
5069
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
5170
const element = await fixture<CopyToClipboard>(layout);
5271
const icon = element.renderRoot.querySelector('iron-icon') as HTMLElement;
5372

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

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

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

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

89-
it('switches to the busy state when copying text', async () => {
108+
it('copies text on click in text layout', async () => {
109+
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
110+
const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
111+
const element = await fixture<CopyToClipboard>(layout);
112+
const button = element.renderRoot.querySelector('vaadin-button');
113+
114+
button?.click();
115+
await waitUntil(
116+
() => {
117+
try {
118+
expect(writeTextMethod).to.have.been.calledOnceWith('Foo');
119+
return true;
120+
} catch {
121+
return false;
122+
}
123+
},
124+
undefined,
125+
{ timeout: 5000 }
126+
);
127+
128+
expect(writeTextMethod).to.have.been.calledOnceWith('Foo');
129+
writeTextMethod.restore();
130+
});
131+
132+
it('switches to the busy state when copying text in icon layout', async () => {
90133
const writeTextMethod = stub(navigator.clipboard, 'writeText').returns(
91134
new Promise(() => void 0)
92135
);
@@ -104,7 +147,25 @@ describe('CopyToClipboard', () => {
104147
writeTextMethod.restore();
105148
});
106149

107-
it('switches to the idle state ~2s after copying text successfully', async () => {
150+
it('switches to the busy state when copying text in text layout', async () => {
151+
const writeTextMethod = stub(navigator.clipboard, 'writeText').returns(
152+
new Promise(() => void 0)
153+
);
154+
155+
const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
156+
const element = await fixture<CopyToClipboard>(layout);
157+
const button = element.renderRoot.querySelector('vaadin-button');
158+
const tooltip = button?.querySelector('foxy-i18n');
159+
160+
button?.click();
161+
await element.requestUpdate();
162+
163+
expect(tooltip).to.have.property('infer', '');
164+
expect(tooltip).to.have.property('key', 'copying');
165+
writeTextMethod.restore();
166+
});
167+
168+
it('switches to the idle state ~2s after copying text successfully in icon layout', async () => {
108169
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
109170
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
110171
const element = await fixture<CopyToClipboard>(layout);
@@ -127,7 +188,30 @@ describe('CopyToClipboard', () => {
127188
writeTextMethod.restore();
128189
});
129190

130-
it('switches to the error state when copying text fails', async () => {
191+
it('switches to the idle state ~2s after copying text successfully in text layout', async () => {
192+
const writeTextMethod = stub(navigator.clipboard, 'writeText').resolves();
193+
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
194+
const element = await fixture<CopyToClipboard>(layout);
195+
const button = element.renderRoot.querySelector('vaadin-button');
196+
const tooltip = button?.querySelector('foxy-i18n');
197+
198+
button?.click();
199+
200+
await waitUntil(
201+
async () => {
202+
await element.requestUpdate();
203+
return tooltip?.getAttribute('key') === 'click_to_copy';
204+
},
205+
undefined,
206+
{ timeout: 5000 }
207+
);
208+
209+
expect(tooltip).to.have.property('infer', '');
210+
expect(tooltip).to.have.property('key', 'click_to_copy');
211+
writeTextMethod.restore();
212+
});
213+
214+
it('switches to the error state when copying text fails in icon layout', async () => {
131215
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
132216
const layout = html`<foxy-copy-to-clipboard text="Foo"></foxy-copy-to-clipboard>`;
133217
const element = await fixture<CopyToClipboard>(layout);
@@ -150,7 +234,30 @@ describe('CopyToClipboard', () => {
150234
writeTextMethod.restore();
151235
});
152236

153-
it('switches to the idle state ~2s after copying text fails', async () => {
237+
it('switches to the error state when copying text fails in text layout', async () => {
238+
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
239+
const layout = html`<foxy-copy-to-clipboard layout="text" text="Foo"></foxy-copy-to-clipboard>`;
240+
const element = await fixture<CopyToClipboard>(layout);
241+
const button = element.renderRoot.querySelector('vaadin-button');
242+
const tooltip = button?.querySelector('foxy-i18n');
243+
244+
button?.click();
245+
246+
await waitUntil(
247+
async () => {
248+
await element.requestUpdate();
249+
return tooltip?.getAttribute('key') === 'failed_to_copy';
250+
},
251+
undefined,
252+
{ timeout: 5000 }
253+
);
254+
255+
expect(tooltip).to.have.property('infer', '');
256+
expect(tooltip).to.have.property('key', 'failed_to_copy');
257+
writeTextMethod.restore();
258+
});
259+
260+
it('switches to the idle state ~2s after copying text fails in icon layout', async () => {
154261
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
155262
const layout = html`<foxy-copy-to-clipboard></foxy-copy-to-clipboard>`;
156263
const element = await fixture<CopyToClipboard>(layout);
@@ -172,4 +279,36 @@ describe('CopyToClipboard', () => {
172279
expect(tooltip).to.have.property('key', 'click_to_copy');
173280
writeTextMethod.restore();
174281
});
282+
283+
it('switches to the idle state ~2s after copying text fails in text layout', async () => {
284+
const writeTextMethod = stub(navigator.clipboard, 'writeText').rejects();
285+
const layout = html`<foxy-copy-to-clipboard layout="text"></foxy-copy-to-clipboard>`;
286+
const element = await fixture<CopyToClipboard>(layout);
287+
const button = element.renderRoot.querySelector('vaadin-button');
288+
const tooltip = button?.querySelector('foxy-i18n');
289+
290+
button?.click();
291+
292+
await waitUntil(
293+
async () => {
294+
await element.requestUpdate();
295+
return tooltip?.getAttribute('key') === 'click_to_copy';
296+
},
297+
undefined,
298+
{ timeout: 5000 }
299+
);
300+
301+
expect(tooltip).to.have.property('infer', '');
302+
expect(tooltip).to.have.property('key', 'click_to_copy');
303+
writeTextMethod.restore();
304+
});
305+
306+
it('propagates theme attribute to vaadin-button in text layout', async () => {
307+
const element = await fixture<CopyToClipboard>(html`
308+
<foxy-copy-to-clipboard layout="text" theme="foo"></foxy-copy-to-clipboard>
309+
`);
310+
311+
const button = element.renderRoot.querySelector('vaadin-button');
312+
expect(button).to.have.attribute('theme', 'foo');
313+
});
175314
});
Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import {
2-
CSSResult,
3-
LitElement,
4-
PropertyDeclarations,
5-
TemplateResult,
6-
css,
7-
html,
8-
} from 'lit-element';
1+
import type { CSSResult, PropertyDeclarations, TemplateResult } from 'lit-element';
92

3+
import { LitElement, css, html } from 'lit-element';
4+
import { TranslatableMixin } from '../../../mixins/translatable';
105
import { ConfigurableMixin } from '../../../mixins/configurable';
116
import { InferrableMixin } from '../../../mixins/inferrable';
12-
import { TranslatableMixin } from '../../../mixins/translatable';
7+
import { ifDefined } from 'lit-html/directives/if-defined';
138

14-
const Base = ConfigurableMixin(TranslatableMixin(InferrableMixin(LitElement), 'copy-to-clipboard'));
9+
const NS = 'copy-to-clipboard';
10+
const Base = ConfigurableMixin(TranslatableMixin(InferrableMixin(LitElement), NS));
1511

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

3331
static get styles(): CSSResult {
3432
return css`
35-
button {
33+
.icon-button {
3634
position: relative;
3735
appearance: none;
3836
background: none;
@@ -48,7 +46,7 @@ export class CopyToClipboard extends Base {
4846
align-items: center;
4947
}
5048
51-
button::before {
49+
.icon-button::before {
5250
position: absolute;
5351
inset: 0;
5452
content: ' ';
@@ -59,22 +57,22 @@ export class CopyToClipboard extends Base {
5957
border-radius: var(--lumo-border-radius-s);
6058
}
6159
62-
button:focus {
60+
.icon-button:focus {
6361
outline: none;
6462
box-shadow: 0 0 0 2px currentColor;
6563
}
6664
67-
button:disabled {
65+
.icon-button:disabled {
6866
opacity: 0.5;
6967
cursor: default;
7068
}
7169
7270
@media (hover: hover) {
73-
button:not(:disabled):hover {
71+
.icon-button:not(:disabled):hover {
7472
cursor: pointer;
7573
}
7674
77-
button:not(:disabled):hover::before {
75+
.icon-button:not(:disabled):hover::before {
7876
opacity: 0.16;
7977
}
8078
}
@@ -86,6 +84,12 @@ export class CopyToClipboard extends Base {
8684
`;
8785
}
8886

87+
/** Icon or text UI. Icon UI by default. */
88+
layout: 'text' | 'icon' | null = null;
89+
90+
/** VaadinButton theme for text layout. */
91+
theme: string | null = null;
92+
8993
/** Default icon. */
9094
icon: string | null = null;
9195

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

97101
render(): TemplateResult {
102+
const layout = this.layout === 'text' ? 'text' : 'icon';
98103
let label = '';
99104
let icon = '';
100105

@@ -113,26 +118,48 @@ export class CopyToClipboard extends Base {
113118
}
114119

115120
return html`
116-
<button
117-
id="trigger"
118-
?disabled=${this.disabled}
119-
@click=${() => {
120-
if (this.__state === 'idle') {
121-
this.__state = 'busy';
122-
123-
navigator.clipboard
124-
.writeText(this.text ?? '')
125-
.then(() => (this.__state = 'done'))
126-
.catch(() => (this.__state = 'fail'))
127-
.then(() => setTimeout(() => (this.__state = 'idle'), 2000));
128-
}
129-
}}
130-
>
131-
<iron-icon icon=${icon}></iron-icon>
132-
</button>
133-
<vcf-tooltip for="trigger" position="bottom">
134-
<span class="text-s"><foxy-i18n infer="" class="text-s" key=${label}></foxy-i18n></span>
135-
</vcf-tooltip>
121+
${layout === 'icon'
122+
? html`
123+
<button
124+
id="trigger"
125+
class="icon-button"
126+
?disabled=${this.disabled}
127+
@click=${this.__copy}
128+
>
129+
<iron-icon icon=${icon}></iron-icon>
130+
</button>
131+
<vcf-tooltip
132+
position="bottom"
133+
style="--lumo-base-color: black"
134+
theme="light"
135+
for="trigger"
136+
>
137+
<span class="text-s" style="color: white">
138+
<foxy-i18n infer="" key=${label}></foxy-i18n>
139+
</span>
140+
</vcf-tooltip>
141+
`
142+
: html`
143+
<vaadin-button
144+
theme=${ifDefined(this.theme ?? void 0)}
145+
?disabled=${this.disabled}
146+
@click=${this.__copy}
147+
>
148+
<foxy-i18n infer="" key=${label}></foxy-i18n>
149+
</vaadin-button>
150+
`}
136151
`;
137152
}
153+
154+
private __copy() {
155+
if (this.__state === 'idle') {
156+
this.__state = 'busy';
157+
158+
navigator.clipboard
159+
.writeText(this.text ?? '')
160+
.then(() => (this.__state = 'done'))
161+
.catch(() => (this.__state = 'fail'))
162+
.then(() => setTimeout(() => (this.__state = 'idle'), 2000));
163+
}
164+
}
138165
}

0 commit comments

Comments
 (0)