kopia lustrzana https://github.com/shoelace-style/shoelace
copy button updates
rodzic
d6f9618814
commit
2e6133d4d8
|
@ -0,0 +1,252 @@
|
|||
---
|
||||
meta:
|
||||
title: Copy Button
|
||||
description: Copies data to the clipboard when the user clicks the button.
|
||||
layout: component
|
||||
---
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button value="Shoelace rocks!"></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const App = () => (
|
||||
<SlCopyButton value="Shoelace rocks!" />
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Labels
|
||||
|
||||
Copy Buttons display feedback labels in a tooltip. You can change the labels by setting the `copy-label`, `success-label`, and `error-label` attributes.
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button
|
||||
value="Custom labels are easy"
|
||||
copy-label="Click to copy"
|
||||
success-label="You did it!"
|
||||
error-label="Whoops, your browser doesn't support this!"
|
||||
></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const App = () => (
|
||||
<SlCopyButton
|
||||
value="Custom labels are easy"
|
||||
copy-label="Click to copy"
|
||||
success-label="You did it!"
|
||||
error-label="Whoops, your browser doesn't support this!"
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Icons
|
||||
|
||||
Use the `copy-icon`, `success-icon`, and `error-icon` slots to customize the icons that get displayed for each state. You can use [`<sl-icon>`](/components/icon) or your own images.
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button value="Copied from a custom button">
|
||||
<sl-icon slot="copy-icon" name="clipboard"></sl-icon>
|
||||
<sl-icon slot="success-icon" name="clipboard-check"></sl-icon>
|
||||
<sl-icon slot="error-icon" name="clipboard-x"></sl-icon>
|
||||
</sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
import { SlIcon } from '@shoelace-style/shoelace/dist/react/sl-icon';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCopyButton value="Copied from a custom button">
|
||||
<SlIcon slot="copy-icon" name="clipboard" />
|
||||
<SlIcon slot="success-icon" name="clipboard-check" />
|
||||
<SlIcon slot="error-icon" name="clipboard-x" />
|
||||
</SlCopyButton>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Copying Values From Other Elements
|
||||
|
||||
Normally, the data that gets copied will come from the component's `value` attribute, but you can copy data from any element within the same document by providing its `id` to the `from` attribute.
|
||||
|
||||
When using the `from` attribute, the element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) will be copied by default. Passing an attribute or property modifier will let you copy data from one of the element's attributes or properties instead.
|
||||
|
||||
To copy data from an attribute, use `from="id[attr]"` where `id` is the id of the target element and `attr` is the name of the attribute you'd like to copy. To copy data from a property, use `from="id.prop"` where `id` is the id of the target element and `prop` is the name of the property you'd like to copy.
|
||||
|
||||
```html:preview
|
||||
<!-- Copies the span's textContent -->
|
||||
<span id="my-phone">+1 (234) 456-7890</span>
|
||||
<sl-copy-button from="my-phone"></sl-copy-button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- Copies the input's "value" property -->
|
||||
<sl-input id="my-input" type="text" value="User input" style="display: inline-block; max-width: 300px;"></sl-input>
|
||||
<sl-copy-button from="my-input.value"></sl-copy-button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<!-- Copies the link's "href" attribute -->
|
||||
<a id="my-link" href="https://shoelace.style/">Shoelace Website</a>
|
||||
<sl-copy-button from="my-link[href]"></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react/sl-input';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
{/* Copies the span's textContent */}
|
||||
<span id="my-phone">+1 (234) 456-7890</span>
|
||||
<SlCopyButton from="my-phone" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* Copies the input's "value" property */}
|
||||
<SlInput id="my-input" type="text" />
|
||||
<SlCopyButton from="my-input.value" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* Copies the link's "href" attribute */}
|
||||
<a id="my-link" href="https://shoelace.style/">Shoelace Website</a>
|
||||
<SlCopyButton from="my-link[href]" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Handling Errors
|
||||
|
||||
Copy errors occur if the value is an empty string, if the `from` attribute points to an id that doesn't exist, or if the browser rejects the operation for any reason. This example shows what happens when a copy error occurs.
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button from="i-do-not-exist"></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const App = () => (
|
||||
<SlCopyButton from="i-do-not-exist" />
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Copy buttons can be disabled by adding the `disabled` attribute.
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button value="You can't copy me" disabled></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const App = () => (
|
||||
<SlCopyButton value="You can't copy me" disabled />
|
||||
);
|
||||
```
|
||||
|
||||
### 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-button value="Shoelace rocks!" feedback-duration="250"></sl-copy-button>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const App = () => (
|
||||
<SlCopyButton value="Shoelace rocks!" feedback-duration={250} />
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Styles
|
||||
|
||||
You can customize the button to your liking with CSS.
|
||||
|
||||
```html:preview
|
||||
<sl-copy-button value="I'm so stylish" class="custom-styles"></sl-copy-button>
|
||||
|
||||
<style>
|
||||
.custom-styles {
|
||||
--success-color: white;
|
||||
--error-color: white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-styles::part(button) {
|
||||
background-color: #ff1493;
|
||||
border: solid 4px #ff7ac1;
|
||||
border-right-color: #ad005c;
|
||||
border-bottom-color: #ad005c;
|
||||
border-radius: 0;
|
||||
transition: 100ms scale ease-in-out, 100ms translate ease-in-out;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):hover {
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):active {
|
||||
translate: 0 2px;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):focus-visible {
|
||||
outline: dashed 2px deeppink;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx:react
|
||||
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/sl-copy-button';
|
||||
|
||||
const css = `
|
||||
.custom-styles {
|
||||
--success-color: white;
|
||||
--error-color: white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-styles::part(button) {
|
||||
background-color: #ff1493;
|
||||
border: solid 4px #ff7ac1;
|
||||
border-right-color: #ad005c;
|
||||
border-bottom-color: #ad005c;
|
||||
border-radius: 0;
|
||||
transition: 100ms scale ease-in-out, 100ms translate ease-in-out;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):hover {
|
||||
scale: 1.1;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):active {
|
||||
translate: 0 2px;
|
||||
}
|
||||
|
||||
.custom-styles::part(button):focus-visible {
|
||||
outline: dashed 2px deeppink;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCopyButton value="I'm so stylish" className="custom-styles" />
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
|
@ -1,156 +0,0 @@
|
|||
---
|
||||
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 `from` 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} />
|
||||
);
|
||||
```
|
|
@ -14,9 +14,10 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
## Next
|
||||
|
||||
- Added the `<sl-copy>` component [#1473]
|
||||
- Added the `<sl-copy-button>` component [#1473]
|
||||
- Fixed a bug in `<sl-dropdown>` where pressing [[Up]] or [[Down]] when focused on the trigger wouldn't focus the first/last menu items [#1472]
|
||||
- Improved the behavior of the clear button in `<sl-input>` to prevent the component's width from shifting when toggled [#1496]
|
||||
- Improved `<sl-tooltip>` to prevent user selection so the tooltip doesn't get highlighted when dragging selections
|
||||
- Removed `sideEffects` key from `package.json`. Update React docs to use cherry-picking. [#1485]
|
||||
- Updated Bootstrap Icons to 1.10.5
|
||||
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||
import SlIcon from '../icon/icon.component.js';
|
||||
import SlTooltip from '../tooltip/tooltip.component.js';
|
||||
import styles from './copy-button.styles.js';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Copies text data to the clipboard when the user clicks the trigger.
|
||||
* @documentation https://shoelace.style/components/copy
|
||||
* @status experimental
|
||||
* @since 2.7
|
||||
*
|
||||
* @dependency sl-icon
|
||||
* @dependency sl-tooltip
|
||||
*
|
||||
* @event sl-copy - Emitted when the data has been copied.
|
||||
* @event sl-error - Emitted when the data could not be copied.
|
||||
*
|
||||
* @slot copy-icon - The icon to show in the default copy state. Works best with `<sl-icon>`.
|
||||
* @slot success-icon - The icon to show when the content is copied. Works best with `<sl-icon>`.
|
||||
* @slot error-icon - The icon to show when a copy error occurs. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @csspart button - The internal `<button>` element.
|
||||
*
|
||||
* @cssproperty --success-color - The color to use for success feedback.
|
||||
* @cssproperty --error-color - The color to use for error feedback.
|
||||
*
|
||||
* @animation copy.in - The animation to use when feedback icons animate in.
|
||||
* @animation copy.out - The animation to use when feedback icons animate out.
|
||||
*/
|
||||
export default class SlCopyButton extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static dependencies = {
|
||||
'sl-icon': SlIcon,
|
||||
'sl-tooltip': SlTooltip
|
||||
};
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('slot[name="copy-icon"]') copyIcon: HTMLSlotElement;
|
||||
@query('slot[name="success-icon"]') successIcon: HTMLSlotElement;
|
||||
@query('slot[name="error-icon"]') errorIcon: HTMLSlotElement;
|
||||
@query('sl-tooltip') tooltip: SlTooltip;
|
||||
|
||||
@state() isCopying = false;
|
||||
@state() status: 'rest' | 'success' | 'error' = 'rest';
|
||||
|
||||
/** The text value to copy. */
|
||||
@property() value = '';
|
||||
|
||||
/**
|
||||
* An id that references an element in the same document from which data will be copied. If both this and `value` are
|
||||
* present, this value will take precedence. By default, the target element's `textContent` will be copied. To copy an
|
||||
* attribute, append the attribute name wrapped in square brackets, e.g. `from="el[value]"`. To copy a property,
|
||||
* append a dot and the property name, e.g. `from="el.value"`.
|
||||
*/
|
||||
@property() from = '';
|
||||
|
||||
/** Disables the copy button. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** A custom label to show in the tooltip. */
|
||||
@property({ attribute: 'copy-label' }) copyLabel = '';
|
||||
|
||||
/** A custom label to show in the tooltip after copying. */
|
||||
@property({ attribute: 'success-label' }) successLabel = '';
|
||||
|
||||
/** A custom label to show in the tooltip when a copy error occurs. */
|
||||
@property({ attribute: 'error-label' }) errorLabel = '';
|
||||
|
||||
/** The length of time to show feedback before restoring the default trigger. */
|
||||
@property({ attribute: 'feedback-duration', type: Number }) feedbackDuration = 1000;
|
||||
|
||||
/** The preferred placement of the tooltip. */
|
||||
@property({ attribute: 'tooltip-placement' }) tooltipPlacement: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||
|
||||
private async handleCopy() {
|
||||
if (this.disabled || this.isCopying) {
|
||||
return;
|
||||
}
|
||||
this.isCopying = true;
|
||||
|
||||
// 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;
|
||||
|
||||
// Simple way to parse ids, properties, and attributes
|
||||
const isProperty = this.from.includes('.');
|
||||
const isAttribute = this.from.includes('[');
|
||||
let id = this.from;
|
||||
let field = '';
|
||||
|
||||
if (isProperty) {
|
||||
[id, field] = this.from.trim().split('.');
|
||||
} else if (isAttribute) {
|
||||
[id, field] = this.from.trim().replace(/\]$/, '').split('[');
|
||||
}
|
||||
|
||||
// Locate the target element by id
|
||||
const target = 'getElementById' in root ? root.getElementById(id) : null;
|
||||
|
||||
if (target) {
|
||||
if (isAttribute) {
|
||||
valueToCopy = target.getAttribute(field) || '';
|
||||
} else if (isProperty) {
|
||||
// @ts-expect-error - deal with it
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
valueToCopy = target[field] || '';
|
||||
} else {
|
||||
valueToCopy = target.textContent || '';
|
||||
}
|
||||
} else {
|
||||
// No target
|
||||
this.showStatus('error');
|
||||
this.emit('sl-error');
|
||||
}
|
||||
}
|
||||
|
||||
// No value
|
||||
if (!valueToCopy) {
|
||||
this.showStatus('error');
|
||||
this.emit('sl-error');
|
||||
} else {
|
||||
try {
|
||||
await navigator.clipboard.writeText(valueToCopy);
|
||||
this.showStatus('success');
|
||||
this.emit('sl-copy', {
|
||||
detail: {
|
||||
value: valueToCopy
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Rejected by browser
|
||||
this.showStatus('error');
|
||||
this.emit('sl-error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async showStatus(status: 'success' | 'error') {
|
||||
const copyLabel = this.copyLabel || this.localize.term('copy');
|
||||
const successLabel = this.successLabel || this.localize.term('copied');
|
||||
const errorLabel = this.errorLabel || this.localize.term('error');
|
||||
const iconToShow = status === 'success' ? this.successIcon : this.errorIcon;
|
||||
const showAnimation = getAnimation(this, 'copy.in', { dir: 'ltr' });
|
||||
const hideAnimation = getAnimation(this, 'copy.out', { dir: 'ltr' });
|
||||
|
||||
this.tooltip.content = status === 'success' ? successLabel : errorLabel;
|
||||
|
||||
// Show the feedback icon
|
||||
await this.copyIcon.animate(hideAnimation.keyframes, hideAnimation.options).finished;
|
||||
this.copyIcon.hidden = true;
|
||||
this.status = status;
|
||||
iconToShow.hidden = false;
|
||||
await iconToShow.animate(showAnimation.keyframes, showAnimation.options).finished;
|
||||
|
||||
// After a brief delay, restore the original state
|
||||
setTimeout(async () => {
|
||||
await iconToShow.animate(hideAnimation.keyframes, hideAnimation.options).finished;
|
||||
iconToShow.hidden = true;
|
||||
this.status = 'rest';
|
||||
this.copyIcon.hidden = false;
|
||||
await this.copyIcon.animate(showAnimation.keyframes, showAnimation.options).finished;
|
||||
|
||||
this.tooltip.content = copyLabel;
|
||||
this.isCopying = false;
|
||||
}, this.feedbackDuration);
|
||||
}
|
||||
|
||||
render() {
|
||||
const copyLabel = this.copyLabel || this.localize.term('copy');
|
||||
|
||||
return html`
|
||||
<sl-tooltip
|
||||
class=${classMap({
|
||||
'copy-button': true,
|
||||
'copy-button--success': this.status === 'success',
|
||||
'copy-button--error': this.status === 'error'
|
||||
})}
|
||||
content=${copyLabel}
|
||||
placement=${this.tooltipPlacement}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
<button
|
||||
class="copy-button__button"
|
||||
part="button"
|
||||
type="button"
|
||||
?disabled=${this.disabled}
|
||||
@click=${this.handleCopy}
|
||||
>
|
||||
<slot name="copy-icon">
|
||||
<sl-icon library="system" name="copy"></sl-icon>
|
||||
</slot>
|
||||
<slot name="success-icon" hidden>
|
||||
<sl-icon library="system" name="check"></sl-icon>
|
||||
</slot>
|
||||
<slot name="error-icon" hidden>
|
||||
<sl-icon library="system" name="x-lg"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
</sl-tooltip>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('copy.in', {
|
||||
keyframes: [
|
||||
{ scale: '.25', opacity: '.25' },
|
||||
{ scale: '1', opacity: '1' }
|
||||
],
|
||||
options: { duration: 100 }
|
||||
});
|
||||
|
||||
setDefaultAnimation('copy.out', {
|
||||
keyframes: [
|
||||
{ scale: '1', opacity: '1' },
|
||||
{ scale: '.25', opacity: '0' }
|
||||
],
|
||||
options: { duration: 100 }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-copy-button': SlCopyButton;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles.js';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--error-color: var(--sl-color-danger-600);
|
||||
--success-color: var(--sl-color-success-600);
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.copy-button__button {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
padding: var(--sl-spacing-x-small);
|
||||
cursor: pointer;
|
||||
transition: var(--sl-transition-x-fast) color;
|
||||
}
|
||||
|
||||
.copy-button--success .copy-button__button {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.copy-button--error .copy-button__button {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.copy-button__button:focus-visible {
|
||||
outline: var(--sl-focus-ring);
|
||||
outline-offset: var(--sl-focus-ring-offset);
|
||||
}
|
||||
|
||||
.copy-button__button[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
slot {
|
||||
display: inline-flex;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,20 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlCopyButton from './copy-button.js';
|
||||
|
||||
// We use aria-live to announce labels via tooltips
|
||||
const ignoredRules = ['button-name'];
|
||||
|
||||
describe('<sl-copy-button>', () => {
|
||||
let el: SlCopyButton;
|
||||
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture(html`<sl-copy-button value="something"></sl-copy-button> `);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
import SlCopyButton from './copy-button.component.js';
|
||||
export * from './copy-button.component.js';
|
||||
export default SlCopyButton;
|
||||
SlCopyButton.define('sl-copy-button');
|
|
@ -1,165 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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,29 +0,0 @@
|
|||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlCopy from './copy.js';
|
||||
|
||||
describe('<sl-copy>', () => {
|
||||
let el: SlCopy;
|
||||
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCopy>(html`<sl-copy value="something"></sl-copy> `);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
// it('should initially be in the trigger status', () => {
|
||||
// expect(el.copyStatus).to.equal('trigger');
|
||||
// });
|
||||
|
||||
// it('should reset copyStatus after 2 seconds', async () => {
|
||||
// expect(el.copyStatus).to.equal('trigger');
|
||||
// await el.copy(); // this will result in an error as copy needs to always be called from a user action
|
||||
// expect(el.copyStatus).to.equal('error');
|
||||
// await aTimeout(2100);
|
||||
// expect(el.copyStatus).to.equal('trigger');
|
||||
// });
|
||||
});
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
import SlCopy from './copy.component.js';
|
||||
export * from './copy.component.js';
|
||||
export default SlCopy;
|
||||
SlCopy.define('sl-copy');
|
|
@ -41,8 +41,8 @@ const icons = {
|
|||
</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 xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<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" />
|
||||
</svg>
|
||||
`,
|
||||
eye: `
|
||||
|
|
|
@ -51,5 +51,6 @@ export default css`
|
|||
color: var(--sl-tooltip-color);
|
||||
padding: var(--sl-tooltip-padding);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -8,7 +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 { default as SlCopiedEvent } from './sl-copied';
|
||||
export type { default as SlCopyEvent } from './sl-copy';
|
||||
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,9 +0,0 @@
|
|||
type SlCopiedEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copied': SlCopiedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCopiedEvent;
|
|
@ -0,0 +1,9 @@
|
|||
type SlCopyEvent = CustomEvent<{ value: string }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copy': SlCopyEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCopyEvent;
|
|
@ -13,7 +13,7 @@ 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 SlColorPicker } from './components/color-picker/color-picker.js';
|
||||
export { default as SlCopy } from './components/copy/copy.js';
|
||||
export { default as SlCopyButton } from './components/copy-button/copy-button.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';
|
||||
|
|
Ładowanie…
Reference in New Issue