Skip to content

Commit 9620a66

Browse files
committed
Adds first attempt at uui-copy
1 parent 380f454 commit 9620a66

File tree

5 files changed

+323
-20
lines changed

5 files changed

+323
-20
lines changed

packages/uui-copy/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ import { UUICopyElement } from '@umbraco-ui/uui-copy';
2727
## Usage
2828

2929
```html
30-
<uui-copy></uui-copy>
30+
<uui-copy value="I am copied to the clipboard"></uui-copy>
3131
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { UUIEvent } from '@umbraco-ui/uui-base/lib/events';
2+
import { UUICopyElement } from './uui-copy.element';
3+
4+
export class UUICopyEvent extends UUIEvent<{ text: string }, UUICopyElement> {
5+
public static readonly COPIED: string = 'copied';
6+
public static readonly COPYING: string = 'copying';
7+
8+
constructor(evName: string, eventInit: any | null = {}) {
9+
super(evName, {
10+
...{ bubbles: true },
11+
...eventInit,
12+
});
13+
}
14+
}

packages/uui-copy/lib/uui-copy.element.ts

Lines changed: 137 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,152 @@
11
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
22
import { css, html, LitElement } from 'lit';
3+
import { property } from 'lit/decorators.js';
4+
import { UUIButtonElement } from '@umbraco-ui/uui-button/lib';
5+
import { UUICopyEvent } from './UUICopyEvent';
36

47
/**
8+
* @summary A button to trigger text content to be copied to the clipboard
9+
* Inspired by shoelace.style copy button
510
* @element uui-copy
11+
* @dependency uui-button
12+
* @dependancy uui-icon
13+
* @fires {UUICopyEvent} copying - Fires before the content is about to copied to the clipboard and can be used to transform or modify the data before its added to the clipboard
14+
* @fires {UUICopyEvent} copied - Fires when the content is copied to the clipboard
15+
* @slot - Use to replace the default content of 'Copy' and the copy icon
616
*/
717
@defineElement('uui-copy')
818
export class UUICopyElement extends LitElement {
9-
static styles = [
19+
/**
20+
* Set a string you wish to copy to the clipboard
21+
* @type {string}
22+
* @default ''
23+
*/
24+
@property({ type: String })
25+
value = '';
26+
27+
/**
28+
* Disables the button
29+
* @type {boolean}
30+
* @attr
31+
* @default false
32+
*/
33+
@property({ type: Boolean, reflect: true })
34+
disabled = false;
35+
36+
/**
37+
* Copies the text content from another element by specifying the ID of the element
38+
* The ID of the element does not need to start with # like a CSS selector
39+
* If this property is set, the value property is ignored
40+
* @type {string}
41+
* @attr
42+
* @default ''
43+
* @example copy-from="element-id"
44+
*/
45+
@property({ type: String, reflect: true, attribute: 'copy-from' })
46+
copyFrom = '';
47+
48+
/**
49+
* Changes the look of the button to one of the predefined, symbolic looks.
50+
* @type {"default" | "primary" | "secondary" | "outline" | "placeholder"}
51+
* @attr
52+
* @default "default"
53+
*/
54+
@property({ reflect: true })
55+
look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder' =
56+
'default';
57+
58+
/**
59+
* Changes the color of the button to one of the predefined, symbolic colors.
60+
* @type {"default" | "positive" | "warning" | "danger"}
61+
* @attr
62+
* @default "default"
63+
*/
64+
@property({ reflect: true })
65+
color: 'default' | 'positive' | 'warning' | 'danger' = 'default';
66+
67+
/**
68+
* Makes the left and right padding of the button narrower.
69+
* @type {boolean}
70+
* @attr
71+
* @default false
72+
*/
73+
@property({ type: Boolean, reflect: true })
74+
compact = false;
75+
76+
// Used to store the value that will be copied to the clipboard
77+
#valueToCopy = '';
78+
79+
#onClick = async (e: Event) => {
80+
const button = e.target as UUIButtonElement;
81+
button.state = 'waiting';
82+
83+
// By default use the value property
84+
this.#valueToCopy = this.value;
85+
86+
// If copy-from is set use that instead
87+
if (this.copyFrom) {
88+
// Try & find an element with the ID
89+
const el = document.getElementById(this.copyFrom);
90+
if (el) {
91+
console.log('Element found to copy from', el);
92+
this.#valueToCopy = el.textContent || el.innerText || '';
93+
94+
// Overrude the value to copy ,if the element has a value property
95+
// Such as uui-input or uui-textarea or native inout elements
96+
if ('value' in el) {
97+
console.log('This element has a value property', el);
98+
this.#valueToCopy = (el as any).value;
99+
}
100+
} else {
101+
console.error(`Element ID ${this.copyFrom} not found to copy from`);
102+
button.state = 'failed';
103+
return;
104+
}
105+
}
106+
107+
const beforeCopyEv = new UUICopyEvent(UUICopyEvent.COPYING, {
108+
detail: { text: this.#valueToCopy },
109+
});
110+
this.dispatchEvent(beforeCopyEv);
111+
112+
if (beforeCopyEv.detail.text != null) {
113+
this.#valueToCopy = beforeCopyEv.detail.text;
114+
}
115+
116+
await navigator.clipboard
117+
.writeText(this.#valueToCopy)
118+
.then(() => {
119+
button.state = 'success';
120+
this.dispatchEvent(
121+
new UUICopyEvent(UUICopyEvent.COPIED, {
122+
detail: { text: this.#valueToCopy },
123+
}),
124+
);
125+
})
126+
.catch(err => {
127+
button.state = 'failed';
128+
console.error('Error copying to clipboard', err);
129+
});
130+
};
131+
132+
render() {
133+
return html` <uui-button
134+
.color=${this.color}
135+
.look=${this.look}
136+
.disabled=${this.disabled}
137+
.compact=${this.compact}
138+
@click=${this.#onClick}>
139+
<slot> <uui-icon name="copy"></uui-icon> Copy </slot>
140+
</uui-button>`;
141+
}
142+
143+
static styles = [
10144
css`
11-
:host {
12-
/* Styles goes here */
145+
slot {
146+
pointer-events: none;
13147
}
14148
`,
15149
];
16-
17-
render(){
18-
return html`
19-
Markup goes here
20-
`;
21-
}
22150
}
23151

24152
declare global {
Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,187 @@
11
import type { Meta, StoryObj } from '@storybook/web-components';
2-
32
import './uui-copy.element';
43
import type { UUICopyElement } from './uui-copy.element';
54
import readme from '../README.md?raw';
5+
import { html } from 'lit';
6+
import { UUICopyEvent } from './UUICopyEvent';
67

78
const meta: Meta<UUICopyElement> = {
89
id: 'uui-copy',
9-
title: 'Copy',
10+
title: 'Inputs/Copy',
1011
component: 'uui-copy',
1112
parameters: {
1213
readme: { markdown: readme },
14+
},
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<UUICopyElement>;
19+
20+
export const Overview: Story = {
21+
name: 'Simple Copy',
22+
args: {
23+
value: 'Hey stop copying me 🥸',
24+
disabled: false,
25+
},
26+
parameters: {
1327
docs: {
1428
source: {
15-
code: `<uui-copy></uui-copy>`,
29+
code: `<uui-copy value="Hey stop copying me 🥸"></uui-copy>`,
1630
},
1731
},
1832
},
1933
};
2034

21-
export default meta;
22-
type Story = StoryObj<UUICopyElement>;
35+
export const Disabled: Story = {
36+
name: 'Disabled State',
37+
args: {
38+
value: 'You cannot copy this',
39+
disabled: true,
40+
},
41+
parameters: {
42+
docs: {
43+
source: {
44+
code: `<uui-copy value="You cannot copy this" disabled></uui-copy>`,
45+
},
46+
},
47+
},
48+
};
49+
50+
export const CustomSlotContent: Story = {
51+
name: 'Custom Slot Content',
52+
args: {
53+
value: 'Custom slot content',
54+
},
55+
render: args => html`
56+
<uui-copy .value=${args.value}> Custom Copy Text </uui-copy>
57+
`,
58+
parameters: {
59+
docs: {
60+
source: {
61+
code: `<uui-copy value="Custom slot content">Custom Copy Text</uui-copy>`,
62+
},
63+
},
64+
},
65+
};
2366

24-
export const Overview: Story = {};
67+
export const ColorAndLook: Story = {
68+
name: 'Color and Look',
69+
args: {
70+
value: 'Copy this text',
71+
color: 'positive',
72+
look: 'primary',
73+
},
74+
render: args => html`
75+
<uui-copy .value=${args.value} .color=${args.color} .look=${args.look}>
76+
<uui-icon name="copy"></uui-icon> Copy
77+
</uui-copy>
78+
`,
79+
parameters: {
80+
docs: {
81+
source: {
82+
code: `
83+
<uui-copy value="I have the same look and color props as UUI-Button" color="positive" look="primary"></uui-copy>
84+
`,
85+
},
86+
},
87+
},
88+
};
89+
90+
export const CopiedEvent: Story = {
91+
name: 'Copied Event',
92+
args: {
93+
value: 'Copy this text',
94+
},
95+
render: args => html`
96+
<uui-copy
97+
.value=${args.value}
98+
@copied=${(event: UUICopyEvent) => {
99+
alert(`Copied text: ${event.detail.text}`);
100+
}}></uui-copy>
101+
`,
102+
parameters: {
103+
docs: {
104+
source: {
105+
code: `
106+
<uui-copy value="Copy this text"></uui-copy>
107+
<script>
108+
document.querySelector('uui-copy').addEventListener('copied', (event) => {
109+
alert(\`Copied text: \${event.detail.text}\`);
110+
});
111+
</script>
112+
`,
113+
},
114+
},
115+
},
116+
};
117+
118+
export const ModifyClipboardContent: Story = {
119+
name: 'Modify Clipboard Content',
120+
args: {
121+
value: 'Original text',
122+
},
123+
render: args => html`
124+
<uui-copy
125+
.value=${args.value}
126+
@copying=${(event: UUICopyEvent) => {
127+
event.detail.text += ' - Modified before copying';
128+
}}>
129+
<uui-icon name="copy"></uui-icon> Copy
130+
</uui-copy>
131+
`,
132+
parameters: {
133+
docs: {
134+
source: {
135+
code: `
136+
<uui-copy value="Original text"></uui-copy>
137+
<script>
138+
document.querySelector('uui-copy').addEventListener('copying', (event) => {
139+
event.detail.text += ' - Modified before copying';
140+
});
141+
</script>
142+
`,
143+
},
144+
},
145+
},
146+
};
147+
148+
export const EmptyValueErrorState: Story = {
149+
name: 'Empty Value - shows an Error State',
150+
args: {
151+
value: '',
152+
},
153+
render: args => html` <uui-copy .value=${args.value}></uui-copy> `,
154+
parameters: {
155+
docs: {
156+
source: {
157+
code: `
158+
<uui-copy value=""></uui-copy>
159+
`,
160+
},
161+
},
162+
},
163+
};
164+
165+
export const CopyFromInput: Story = {
166+
name: 'Copy From uui-input',
167+
render: () => html`
168+
<uui-input id="inputToCopy" placeholder="Type something">
169+
<uui-copy copy-from="inputToCopy" slot="append" compact>
170+
<uui-icon name="copy"></uui-icon>
171+
</uui-copy>
172+
</uui-input>
173+
`,
174+
parameters: {
175+
docs: {
176+
source: {
177+
code: `
178+
<uui-input id="inputToCopy" placeholder="Type something">
179+
<uui-copy copy-from="inputToCopy" slot="append" compact>
180+
<uui-icon name="copy"></uui-icon>
181+
</uui-copy>
182+
</uui-input>
183+
`,
184+
},
185+
},
186+
},
187+
};

packages/uui-copy/lib/uui-copy.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ describe('UUICopyElement', () => {
55
let element: UUICopyElement;
66

77
beforeEach(async () => {
8-
element = await fixture(
9-
html` <uui-copy></uui-copy> `
10-
);
8+
element = await fixture(html` <uui-copy></uui-copy> `);
119
});
1210

1311
it('is defined with its own instance', () => {
@@ -17,4 +15,4 @@ describe('UUICopyElement', () => {
1715
it('passes the a11y audit', async () => {
1816
await expect(element).shadowDom.to.be.accessible();
1917
});
20-
});
18+
});

0 commit comments

Comments
 (0)