Skip to content

Commit

Permalink
Adds first attempt at uui-copy
Browse files Browse the repository at this point in the history
  • Loading branch information
warrenbuckley committed Dec 23, 2024
1 parent 380f454 commit 9620a66
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/uui-copy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ import { UUICopyElement } from '@umbraco-ui/uui-copy';
## Usage

```html
<uui-copy></uui-copy>
<uui-copy value="I am copied to the clipboard"></uui-copy>
```
14 changes: 14 additions & 0 deletions packages/uui-copy/lib/UUICopyEvent.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
146 changes: 137 additions & 9 deletions packages/uui-copy/lib/uui-copy.element.ts
Original file line number Diff line number Diff line change
@@ -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` <uui-button
.color=${this.color}
.look=${this.look}
.disabled=${this.disabled}
.compact=${this.compact}
@click=${this.#onClick}>
<slot> <uui-icon name="copy"></uui-icon> Copy </slot>
</uui-button>`;
}

static styles = [
css`
:host {
/* Styles goes here */
slot {
pointer-events: none;
}
`,
];

render(){
return html`
Markup goes here
`;
}
}

declare global {
Expand Down
175 changes: 169 additions & 6 deletions packages/uui-copy/lib/uui-copy.story.ts
Original file line number Diff line number Diff line change
@@ -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<UUICopyElement> = {
id: 'uui-copy',
title: 'Copy',
title: 'Inputs/Copy',
component: 'uui-copy',
parameters: {
readme: { markdown: readme },
},
};

export default meta;
type Story = StoryObj<UUICopyElement>;

export const Overview: Story = {
name: 'Simple Copy',
args: {
value: 'Hey stop copying me 🥸',
disabled: false,
},
parameters: {
docs: {
source: {
code: `<uui-copy></uui-copy>`,
code: `<uui-copy value="Hey stop copying me 🥸"></uui-copy>`,
},
},
},
};

export default meta;
type Story = StoryObj<UUICopyElement>;
export const Disabled: Story = {
name: 'Disabled State',
args: {
value: 'You cannot copy this',
disabled: true,
},
parameters: {
docs: {
source: {
code: `<uui-copy value="You cannot copy this" disabled></uui-copy>`,
},
},
},
};

export const CustomSlotContent: Story = {
name: 'Custom Slot Content',
args: {
value: 'Custom slot content',
},
render: args => html`
<uui-copy .value=${args.value}> Custom Copy Text </uui-copy>
`,
parameters: {
docs: {
source: {
code: `<uui-copy value="Custom slot content">Custom Copy Text</uui-copy>`,
},
},
},
};

export const Overview: Story = {};
export const ColorAndLook: Story = {
name: 'Color and Look',
args: {
value: 'Copy this text',
color: 'positive',
look: 'primary',
},
render: args => html`
<uui-copy .value=${args.value} .color=${args.color} .look=${args.look}>
<uui-icon name="copy"></uui-icon> Copy
</uui-copy>
`,
parameters: {
docs: {
source: {
code: `
<uui-copy value="I have the same look and color props as UUI-Button" color="positive" look="primary"></uui-copy>
`,
},
},
},
};

export const CopiedEvent: Story = {
name: 'Copied Event',
args: {
value: 'Copy this text',
},
render: args => html`
<uui-copy
.value=${args.value}
@copied=${(event: UUICopyEvent) => {
alert(`Copied text: ${event.detail.text}`);
}}></uui-copy>
`,
parameters: {
docs: {
source: {
code: `
<uui-copy value="Copy this text"></uui-copy>
<script>
document.querySelector('uui-copy').addEventListener('copied', (event) => {
alert(\`Copied text: \${event.detail.text}\`);
});
</script>
`,
},
},
},
};

export const ModifyClipboardContent: Story = {
name: 'Modify Clipboard Content',
args: {
value: 'Original text',
},
render: args => html`
<uui-copy
.value=${args.value}
@copying=${(event: UUICopyEvent) => {
event.detail.text += ' - Modified before copying';
}}>
<uui-icon name="copy"></uui-icon> Copy
</uui-copy>
`,
parameters: {
docs: {
source: {
code: `
<uui-copy value="Original text"></uui-copy>
<script>
document.querySelector('uui-copy').addEventListener('copying', (event) => {
event.detail.text += ' - Modified before copying';
});
</script>
`,
},
},
},
};

export const EmptyValueErrorState: Story = {
name: 'Empty Value - shows an Error State',
args: {
value: '',
},
render: args => html` <uui-copy .value=${args.value}></uui-copy> `,
parameters: {
docs: {
source: {
code: `
<uui-copy value=""></uui-copy>
`,
},
},
},
};

export const CopyFromInput: Story = {
name: 'Copy From uui-input',
render: () => html`
<uui-input id="inputToCopy" placeholder="Type something">
<uui-copy copy-from="inputToCopy" slot="append" compact>
<uui-icon name="copy"></uui-icon>
</uui-copy>
</uui-input>
`,
parameters: {
docs: {
source: {
code: `
<uui-input id="inputToCopy" placeholder="Type something">
<uui-copy copy-from="inputToCopy" slot="append" compact>
<uui-icon name="copy"></uui-icon>
</uui-copy>
</uui-input>
`,
},
},
},
};
6 changes: 2 additions & 4 deletions packages/uui-copy/lib/uui-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ describe('UUICopyElement', () => {
let element: UUICopyElement;

beforeEach(async () => {
element = await fixture(
html` <uui-copy></uui-copy> `
);
element = await fixture(html` <uui-copy></uui-copy> `);
});

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

0 comments on commit 9620a66

Please sign in to comment.