Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clipboard): add new component sl-clipboard #1473

Merged
merged 6 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions docs/pages/components/clipboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
---
meta:
title: Clipboard
description: Enables you to save content into the clipboard providing visual feedback.
layout: component
---

```html:preview
<p>Clicking the clipboard button will put "shoelace rocks" into your clipboard</p>
<sl-clipboard value="shoelace rocks"></sl-clipboard>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<p>Clicking the clipboard button will put "shoelace rocks" into your clipboard</p>
<SlClipboard value="shoelace rocks"></SlClipboard>
</>
);
```

## Examples

### Use your own button

```html:preview
<sl-clipboard value="shoelace rocks">
<button type="button">Copy to clipboard</button>
<button slot="copied">Copied</button>
<button slot="error">Error</button>
</sl-clipboard>
<br>
<sl-clipboard value="shoelace rocks">
<sl-button>Copy</sl-button>
<sl-button slot="copied">Copied</sl-button>
<sl-button slot="error">Error</sl-button>
</sl-clipboard>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<SlClipboard value="shoelace rocks">
<button type="button">Copy to clipboard</button>
<div slot="copied">copied</div>
<button slot="error">Error</button>
</SlClipboard>
<SlClipboard value="shoelace rocks">
<sl-button>Copy</sl-button>
<sl-button slot="copied">Copied</sl-button>
<sl-button slot="error">Error</sl-button>
</SlClipboard>
</>
);
```

### Get the textValue from a different element

```html:preview
<div class="row">
<dl>
<dt>Phone Number</dt>
<dd id="phone-value">+1 234 456789</dd>
</dl>
<sl-clipboard for="phone-value"></sl-clipboard>
</div>

<style>
dl, .row {
display: flex;
margin: 0;
}
</style>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const css = `
dl, .row {
display: flex;
margin: 0;
}
`;

const App = () => (
<>
<div class="row">
<dl>
<dt>Phone Number</dt>
<dd id="phone-value">+1 234 456789</dd>
</dl>
<SlClipboard for="phone-value"></SlClipboard>
</div>

<style>{css}</style>
</>
);
```

### Copy an input/textarea or link

```html:preview
<input type="text" value="input rocks" id="input-rocks">
<sl-clipboard for="input-rocks"></sl-clipboard>
<br>

<textarea id="textarea-rocks">textarea
rocks</textarea>
<sl-clipboard for="textarea-rocks"></sl-clipboard>
<br>

<a href="https://shoelace.style/" id="link-rocks">Shoelace</a>
<sl-clipboard for="link-rocks"></sl-clipboard>
<br>

<sl-input value="sl-input rocks" id="sl-input-rocks"></sl-input>
<sl-clipboard for="sl-input-rocks"></sl-clipboard>
<br>

<sl-textarea value="sl-textarea rocks" id="sl-textarea-rocks"></sl-textarea>
<sl-clipboard for="sl-textarea-rocks"></sl-clipboard>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<input type="text" value="input rocks" id="input-rocks">
<SlClipboard for="input-rocks"></SlClipboard>
<br>
<textarea id="textarea-rocks">textarea
rocks</textarea>
<SlClipboard for="textarea-rocks"></SlClipboard>
<br>
<a href="https://shoelace.style/" id="link-rocks">Shoelace</a>
<SlClipboard for="input-rocks"></SlClipboard>
</>
);
```

### Error if copy fails

For example if a `for` target element is not found or if not using `https`.
An empty string value like `value=""` will also result in an error.

```html:preview
<sl-clipboard for="not-found"></sl-clipboard>
<br>
<sl-clipboard for="not-found">
<sl-button>Copy</sl-button>
<sl-button slot="copied">Copied</sl-button>
<sl-button slot="error">Error</sl-button>
</sl-clipboard>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<SlClipboard for="not-found"></SlClipboard>
<SlClipboard for="not-found">
<sl-button>Copy</sl-button>
<sl-button slot="copied">Copied</sl-button>
<sl-button slot="error">Error</sl-button>
</SlClipboard>
</>
);
```

### Change duration of reset to copy button

```html:preview
<sl-clipboard value="shoelace rocks" reset-timeout="500"></sl-clipboard>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<SlClipboard value="shoelace rocks" reset-timeout="500"></SlClipboard>
</>
);
```

### Supports Shadow Dom

```html:preview
<sl-copy-demo-el></sl-copy-demo-el>

<script>
customElements.define('sl-copy-demo-el', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}

connectedCallback() {
this.shadowRoot.innerHTML = `
<p id="copy-me">copy me (inside shadow root)</p>
<sl-clipboard for="copy-me"></sl-clipboard>
`;
}
});
</script>
```

```jsx:react
import { SlClipboard } from '@shoelace-style/shoelace/dist/react';

const App = () => (
<>
<sl-copy-demo-el></sl-copy-demo-el>
</>
);

customElements.define('sl-copy-demo-el', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}

connectedCallback() {
this.shadowRoot.innerHTML = `
<p id="copy-me">copy me (inside shadow root)</p>
<sl-clipboard for="copy-me"></sl-clipboard>
`;
}
});
```

## Disclaimer

The public API is partially inspired by https://github.com/github/clipboard-copy-element
116 changes: 116 additions & 0 deletions src/components/clipboard/clipboard.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIconButton from '../icon-button/icon-button.component.js';
import SlTooltip from '../tooltip/tooltip.component.js';
import styles from './clipboard.styles.js';
import type { CSSResultGroup } from 'lit';

/**
* @summary Enables you to save content into the clipboard providing visual feedback.
* @documentation https://shoelace.style/components/clipboard
* @status experimental
* @since 2.0
*
* @dependency sl-icon-button
* @dependency sl-tooltip
*
* @event sl-copying - Event when copying starts.
* @event sl-copied - Event when copying finished.
*
* @slot - The content that gets clicked to copy.
* @slot copied - The content shown after a successful copy.
* @slot error - The content shown if an error occurs.
*/
export default class SlClipboard extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static dependencies = { 'sl-tooltip': SlTooltip, 'sl-icon-button': SlIconButton };

/**
* Indicates the current status the copy action is in.
*/
@property({ type: String }) copyStatus: 'trigger' | 'copied' | 'error' = 'trigger';

/** Value to copy. */
@property({ type: String }) value = '';

/** Id of the element to copy the text value from. */
@property({ type: String }) for = '';

/** Duration in milliseconds to reset to the trigger state. */
@property({ type: Number, attribute: 'reset-timeout' }) resetTimeout = 2000;

private handleClick() {
if (this.copyStatus === 'copied') return;
this.copy();
claviska marked this conversation as resolved.
Show resolved Hide resolved
}

/** Copies the clipboard */
async copy() {
if (this.for) {
const root = this.getRootNode() as ShadowRoot | Document;
const target = 'getElementById' in root ? root.getElementById(this.for) : false;
if (target) {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
this.value = target.value;
} else if (target instanceof HTMLAnchorElement && target.hasAttribute('href')) {
this.value = target.href;
} else if ('value' in target) {
this.value = String(target.value);
} else {
this.value = target.textContent || '';
}
}
}
if (this.value) {
try {
this.emit('sl-copying');
await navigator.clipboard.writeText(this.value);
this.emit('sl-copied');
this.copyStatus = 'copied';
} catch (error) {
this.copyStatus = 'error';
}
} else {
this.copyStatus = 'error';
}

setTimeout(() => (this.copyStatus = 'trigger'), this.resetTimeout);
}

render() {
return html`
<div
part="base"
aria-live="polite"
class=${classMap({
clipboard: true,
[`clipboard--${this.copyStatus}`]: true
})}
>
<slot id="default" @click=${this.handleClick}>
<sl-tooltip content="Copy">
<sl-icon-button name="files" label="Copy"></sl-icon-button>
</sl-tooltip>
</slot>
<slot name="copied" @click=${this.handleClick}>
<sl-tooltip content="Copied">
<sl-icon-button class="green" name="file-earmark-check" label="Copied"></sl-icon-button>
</sl-tooltip>
</slot>
<slot name="error" @click=${this.handleClick}>
<sl-tooltip content="Failed to copy">
<sl-icon-button class="red" name="file-earmark-x" label="Failed to copy"></sl-icon-button>
</sl-tooltip>
</slot>
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'sl-clipboard': SlClipboard;
}
}
Loading