kopia lustrzana https://github.com/shoelace-style/shoelace
<sl-copy> (#1483)
* copy updates * Update docs/pages/components/copy.md Co-authored-by: Thomas Allmer <d4kmor@gmail.com> * unwrap and fix case * copy button updates * use bs icon * add parts, hoist, and improve parsing a bit * update docs * remove comment --------- Co-authored-by: Thomas Allmer <d4kmor@gmail.com>pull/1504/head
rodzic
458def7830
commit
c36df5ecc1
|
|
@ -0,0 +1,258 @@
|
|||
---
|
||||
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 in a tooltip. You can customize the labels using 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
|
||||
|
||||
A copy error will 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. When this happens, the `sl-error` event will be emitted.
|
||||
|
||||
This example demonstrates what happens when a copy error occurs. You can customize the error label and icon using the `error-label` attribute and the `error-icon` slot, respectively.
|
||||
|
||||
```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-icon slot="copy-icon" name="asterisk"></sl-icon>
|
||||
<sl-icon slot="success-icon" name="check-lg"></sl-icon>
|
||||
<sl-icon slot="error-icon" name="x-lg"></sl-icon>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
|
@ -645,9 +645,7 @@ When using sprite sheets, the `sl-load` and `sl-error` events will not fire.
|
|||
:::
|
||||
|
||||
:::danger
|
||||
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<sl-icon>` shadow dom and
|
||||
may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements.
|
||||
For this reason, sprite sheets should only be used if you're self-hosting them.
|
||||
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<sl-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
|
||||
:::
|
||||
|
||||
```html:preview
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
## Next
|
||||
|
||||
- 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,257 @@
|
|||
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.
|
||||
* @csspart copy-icon - The container that holds the copy icon.
|
||||
* @csspart success-icon - The container that holds the success icon.
|
||||
* @csspart error-icon - The container that holds the error icon.
|
||||
* @csspart tooltip__base - The tooltip's exported `base` part.
|
||||
* @csspart tooltip__base__popup - The tooltip's exported `popup` part.
|
||||
* @csspart tooltip__base__arrow - The tooltip's exported `arrow` part.
|
||||
* @csspart tooltip__body - The tooltip's exported `body` part.
|
||||
*
|
||||
* @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';
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the tooltip from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|hidden|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all,
|
||||
* scenarios.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
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('[') && this.from.includes(']');
|
||||
let id = this.from;
|
||||
let field = '';
|
||||
|
||||
if (isProperty) {
|
||||
// Split at the dot
|
||||
[id, field] = this.from.trim().split('.');
|
||||
} else if (isAttribute) {
|
||||
// Trim the ] and split at the [
|
||||
[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}
|
||||
?hoist=${this.hoist}
|
||||
exportparts="
|
||||
base:tooltip__base
|
||||
base__popup:tooltip__base__popup
|
||||
base__arrow:tooltip__base__arrow
|
||||
body:tooltip__body
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="copy-button__button"
|
||||
part="button"
|
||||
type="button"
|
||||
?disabled=${this.disabled}
|
||||
@click=${this.handleCopy}
|
||||
>
|
||||
<slot part="copy-icon" name="copy-icon">
|
||||
<sl-icon library="system" name="copy"></sl-icon>
|
||||
</slot>
|
||||
<slot part="success-icon" name="success-icon" hidden>
|
||||
<sl-icon library="system" name="check"></sl-icon>
|
||||
</slot>
|
||||
<slot part="error-icon" 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');
|
||||
|
|
@ -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 xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
|
||||
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
|
||||
</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"/>
|
||||
|
|
|
|||
|
|
@ -99,8 +99,6 @@ export default class SlTooltip extends ShoelaceElement {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
// TODO (justinfagnani): does this need to be done in firstUpdated for some
|
||||
// reason? If so, document why in a comment.
|
||||
this.addEventListener('blur', this.handleBlur, true);
|
||||
this.addEventListener('focus', this.handleFocus, true);
|
||||
this.addEventListener('click', this.handleClick);
|
||||
|
|
|
|||
|
|
@ -51,5 +51,6 @@ export default css`
|
|||
color: var(--sl-tooltip-color);
|
||||
padding: var(--sl-tooltip-padding);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -8,6 +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 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';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
type SlCopyEvent = CustomEvent<{ value: string }>;
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'sl-copy': SlCopyEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlCopyEvent;
|
||||
|
|
@ -13,6 +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 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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue