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 +});