diff --git a/packages/uui-copy/README.md b/packages/uui-copy/README.md
index d8d052dc7..e85f16830 100644
--- a/packages/uui-copy/README.md
+++ b/packages/uui-copy/README.md
@@ -27,5 +27,5 @@ import { UUICopyElement } from '@umbraco-ui/uui-copy';
## Usage
```html
-
+
```
diff --git a/packages/uui-copy/lib/UUICopyEvent.ts b/packages/uui-copy/lib/UUICopyEvent.ts
new file mode 100644
index 000000000..7fc82da5e
--- /dev/null
+++ b/packages/uui-copy/lib/UUICopyEvent.ts
@@ -0,0 +1,14 @@
+import { UUIEvent } from '@umbraco-ui/uui-base/lib/events';
+import { UUICopyElement } from './uui-copy.element';
+
+export class UUICopyEvent extends UUIEvent<{ text: string }, UUICopyElement> {
+ public static readonly COPIED: string = 'copied';
+ public static readonly COPYING: string = 'copying';
+
+ constructor(evName: string, eventInit: any | null = {}) {
+ super(evName, {
+ ...{ bubbles: true },
+ ...eventInit,
+ });
+ }
+}
diff --git a/packages/uui-copy/lib/uui-copy.element.ts b/packages/uui-copy/lib/uui-copy.element.ts
index d33800714..304b478c4 100644
--- a/packages/uui-copy/lib/uui-copy.element.ts
+++ b/packages/uui-copy/lib/uui-copy.element.ts
@@ -1,24 +1,152 @@
import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { css, html, LitElement } from 'lit';
+import { property } from 'lit/decorators.js';
+import { UUIButtonElement } from '@umbraco-ui/uui-button/lib';
+import { UUICopyEvent } from './UUICopyEvent';
/**
+ * @summary A button to trigger text content to be copied to the clipboard
+ * Inspired by shoelace.style copy button
* @element uui-copy
+ * @dependency uui-button
+ * @dependancy uui-icon
+ * @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
+ * @fires {UUICopyEvent} copied - Fires when the content is copied to the clipboard
+ * @slot - Use to replace the default content of 'Copy' and the copy icon
*/
@defineElement('uui-copy')
export class UUICopyElement extends LitElement {
- static styles = [
+ /**
+ * Set a string you wish to copy to the clipboard
+ * @type {string}
+ * @default ''
+ */
+ @property({ type: String })
+ value = '';
+
+ /**
+ * Disables the button
+ * @type {boolean}
+ * @attr
+ * @default false
+ */
+ @property({ type: Boolean, reflect: true })
+ disabled = false;
+
+ /**
+ * Copies the text content from another element by specifying the ID of the element
+ * The ID of the element does not need to start with # like a CSS selector
+ * If this property is set, the value property is ignored
+ * @type {string}
+ * @attr
+ * @default ''
+ * @example copy-from="element-id"
+ */
+ @property({ type: String, reflect: true, attribute: 'copy-from' })
+ copyFrom = '';
+
+ /**
+ * Changes the look of the button to one of the predefined, symbolic looks.
+ * @type {"default" | "primary" | "secondary" | "outline" | "placeholder"}
+ * @attr
+ * @default "default"
+ */
+ @property({ reflect: true })
+ look: 'default' | 'primary' | 'secondary' | 'outline' | 'placeholder' =
+ 'default';
+
+ /**
+ * Changes the color of the button to one of the predefined, symbolic colors.
+ * @type {"default" | "positive" | "warning" | "danger"}
+ * @attr
+ * @default "default"
+ */
+ @property({ reflect: true })
+ color: 'default' | 'positive' | 'warning' | 'danger' = 'default';
+
+ /**
+ * Makes the left and right padding of the button narrower.
+ * @type {boolean}
+ * @attr
+ * @default false
+ */
+ @property({ type: Boolean, reflect: true })
+ compact = false;
+
+ // Used to store the value that will be copied to the clipboard
+ #valueToCopy = '';
+
+ #onClick = async (e: Event) => {
+ const button = e.target as UUIButtonElement;
+ button.state = 'waiting';
+
+ // By default use the value property
+ this.#valueToCopy = this.value;
+
+ // If copy-from is set use that instead
+ if (this.copyFrom) {
+ // Try & find an element with the ID
+ const el = document.getElementById(this.copyFrom);
+ if (el) {
+ console.log('Element found to copy from', el);
+ this.#valueToCopy = el.textContent || el.innerText || '';
+
+ // Overrude the value to copy ,if the element has a value property
+ // Such as uui-input or uui-textarea or native inout elements
+ if ('value' in el) {
+ console.log('This element has a value property', el);
+ this.#valueToCopy = (el as any).value;
+ }
+ } else {
+ console.error(`Element ID ${this.copyFrom} not found to copy from`);
+ button.state = 'failed';
+ return;
+ }
+ }
+
+ const beforeCopyEv = new UUICopyEvent(UUICopyEvent.COPYING, {
+ detail: { text: this.#valueToCopy },
+ });
+ this.dispatchEvent(beforeCopyEv);
+
+ if (beforeCopyEv.detail.text != null) {
+ this.#valueToCopy = beforeCopyEv.detail.text;
+ }
+
+ await navigator.clipboard
+ .writeText(this.#valueToCopy)
+ .then(() => {
+ button.state = 'success';
+ this.dispatchEvent(
+ new UUICopyEvent(UUICopyEvent.COPIED, {
+ detail: { text: this.#valueToCopy },
+ }),
+ );
+ })
+ .catch(err => {
+ button.state = 'failed';
+ console.error('Error copying to clipboard', err);
+ });
+ };
+
+ render() {
+ return html`
+ Copy
+ `;
+ }
+
+ static styles = [
css`
- :host {
- /* Styles goes here */
+ slot {
+ pointer-events: none;
}
`,
];
-
- render(){
- return html`
- Markup goes here
- `;
- }
}
declare global {
diff --git a/packages/uui-copy/lib/uui-copy.story.ts b/packages/uui-copy/lib/uui-copy.story.ts
index d8598b1ad..421c09fc1 100644
--- a/packages/uui-copy/lib/uui-copy.story.ts
+++ b/packages/uui-copy/lib/uui-copy.story.ts
@@ -1,24 +1,187 @@
import type { Meta, StoryObj } from '@storybook/web-components';
-
import './uui-copy.element';
import type { UUICopyElement } from './uui-copy.element';
import readme from '../README.md?raw';
+import { html } from 'lit';
+import { UUICopyEvent } from './UUICopyEvent';
const meta: Meta = {
id: 'uui-copy',
- title: 'Copy',
+ title: 'Inputs/Copy',
component: 'uui-copy',
parameters: {
readme: { markdown: readme },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Overview: Story = {
+ name: 'Simple Copy',
+ args: {
+ value: 'Hey stop copying me 🥸',
+ disabled: false,
+ },
+ parameters: {
docs: {
source: {
- code: ``,
+ code: ``,
},
},
},
};
-export default meta;
-type Story = StoryObj;
+export const Disabled: Story = {
+ name: 'Disabled State',
+ args: {
+ value: 'You cannot copy this',
+ disabled: true,
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: ``,
+ },
+ },
+ },
+};
+
+export const CustomSlotContent: Story = {
+ name: 'Custom Slot Content',
+ args: {
+ value: 'Custom slot content',
+ },
+ render: args => html`
+ Custom Copy Text
+ `,
+ parameters: {
+ docs: {
+ source: {
+ code: `Custom Copy Text`,
+ },
+ },
+ },
+};
-export const Overview: Story = {};
+export const ColorAndLook: Story = {
+ name: 'Color and Look',
+ args: {
+ value: 'Copy this text',
+ color: 'positive',
+ look: 'primary',
+ },
+ render: args => html`
+
+ Copy
+
+ `,
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ `,
+ },
+ },
+ },
+};
+
+export const CopiedEvent: Story = {
+ name: 'Copied Event',
+ args: {
+ value: 'Copy this text',
+ },
+ render: args => html`
+ {
+ alert(`Copied text: ${event.detail.text}`);
+ }}>
+ `,
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+
+ `,
+ },
+ },
+ },
+};
+
+export const ModifyClipboardContent: Story = {
+ name: 'Modify Clipboard Content',
+ args: {
+ value: 'Original text',
+ },
+ render: args => html`
+ {
+ event.detail.text += ' - Modified before copying';
+ }}>
+ Copy
+
+ `,
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+
+ `,
+ },
+ },
+ },
+};
+
+export const EmptyValueErrorState: Story = {
+ name: 'Empty Value - shows an Error State',
+ args: {
+ value: '',
+ },
+ render: args => html` `,
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ `,
+ },
+ },
+ },
+};
+
+export const CopyFromInput: Story = {
+ name: 'Copy From uui-input',
+ render: () => html`
+
+
+
+
+
+ `,
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+
+
+
+
+ `,
+ },
+ },
+ },
+};
diff --git a/packages/uui-copy/lib/uui-copy.test.ts b/packages/uui-copy/lib/uui-copy.test.ts
index cd83c94de..93b6008f6 100644
--- a/packages/uui-copy/lib/uui-copy.test.ts
+++ b/packages/uui-copy/lib/uui-copy.test.ts
@@ -5,9 +5,7 @@ describe('UUICopyElement', () => {
let element: UUICopyElement;
beforeEach(async () => {
- element = await fixture(
- html` `
- );
+ element = await fixture(html` `);
});
it('is defined with its own instance', () => {
@@ -17,4 +15,4 @@ describe('UUICopyElement', () => {
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible();
});
-});
\ No newline at end of file
+});