mirror of
https://github.com/shoelace-style/shoelace.git
synced 2026-01-12 02:59:13 +00:00
copy updates
This commit is contained in:
@@ -1,241 +0,0 @@
|
||||
---
|
||||
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
|
||||
156
docs/pages/components/copy.md
Normal file
156
docs/pages/components/copy.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
meta:
|
||||
title: Copy
|
||||
description: Copies data to the clipboard when the user clicks or taps the trigger.
|
||||
layout: component
|
||||
---
|
||||
|
||||
```html:preview
|
||||
<sl-copy value="Shoelace rocks!"></sl-copy>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopy } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlCopy value="Shoelace rocks!"></SlCopy>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Buttons
|
||||
|
||||
Use the default slot to customize the copy trigger. You can also customize the success and error messages using the respective slots.
|
||||
|
||||
```html:preview
|
||||
<sl-copy value="Copied from a custom button" class="custom-buttons">
|
||||
<sl-button>Copy</sl-button>
|
||||
<sl-button slot="success">Copied!</sl-button>
|
||||
<sl-button slot="error">Error</sl-button>
|
||||
</sl-copy>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlButton, SlCopy } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCopy value="Copied from a custom button">
|
||||
<SlButton>Copy</SlButton>
|
||||
<SlButton slot="success">Copied!</SlButton>
|
||||
<SlButton slot="error">Error</SlButton>
|
||||
</SlCopy>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Copying the Value From Other Elements
|
||||
|
||||
By default, the data to copy will come from the `value` attribute. You
|
||||
|
||||
```html:preview
|
||||
<span id="phone-number">+1 (234) 456-7890</span>
|
||||
<sl-copy from="phone-number"></sl-copy>
|
||||
|
||||
<br><br>
|
||||
|
||||
<sl-input type="text" value="Just an input" id="my-input" style="display: inline-block; max-width: 300px;"></sl-input>
|
||||
<sl-copy from="my-input"></sl-copy>
|
||||
|
||||
<br><br>
|
||||
|
||||
<a href="https://shoelace.style/" id="my-link">Shoelace Website</a>
|
||||
<sl-copy from="my-link"></sl-copy>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopy, SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<span id="phone-number">+1 (234) 456-7890</span>
|
||||
<SlCopy from="phone-number" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<SlInput type="text" value="Just an input" id="my-input" style="display: inline-block; max-width: 300px;" />
|
||||
<SlCopy from="my-input" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<a href="https://shoelace.style/" id="my-link">Shoelace Website</a>
|
||||
<SlCopy from="my-link" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Displaying Copy Errors
|
||||
|
||||
Copy errors can occur if the value is an empty string, if the `for` attribute points to an id that doesn't exist, or if the browser rejects the operation. You can customize the error that's shown by populating the `error` slot with your own content.
|
||||
|
||||
```html:preview
|
||||
<sl-copy from="not-found"></sl-copy>
|
||||
|
||||
<br><br>
|
||||
|
||||
<sl-copy from="not-found">
|
||||
<sl-button>Copy</sl-button>
|
||||
<sl-button slot="success">Copied</sl-button>
|
||||
<sl-button slot="error">Error</sl-button>
|
||||
</sl-copy>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopy } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCopy from="not-found"></SlCopy>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<SlCopy from="not-found">
|
||||
<sl-button>Copy</sl-button>
|
||||
<sl-button slot="success">Copied</sl-button>
|
||||
<sl-button slot="error">Error</sl-button>
|
||||
</SlCopy>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Showing Tooltips
|
||||
|
||||
You can wrap a tooltip around `<sl-copy>` to provide a hint to users.
|
||||
|
||||
```html:preview
|
||||
<sl-tooltip content="Copy to clipboard">
|
||||
<sl-copy value="Shoelace rocks!"></sl-copy>
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopy, SlTooltip } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlTooltip content="Copy to clipboard">
|
||||
<SlCopy value="Shoelace rocks!" />
|
||||
</SlTooltip>
|
||||
);
|
||||
```
|
||||
|
||||
### Changing Feedback Duration
|
||||
|
||||
A success indicator is briefly shown after copying. You can customize the length of time the indicator is shown using the `feedback-duration` attribute.
|
||||
|
||||
```html:preview
|
||||
<sl-copy value="Shoelace rocks!" feedback-duration="250"></sl-copy>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopy } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlCopy value="Shoelace rocks!" feedback-duration={250} />
|
||||
);
|
||||
```
|
||||
@@ -12,6 +12,10 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
|
||||
|
||||
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
|
||||
|
||||
## Next
|
||||
|
||||
- Added the `<sl-copy>` component [[#1473]]
|
||||
|
||||
## 2.6.0
|
||||
|
||||
- Added JSDoc comments to React Wrappers for better documentation when hovering a component. [#1450]
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* successful copy */
|
||||
slot[name='copied'] {
|
||||
display: none;
|
||||
}
|
||||
.clipboard--copied #default {
|
||||
display: none;
|
||||
}
|
||||
.clipboard--copied slot[name='copied'] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.green::part(base) {
|
||||
color: var(--sl-color-success-600);
|
||||
}
|
||||
.green::part(base):hover,
|
||||
.green::part(base):focus {
|
||||
color: var(--sl-color-success-600);
|
||||
}
|
||||
.green::part(base):active {
|
||||
color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
/* failed to copy */
|
||||
slot[name='error'] {
|
||||
display: none;
|
||||
}
|
||||
.clipboard--error #default {
|
||||
display: none;
|
||||
}
|
||||
.clipboard--error slot[name='error'] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.red::part(base) {
|
||||
color: var(--sl-color-danger-600);
|
||||
}
|
||||
.red::part(base):hover,
|
||||
.red::part(base):focus {
|
||||
color: var(--sl-color-danger-600);
|
||||
}
|
||||
.red::part(base):active {
|
||||
color: var(--sl-color-danger-600);
|
||||
}
|
||||
`;
|
||||
@@ -1,4 +0,0 @@
|
||||
import SlClipboard from './clipboard.component.js';
|
||||
export * from './clipboard.component.js';
|
||||
export default SlClipboard;
|
||||
SlClipboard.define('sl-clipboard');
|
||||
165
src/components/copy/copy.component.ts
Normal file
165
src/components/copy/copy.component.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIconButton from '../icon-button/icon-button.component.js';
|
||||
import styles from './copy.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Copies data to the clipboard when the user clicks or taps the trigger.
|
||||
* @documentation https://shoelace.style/components/copy
|
||||
* @status experimental
|
||||
* @since 2.7
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @event sl-copied - Emitted when the data has been copied.
|
||||
* @event sl-error - Emitted when the data could not be copied.
|
||||
*
|
||||
* @slot - A button that triggers copying.
|
||||
* @slot success - A button to briefly show when copying is successful.
|
||||
* @slot error - A button to briefly show when a copy error occurs.
|
||||
*
|
||||
* @animation copy.in - The animation to use when copy buttons animate in.
|
||||
* @animation copy.out - The animation to use when copy buttons animate out.
|
||||
*/
|
||||
export default class SlCopy extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static dependencies = {
|
||||
'sl-icon-button': SlIconButton
|
||||
};
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('slot[name="success"]') successSlot: HTMLSlotElement;
|
||||
@query('slot[name="error"]') errorSlot: HTMLSlotElement;
|
||||
|
||||
/** The text value to copy. */
|
||||
@property({ type: String }) value = '';
|
||||
|
||||
/** The length of time to show feedback before restoring the default trigger. */
|
||||
@property({ attribute: 'feedback-duration', type: Number }) feedbackDuration = 1000;
|
||||
|
||||
/**
|
||||
* An id that references an element in the same document from which data will be copied. If the element is a link, the
|
||||
* `href` will be copied. If the element is a form control or has a `value` property, its `value` will be copied.
|
||||
* Otherwise, the element's text content will be copied.
|
||||
*/
|
||||
@property({ type: String }) from = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('aria-live', 'polite');
|
||||
}
|
||||
|
||||
private async handleCopy() {
|
||||
// Copy the value by default
|
||||
let valueToCopy = this.value;
|
||||
|
||||
// If an element is specified, copy from that instead
|
||||
if (this.from) {
|
||||
const root = this.getRootNode() as ShadowRoot | Document;
|
||||
const target = 'getElementById' in root ? root.getElementById(this.from) : false;
|
||||
|
||||
if (target) {
|
||||
if (target instanceof HTMLAnchorElement && target.hasAttribute('href')) {
|
||||
valueToCopy = target.href;
|
||||
} else if ('value' in target) {
|
||||
valueToCopy = String(target.value);
|
||||
} else {
|
||||
valueToCopy = target.textContent || '';
|
||||
}
|
||||
} else {
|
||||
this.showStatus('error');
|
||||
this.emit('sl-error');
|
||||
}
|
||||
}
|
||||
|
||||
// Copy from the value property otherwise
|
||||
if (valueToCopy) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(valueToCopy);
|
||||
this.showStatus('success');
|
||||
this.emit('sl-copied');
|
||||
} catch (error) {
|
||||
this.showStatus('error');
|
||||
this.emit('sl-error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async showStatus(status: 'success' | 'error') {
|
||||
const target = status === 'success' ? this.successSlot : this.errorSlot;
|
||||
const showAnimation = getAnimation(this, 'copy.in', { dir: 'ltr' });
|
||||
const hideAnimation = getAnimation(this, 'copy.out', { dir: 'ltr' });
|
||||
|
||||
await this.defaultSlot.animate(hideAnimation.keyframes, hideAnimation.options).finished;
|
||||
this.defaultSlot.hidden = true;
|
||||
|
||||
target.hidden = false;
|
||||
await target.animate(showAnimation.keyframes, showAnimation.options).finished;
|
||||
|
||||
setTimeout(async () => {
|
||||
await target.animate(hideAnimation.keyframes, hideAnimation.options).finished;
|
||||
target.hidden = true;
|
||||
this.defaultSlot.hidden = false;
|
||||
this.defaultSlot.animate(showAnimation.keyframes, showAnimation.options);
|
||||
}, this.feedbackDuration);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<slot @click=${this.handleCopy}>
|
||||
<sl-icon-button
|
||||
library="system"
|
||||
name="copy"
|
||||
label=${this.localize.term('copy')}
|
||||
exportparts="base:icon-button__base"
|
||||
></sl-icon-button>
|
||||
</slot>
|
||||
|
||||
<slot name="success" hidden>
|
||||
<sl-icon-button
|
||||
library="system"
|
||||
name="check"
|
||||
label=${this.localize.term('copied')}
|
||||
exportparts="base:icon-button__base"
|
||||
></sl-icon-button>
|
||||
</slot>
|
||||
|
||||
<slot name="error" hidden>
|
||||
<sl-icon-button
|
||||
library="system"
|
||||
name="x-lg"
|
||||
label=${this.localize.term('error')}
|
||||
exportparts="base:icon-button__base"
|
||||
></sl-icon-button>
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('copy.in', {
|
||||
keyframes: [
|
||||
{ scale: '.25', opacity: '.25' },
|
||||
{ scale: '1', opacity: '1' }
|
||||
],
|
||||
options: { duration: 125 }
|
||||
});
|
||||
|
||||
setDefaultAnimation('copy.out', {
|
||||
keyframes: [
|
||||
{ scale: '1', opacity: '1' },
|
||||
{ scale: '.25', opacity: '0' }
|
||||
],
|
||||
options: { duration: 125 }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-copy': SlCopy;
|
||||
}
|
||||
}
|
||||
24
src/components/copy/copy.styles.ts
Normal file
24
src/components/copy/copy.styles.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
slot {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.copy {
|
||||
background: none;
|
||||
border: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
@@ -1,13 +1,13 @@
|
||||
import '../../../dist/shoelace.js';
|
||||
import { aTimeout, expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlClipboard from './clipboard.js';
|
||||
import type SlCopy from './copy.js';
|
||||
|
||||
describe('<sl-clipboard>', () => {
|
||||
let el: SlClipboard;
|
||||
describe('<sl-copy>', () => {
|
||||
let el: SlCopy;
|
||||
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlClipboard>(html`<sl-clipboard value="something"></sl-clipboard> `);
|
||||
el = await fixture<SlCopy>(html`<sl-copy value="something"></sl-copy> `);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
4
src/components/copy/copy.ts
Normal file
4
src/components/copy/copy.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import SlCopy from './copy.component.js';
|
||||
export * from './copy.component.js';
|
||||
export default SlCopy;
|
||||
SlCopy.define('sl-copy');
|
||||
@@ -16,7 +16,7 @@ const icons = {
|
||||
check: `
|
||||
<svg part="checked-icon" class="checkbox__icon" viewBox="0 0 16 16">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||
<g stroke="currentColor" stroke-width="2">
|
||||
<g stroke="currentColor">
|
||||
<g transform="translate(3.428571, 3.428571)">
|
||||
<path d="M0,5.71428571 L3.42857143,9.14285714"></path>
|
||||
<path d="M9.14285714,0 L3.42857143,9.14285714"></path>
|
||||
@@ -40,6 +40,11 @@ const icons = {
|
||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
`,
|
||||
copy: `
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 11.889c-.828 0-1.5-.697-1.5-1.556V2.556C2 1.696 2.672 1 3.5 1h5.25c.555 0 1.04.313 1.3.778M7.25 15h5.25c.828 0 1.5-.696 1.5-1.556V5.667c0-.86-.672-1.556-1.5-1.556H7.25c-.828 0-1.5.697-1.5 1.556v7.777c0 .86.672 1.556 1.5 1.556Z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
eye: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||
|
||||
@@ -8,8 +8,7 @@ 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 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';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export type SlCopiedEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
type SlCopiedEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copied': SlCopiedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCopiedEvent;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export type SlCopyingEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copying': SlCopyingEvent;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ export { default as SlCard } from './components/card/card.js';
|
||||
export { default as SlCarousel } from './components/carousel/carousel.js';
|
||||
export { default as SlCarouselItem } from './components/carousel-item/carousel-item.js';
|
||||
export { default as SlCheckbox } from './components/checkbox/checkbox.js';
|
||||
export { default as SlClipboard } from './components/clipboard/clipboard.js';
|
||||
export { default as SlColorPicker } from './components/color-picker/color-picker.js';
|
||||
export { default as SlCopy } from './components/copy/copy.js';
|
||||
export { default as SlDetails } from './components/details/details.js';
|
||||
export { default as SlDialog } from './components/dialog/dialog.js';
|
||||
export { default as SlDivider } from './components/divider/divider.js';
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Karrusel',
|
||||
clearEntry: 'Ryd indtastning',
|
||||
close: 'Luk',
|
||||
copied: 'Kopieret',
|
||||
copy: 'Kopier',
|
||||
currentValue: 'Nuværende værdi',
|
||||
error: 'Fejl',
|
||||
goToSlide: (slide, count) => `Gå til dias ${slide} af ${count}`,
|
||||
hidePassword: 'Skjul adgangskode',
|
||||
loading: 'Indlæser',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Karussell',
|
||||
clearEntry: 'Eingabe löschen',
|
||||
close: 'Schließen',
|
||||
copied: 'Kopiert',
|
||||
copy: 'Kopieren',
|
||||
currentValue: 'Aktueller Wert',
|
||||
error: 'Fehler',
|
||||
goToSlide: (slide, count) => `Gehen Sie zu Folie ${slide} von ${count}`,
|
||||
hidePassword: 'Passwort verbergen',
|
||||
loading: 'Wird geladen',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Carousel',
|
||||
clearEntry: 'Clear entry',
|
||||
close: 'Close',
|
||||
copied: 'Copied',
|
||||
copy: 'Copy',
|
||||
currentValue: 'Current value',
|
||||
error: 'Error',
|
||||
goToSlide: (slide, count) => `Go to slide ${slide} of ${count}`,
|
||||
hidePassword: 'Hide password',
|
||||
loading: 'Loading',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Carrusel',
|
||||
clearEntry: 'Borrar entrada',
|
||||
close: 'Cerrar',
|
||||
copied: 'Copiado',
|
||||
copy: 'Copiar',
|
||||
currentValue: 'Valor actual',
|
||||
error: 'Error',
|
||||
goToSlide: (slide, count) => `Ir a la diapositiva ${slide} de ${count}`,
|
||||
hidePassword: 'Ocultar contraseña',
|
||||
loading: 'Cargando',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'چرخ فلک',
|
||||
clearEntry: 'پاک کردن ورودی',
|
||||
close: 'بستن',
|
||||
copied: 'کپی شد',
|
||||
copy: 'رونوشت',
|
||||
currentValue: 'مقدار فعلی',
|
||||
error: 'خطا',
|
||||
goToSlide: (slide, count) => `رفتن به اسلاید ${slide} از ${count}`,
|
||||
hidePassword: 'پنهان کردن رمز',
|
||||
loading: 'بارگذاری',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Carrousel',
|
||||
clearEntry: `Effacer l'entrée`,
|
||||
close: 'Fermer',
|
||||
copied: 'Copié',
|
||||
copy: 'Copier',
|
||||
currentValue: 'Valeur actuelle',
|
||||
error: 'Erreur',
|
||||
goToSlide: (slide, count) => `Aller à la diapositive ${slide} de ${count}`,
|
||||
hidePassword: 'Masquer le mot de passe',
|
||||
loading: 'Chargement',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'קרוסלה',
|
||||
clearEntry: 'נקה קלט',
|
||||
close: 'סגור',
|
||||
copied: 'מוּעֲתָק',
|
||||
copy: 'העתק',
|
||||
currentValue: 'ערך נוכחי',
|
||||
error: 'שְׁגִיאָה',
|
||||
goToSlide: (slide, count) => `עבור לשקופית ${slide} של ${count}`,
|
||||
hidePassword: 'הסתר סיסמא',
|
||||
loading: 'טוען',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Körhinta',
|
||||
clearEntry: 'Bejegyzés törlése',
|
||||
close: 'Bezárás',
|
||||
copied: 'Másolva',
|
||||
copy: 'Másolás',
|
||||
currentValue: 'Aktuális érték',
|
||||
error: 'Hiba',
|
||||
goToSlide: (slide, count) => `Ugrás a ${count}/${slide}. diára`,
|
||||
hidePassword: 'Jelszó elrejtése',
|
||||
loading: 'Betöltés',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'カルーセル',
|
||||
clearEntry: 'クリアエントリ',
|
||||
close: '閉じる',
|
||||
copied: 'コピーされました',
|
||||
copy: 'コピー',
|
||||
currentValue: '現在の価値',
|
||||
error: 'エラー',
|
||||
goToSlide: (slide, count) => `${count} 枚中 ${slide} 枚のスライドに移動`,
|
||||
hidePassword: 'パスワードを隠す',
|
||||
loading: '読み込み中',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Carrousel',
|
||||
clearEntry: 'Invoer wissen',
|
||||
close: 'Sluiten',
|
||||
copied: 'Gekopieerd',
|
||||
copy: 'Kopiëren',
|
||||
currentValue: 'Huidige waarde',
|
||||
error: 'Fout',
|
||||
goToSlide: (slide, count) => `Ga naar slide ${slide} van ${count}`,
|
||||
hidePassword: 'Verberg wachtwoord',
|
||||
loading: 'Bezig met laden',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Karuzela',
|
||||
clearEntry: 'Wyczyść wpis',
|
||||
close: 'Zamknij',
|
||||
copied: 'Skopiowane',
|
||||
copy: 'Kopiuj',
|
||||
currentValue: 'Aktualna wartość',
|
||||
error: 'Błąd',
|
||||
goToSlide: (slide, count) => `Przejdź do slajdu ${slide} z ${count}`,
|
||||
hidePassword: 'Ukryj hasło',
|
||||
loading: 'Ładowanie',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Carrossel',
|
||||
clearEntry: 'Limpar entrada',
|
||||
close: 'Fechar',
|
||||
copied: 'Copiado',
|
||||
copy: 'Copiar',
|
||||
currentValue: 'Valor atual',
|
||||
error: 'Erro',
|
||||
goToSlide: (slide, count) => `Vá para o slide ${slide} de ${count}`,
|
||||
hidePassword: 'Esconder a senha',
|
||||
loading: 'Carregando',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Карусель',
|
||||
clearEntry: 'Очистить запись',
|
||||
close: 'Закрыть',
|
||||
copied: 'Скопировано',
|
||||
copy: 'Скопировать',
|
||||
currentValue: 'Текущее значение',
|
||||
error: 'Ошибка',
|
||||
goToSlide: (slide, count) => `Перейти к слайду ${slide} из ${count}`,
|
||||
hidePassword: 'Скрыть пароль',
|
||||
loading: 'Загрузка',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Karusell',
|
||||
clearEntry: 'Återställ val',
|
||||
close: 'Stäng',
|
||||
copied: 'Kopierade',
|
||||
copy: 'Kopiera',
|
||||
currentValue: 'Nuvarande värde',
|
||||
error: 'Fel',
|
||||
goToSlide: (slide, count) => `Gå till bild ${slide} av ${count}`,
|
||||
hidePassword: 'Dölj lösenord',
|
||||
loading: 'Läser in',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: 'Atlıkarınca',
|
||||
clearEntry: 'Girişi sil',
|
||||
close: 'Kapat',
|
||||
copied: 'Kopyalandı',
|
||||
copy: 'Kopya',
|
||||
currentValue: 'Mevcut değer',
|
||||
error: 'Hata',
|
||||
goToSlide: (slide, count) => `${count} slayttan ${slide} slayta gidin`,
|
||||
hidePassword: 'Şifreyi sakla',
|
||||
loading: 'Yükleme',
|
||||
|
||||
@@ -9,8 +9,10 @@ const translation: Translation = {
|
||||
carousel: '旋轉木馬',
|
||||
clearEntry: '清空',
|
||||
close: '關閉',
|
||||
copied: '已復制',
|
||||
copy: '複製',
|
||||
currentValue: '當前值',
|
||||
error: '錯誤',
|
||||
goToSlide: (slide, count) => `轉到第 ${slide} 張幻燈片,共 ${count} 張`,
|
||||
hidePassword: '隱藏密碼',
|
||||
loading: '載入中',
|
||||
|
||||
@@ -16,8 +16,10 @@ export interface Translation extends DefaultTranslation {
|
||||
carousel: string;
|
||||
clearEntry: string;
|
||||
close: string;
|
||||
copied: string;
|
||||
copy: string;
|
||||
currentValue: string;
|
||||
error: string;
|
||||
goToSlide: (slide: number, count: number) => string;
|
||||
hidePassword: string;
|
||||
loading: string;
|
||||
|
||||
Reference in New Issue
Block a user