Skip to content

Commit

Permalink
using a single copyStatus
Browse files Browse the repository at this point in the history
  • Loading branch information
daKmoR committed Aug 1, 2023
1 parent 8e52111 commit a10fd62
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 46 deletions.
56 changes: 55 additions & 1 deletion docs/pages/components/clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ const App = () => (
```html:preview
<sl-clipboard value="shoelace rocks">
<button type="button">Copy to clipboard</button>
<button slot="copied">copied</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>
```

Expand All @@ -45,6 +47,12 @@ 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>
</>
);
Expand Down Expand Up @@ -94,6 +102,52 @@ const App = () => (
);
```

### 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>
</>
);
```

## Disclaimer

The public API is partially inspired by https://github.com/github/clipboard-copy-element
55 changes: 25 additions & 30 deletions src/components/clipboard/clipboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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, PropertyValueMap } from 'lit';
import type { CSSResultGroup } from 'lit';

/**
* @summary Enables you to save content into the clipboard providing visual feedback.
Expand All @@ -16,39 +16,38 @@ import type { CSSResultGroup, PropertyValueMap } from 'lit';
* @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 whether or not copied info is shown.
*/
@property({ type: Boolean, reflect: true }) copy = false;

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

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

/**
* The id of the element to copy the test value from.
*/
/** 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.copy || this.copyError) return;
this.__executeCopy();
if (this.copyStatus === 'copied') return;
this.copy();
}

private async __executeCopy() {
/** Copies the clipboard */
async copy() {
if (this.for) {
const target = document.getElementById(this.for)!;
if (target) {
Expand All @@ -57,21 +56,18 @@ export default class SlClipboard extends ShoelaceElement {
}
if (this.value) {
try {
this.emit('sl-copying');
await navigator.clipboard.writeText(this.value);
this.copy = true;
setTimeout(() => (this.copy = false), 2000);
this.emit('sl-copied');
this.copyStatus = 'copied';
} catch (error) {
this.copyError = true;
setTimeout(() => (this.copyError = false), 2000);
this.copyStatus = 'error';
}
} else {
this.copyStatus = 'error';
}
}

protected update(changedProperties: PropertyValueMap<SlClipboard> | Map<PropertyKey, unknown>): void {
super.update(changedProperties);
if (changedProperties.has('copy') && this.copy) {
this.__executeCopy();
}
setTimeout(() => (this.copyStatus = 'trigger'), this.resetTimeout);
}

render() {
Expand All @@ -80,8 +76,7 @@ export default class SlClipboard extends ShoelaceElement {
part="base"
class=${classMap({
clipboard: true,
'clipboard--copy': this.copy,
'clipboard--copy-error': this.copyError
[`clipboard--${this.copyStatus}`]: true
})}
>
<slot id="default" @click=${this.handleClick}>
Expand All @@ -94,7 +89,7 @@ export default class SlClipboard extends ShoelaceElement {
<sl-icon-button class="green" name="file-earmark-check" label="Copied"></sl-icon-button>
</sl-tooltip>
</slot>
<slot name="copy-error" @click=${this.handleClick}>
<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>
Expand Down
10 changes: 5 additions & 5 deletions src/components/clipboard/clipboard.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export default css`
slot[name='copied'] {
display: none;
}
.clipboard--copy #default {
.clipboard--copied #default {
display: none;
}
.clipboard--copy slot[name='copied'] {
.clipboard--copied slot[name='copied'] {
display: block;
}
Expand All @@ -31,13 +31,13 @@ export default css`
}
/* failed to copy */
slot[name='copy-error'] {
slot[name='error'] {
display: none;
}
.clipboard--copy-error #default {
.clipboard--error #default {
display: none;
}
.clipboard--copy-error slot[name='copy-error'] {
.clipboard--error slot[name='error'] {
display: block;
}
Expand Down
15 changes: 7 additions & 8 deletions src/components/clipboard/clipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ describe('<sl-clipboard>', () => {
await expect(el).to.be.accessible();
});

it('should initially not be in copy state', () => {
expect(el.copy).to.false;
it('should initially be in the trigger status', () => {
expect(el.copyStatus).to.equal('trigger');
});

it('should reset copy state after 2 seconds', async () => {
expect(el.copy).to.be.false;
el.copy = true;
await aTimeout(1000);
expect(el.copy).to.be.true;
await aTimeout(1100);
it('should reset copyStatus after 2 seconds', async () => {
expect(el.copy).to.be.false;
await el.copy();
expect(el.copyStatus).to.equal('copied');
await aTimeout(2100);
expect(el.copyStatus).to.equal('trigger');
});
});
});
4 changes: 2 additions & 2 deletions src/components/tree-item/tree-item.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SlCheckbox from '../checkbox/checkbox.component.js';
import SlIcon from '../icon/icon.component.js';
import SlSpinner from '../spinner/spinner.component.js';
import styles from './tree-item.styles.js';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import type { CSSResultGroup, PropertyValues } from 'lit';

/**
* @summary A tree item serves as a hierarchical node that lives inside a [tree](/components/tree).
Expand Down Expand Up @@ -139,7 +139,7 @@ export default class SlTreeItem extends ShoelaceElement {
this.isLeaf = !this.lazy && this.getChildrenItems().length === 0;
}

protected willUpdate(changedProperties: PropertyValueMap<SlTreeItem> | Map<PropertyKey, unknown>) {
protected willUpdate(changedProperties: PropertyValues<SlTreeItem> | Map<PropertyKey, unknown>) {
if (changedProperties.has('selected') && !changedProperties.has('indeterminate')) {
this.indeterminate = false;
}
Expand Down
2 changes: 2 additions & 0 deletions src/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type { default as SlChangeEvent } from './sl-change';
export type { default as SlClearEvent } from './sl-clear';
export type { default as SlCloseEvent } from './sl-close';
export type { default as SlCollapseEvent } from './sl-collapse';
export type { SlCopyingEvent } from './sl-copying';
export type { SlCopiedEvent } from './sl-copied';
export type { default as SlErrorEvent } from './sl-error';
export type { default as SlExpandEvent } from './sl-expand';
export type { default as SlFinishEvent } from './sl-finish';
Expand Down
7 changes: 7 additions & 0 deletions src/events/sl-copied.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type SlCopiedEvent = CustomEvent<Record<PropertyKey, never>>;

declare global {
interface GlobalEventHandlersEventMap {
'sl-copied': SlCopiedEvent;
}
}
7 changes: 7 additions & 0 deletions src/events/sl-copying.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type SlCopyingEvent = CustomEvent<Record<PropertyKey, never>>;

declare global {
interface GlobalEventHandlersEventMap {
'sl-copying': SlCopyingEvent;
}
}

0 comments on commit a10fd62

Please sign in to comment.