From 64541fa96f1333971506ef806b481e73337c871a Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Sun, 30 Jul 2023 23:51:00 +0200 Subject: [PATCH 1/6] feat(clipboard): add new component sl-clipboard --- docs/pages/components/clipboard.md | 72 +++++++++++++++ .../clipboard/clipboard.component.ts | 89 +++++++++++++++++++ src/components/clipboard/clipboard.styles.ts | 23 +++++ src/components/clipboard/clipboard.test.ts | 30 +++++++ src/components/clipboard/clipboard.ts | 4 + src/shoelace.ts | 1 + 6 files changed, 219 insertions(+) create mode 100644 docs/pages/components/clipboard.md create mode 100644 src/components/clipboard/clipboard.component.ts create mode 100644 src/components/clipboard/clipboard.styles.ts create mode 100644 src/components/clipboard/clipboard.test.ts create mode 100644 src/components/clipboard/clipboard.ts diff --git a/docs/pages/components/clipboard.md b/docs/pages/components/clipboard.md new file mode 100644 index 0000000000..2ba8ce069d --- /dev/null +++ b/docs/pages/components/clipboard.md @@ -0,0 +1,72 @@ +--- +meta: + title: Clipboard + description: Enables you to save content into the clipboard providing visual feedback. +layout: component +--- + +```html:preview +
+
+
Phone Number
+
+1 234 456789
+
+ +
+ + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const css = ` + dl, .row { + display: flex; + margin: 0; + } +`; + +const App = () => ( + <> +
+
+
Phone Number
+
+1 234 456789
+
+ +
+ + + +); +``` + +## Examples + +### Providing directly a text value + +```html:preview +

Clicking the clipboard button will put "shoelace rocks" into your clipboard

+ +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> +

Clicking the clipboard button will put "shoelace rocks" into your clipboard

+ + +); +``` + +## Disclaimer + +The public API is partially inspired by https://github.com/github/clipboard-copy-element diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts new file mode 100644 index 0000000000..7c32da0790 --- /dev/null +++ b/src/components/clipboard/clipboard.component.ts @@ -0,0 +1,89 @@ +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, PropertyValueMap } 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 + */ +export default class SlClipboard extends ShoelaceElement { + static styles: CSSResultGroup = styles; + static dependencies = { 'sl-tooltip': SlTooltip, 'sl-icon-button': SlIconButton }; + + /** + * Indicates whether or not copy info is shown. + */ + @property({ type: Boolean, reflect: true }) copy = false; + + /** + * The value to copy. + */ + @property({ type: String }) value = ''; + + /** + * The id of the element to copy the test value from. + */ + @property({ type: String }) for = ''; + + private handleClick() { + if (this.copy) return; + this.__executeCopy(); + } + + private __executeCopy() { + if (this.for) { + const target = document.getElementById(this.for)!; + if (target) { + this.value = target.textContent || ''; + } + } + if (this.value) { + navigator.clipboard.writeText(this.value); + this.copy = true; + setTimeout(() => (this.copy = false), 2000); + } + } + + protected update(changedProperties: PropertyValueMap | Map): void { + super.update(changedProperties); + if (changedProperties.has('copy') && this.copy) { + this.__executeCopy(); + } + } + + render() { + return html` +
+ + + +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sl-clipboard': SlClipboard; + } +} diff --git a/src/components/clipboard/clipboard.styles.ts b/src/components/clipboard/clipboard.styles.ts new file mode 100644 index 0000000000..ea626d5cdd --- /dev/null +++ b/src/components/clipboard/clipboard.styles.ts @@ -0,0 +1,23 @@ +import { css } from 'lit'; +import componentStyles from '../../styles/component.styles.js'; + +export default css` + ${componentStyles} + + :host { + display: inline-block; + } + + .clipboard--copy sl-icon-button::part(base) { + color: var(--sl-color-success-600); + } + + .clipboard--copy sl-icon-button::part(base):hover, + .clipboard--copy sl-icon-button::part(base):focus { + color: var(--sl-color-success-600); + } + + .clipboard--copy sl-icon-button::part(base):active { + color: var(--sl-color-success-600); + } +`; diff --git a/src/components/clipboard/clipboard.test.ts b/src/components/clipboard/clipboard.test.ts new file mode 100644 index 0000000000..76a22a1b60 --- /dev/null +++ b/src/components/clipboard/clipboard.test.ts @@ -0,0 +1,30 @@ +import '../../../dist/shoelace.js'; +import { aTimeout, expect, fixture, html } from '@open-wc/testing'; +import type SlClipboard from './clipboard.js'; + +describe('', () => { + let el: SlClipboard; + + describe('when provided no parameters', () => { + before(async () => { + el = await fixture(html` `); + }); + + it('should pass accessibility tests', async () => { + await expect(el).to.be.accessible(); + }); + + it('should initially not be in copy state', () => { + expect(el.copy).to.false; + }); + + it('should reset copy state after 2 seconds', async () => { + expect(el.copy).to.be.false; + el.copy = true; + await aTimeout(1000); + expect(el.copy).to.be.true; + await aTimeout(1100); + expect(el.copy).to.be.false; + }); + }); +}); diff --git a/src/components/clipboard/clipboard.ts b/src/components/clipboard/clipboard.ts new file mode 100644 index 0000000000..390f5940c1 --- /dev/null +++ b/src/components/clipboard/clipboard.ts @@ -0,0 +1,4 @@ +import SlClipboard from './clipboard.component.js'; +export * from './clipboard.component.js'; +export default SlClipboard; +SlClipboard.define('sl-clipboard'); diff --git a/src/shoelace.ts b/src/shoelace.ts index 1fff365fd9..69c2e6478f 100644 --- a/src/shoelace.ts +++ b/src/shoelace.ts @@ -12,6 +12,7 @@ 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 SlDetails } from './components/details/details.js'; export { default as SlDialog } from './components/dialog/dialog.js'; From 8e521115f1788622012d3e3c6cf11021b48f0958 Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Mon, 31 Jul 2023 18:25:28 +0200 Subject: [PATCH 2/6] using slots --- docs/pages/components/clipboard.md | 67 +++++++++++++------ .../clipboard/clipboard.component.ts | 50 ++++++++++---- src/components/clipboard/clipboard.styles.ts | 43 ++++++++++-- 3 files changed, 120 insertions(+), 40 deletions(-) diff --git a/docs/pages/components/clipboard.md b/docs/pages/components/clipboard.md index 2ba8ce069d..1c5883a8ed 100644 --- a/docs/pages/components/clipboard.md +++ b/docs/pages/components/clipboard.md @@ -5,6 +5,53 @@ meta: layout: component --- +```html:preview +

Clicking the clipboard button will put "shoelace rocks" into your clipboard

+ +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> +

Clicking the clipboard button will put "shoelace rocks" into your clipboard

+ + +); +``` + +## Examples + +### Use your own button + +```html:preview + + + + +
+ + Copy + Copied + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + +
copied
+
+ +); +``` + +### Get the textValue from a different element + ```html:preview
@@ -47,26 +94,6 @@ const App = () => ( ); ``` -## Examples - -### Providing directly a text value - -```html:preview -

Clicking the clipboard button will put "shoelace rocks" into your clipboard

- -``` - -```jsx:react -import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; - -const App = () => ( - <> -

Clicking the clipboard button will put "shoelace rocks" into your clipboard

- - -); -``` - ## Disclaimer The public API is partially inspired by https://github.com/github/clipboard-copy-element diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts index 7c32da0790..54cec30f41 100644 --- a/src/components/clipboard/clipboard.component.ts +++ b/src/components/clipboard/clipboard.component.ts @@ -15,16 +15,24 @@ import type { CSSResultGroup, PropertyValueMap } from 'lit'; * * @dependency sl-icon-button * @dependency sl-tooltip + * + * @slot - The content that gets clicked to copy. + * @slot copied - The content shown after a successful copy. */ export default class SlClipboard extends ShoelaceElement { static styles: CSSResultGroup = styles; static dependencies = { 'sl-tooltip': SlTooltip, 'sl-icon-button': SlIconButton }; /** - * Indicates whether or not copy info is shown. + * Indicates whether or not copied info is shown. */ @property({ type: Boolean, reflect: true }) copy = false; + /** + * Indicates whether or not copy error is shown. + */ + @property({ type: Boolean }) copyError = false; + /** * The value to copy. */ @@ -36,11 +44,11 @@ export default class SlClipboard extends ShoelaceElement { @property({ type: String }) for = ''; private handleClick() { - if (this.copy) return; + if (this.copy || this.copyError) return; this.__executeCopy(); } - private __executeCopy() { + private async __executeCopy() { if (this.for) { const target = document.getElementById(this.for)!; if (target) { @@ -48,9 +56,14 @@ export default class SlClipboard extends ShoelaceElement { } } if (this.value) { - navigator.clipboard.writeText(this.value); - this.copy = true; - setTimeout(() => (this.copy = false), 2000); + try { + await navigator.clipboard.writeText(this.value); + this.copy = true; + setTimeout(() => (this.copy = false), 2000); + } catch (error) { + this.copyError = true; + setTimeout(() => (this.copyError = false), 2000); + } } } @@ -67,16 +80,25 @@ export default class SlClipboard extends ShoelaceElement { part="base" class=${classMap({ clipboard: true, - 'clipboard--copy': this.copy + 'clipboard--copy': this.copy, + 'clipboard--copy-error': this.copyError })} > - - - + + + + + + + + + + + + + + +
`; } diff --git a/src/components/clipboard/clipboard.styles.ts b/src/components/clipboard/clipboard.styles.ts index ea626d5cdd..ef1f409a53 100644 --- a/src/components/clipboard/clipboard.styles.ts +++ b/src/components/clipboard/clipboard.styles.ts @@ -8,16 +8,47 @@ export default css` display: inline-block; } - .clipboard--copy sl-icon-button::part(base) { - color: var(--sl-color-success-600); + /* successful copy */ + slot[name='copied'] { + display: none; + } + .clipboard--copy #default { + display: none; + } + .clipboard--copy slot[name='copied'] { + display: block; } - .clipboard--copy sl-icon-button::part(base):hover, - .clipboard--copy sl-icon-button::part(base):focus { + .green::part(base) { color: var(--sl-color-success-600); } - - .clipboard--copy sl-icon-button::part(base):active { + .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='copy-error'] { + display: none; + } + .clipboard--copy-error #default { + display: none; + } + .clipboard--copy-error slot[name='copy-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); + } `; From a10fd629cd95a30cbe1977c9270888cbee47b3a5 Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Tue, 1 Aug 2023 08:47:36 +0200 Subject: [PATCH 3/6] using a single copyStatus --- docs/pages/components/clipboard.md | 56 ++++++++++++++++++- .../clipboard/clipboard.component.ts | 55 +++++++++--------- src/components/clipboard/clipboard.styles.ts | 10 ++-- src/components/clipboard/clipboard.test.ts | 15 +++-- .../tree-item/tree-item.component.ts | 4 +- src/events/events.ts | 2 + src/events/sl-copied.ts | 7 +++ src/events/sl-copying.ts | 7 +++ 8 files changed, 110 insertions(+), 46 deletions(-) create mode 100644 src/events/sl-copied.ts create mode 100644 src/events/sl-copying.ts diff --git a/docs/pages/components/clipboard.md b/docs/pages/components/clipboard.md index 1c5883a8ed..f42c96bc1a 100644 --- a/docs/pages/components/clipboard.md +++ b/docs/pages/components/clipboard.md @@ -28,12 +28,14 @@ const App = () => ( ```html:preview - + +
Copy Copied + Error ``` @@ -45,6 +47,12 @@ const App = () => (
copied
+ +
+ + Copy + Copied + Error ); @@ -94,6 +102,52 @@ const App = () => ( ); ``` +### Error if copy fails + +For example if a `for` target element is not found or if not using `https`. +An empty string value like `value=""` will also result in an error. + +```html:preview + +
+ + Copy + Copied + Error + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + + Copy + Copied + Error + + +); +``` + +### Change duration of reset to copy button + +```html:preview + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + +); +``` + ## Disclaimer The public API is partially inspired by https://github.com/github/clipboard-copy-element diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts index 54cec30f41..97113d29dd 100644 --- a/src/components/clipboard/clipboard.component.ts +++ b/src/components/clipboard/clipboard.component.ts @@ -5,7 +5,7 @@ import ShoelaceElement from '../../internal/shoelace-element.js'; import SlIconButton from '../icon-button/icon-button.component.js'; import SlTooltip from '../tooltip/tooltip.component.js'; import styles from './clipboard.styles.js'; -import type { CSSResultGroup, PropertyValueMap } from 'lit'; +import type { CSSResultGroup } from 'lit'; /** * @summary Enables you to save content into the clipboard providing visual feedback. @@ -16,39 +16,38 @@ import type { CSSResultGroup, PropertyValueMap } from 'lit'; * @dependency sl-icon-button * @dependency sl-tooltip * + * @event sl-copying - Event when copying starts. + * @event sl-copied - Event when copying finished. + * * @slot - The content that gets clicked to copy. * @slot copied - The content shown after a successful copy. + * @slot error - The content shown if an error occurs. */ export default class SlClipboard extends ShoelaceElement { static styles: CSSResultGroup = styles; static dependencies = { 'sl-tooltip': SlTooltip, 'sl-icon-button': SlIconButton }; /** - * Indicates whether or not copied info is shown. - */ - @property({ type: Boolean, reflect: true }) copy = false; - - /** - * Indicates whether or not copy error is shown. + * Indicates the current status the copy action is in. */ - @property({ type: Boolean }) copyError = false; + @property({ type: String }) copyStatus: 'trigger' | 'copied' | 'error' = 'trigger'; - /** - * The value to copy. - */ + /** Value to copy. */ @property({ type: String }) value = ''; - /** - * The id of the element to copy the test value from. - */ + /** Id of the element to copy the text value from. */ @property({ type: String }) for = ''; + /** Duration in milliseconds to reset to the trigger state. */ + @property({ type: Number, attribute: 'reset-timeout' }) resetTimeout = 2000; + private handleClick() { - if (this.copy || this.copyError) return; - this.__executeCopy(); + if (this.copyStatus === 'copied') return; + this.copy(); } - private async __executeCopy() { + /** Copies the clipboard */ + async copy() { if (this.for) { const target = document.getElementById(this.for)!; if (target) { @@ -57,21 +56,18 @@ export default class SlClipboard extends ShoelaceElement { } if (this.value) { try { + this.emit('sl-copying'); await navigator.clipboard.writeText(this.value); - this.copy = true; - setTimeout(() => (this.copy = false), 2000); + this.emit('sl-copied'); + this.copyStatus = 'copied'; } catch (error) { - this.copyError = true; - setTimeout(() => (this.copyError = false), 2000); + this.copyStatus = 'error'; } + } else { + this.copyStatus = 'error'; } - } - protected update(changedProperties: PropertyValueMap | Map): void { - super.update(changedProperties); - if (changedProperties.has('copy') && this.copy) { - this.__executeCopy(); - } + setTimeout(() => (this.copyStatus = 'trigger'), this.resetTimeout); } render() { @@ -80,8 +76,7 @@ export default class SlClipboard extends ShoelaceElement { part="base" class=${classMap({ clipboard: true, - 'clipboard--copy': this.copy, - 'clipboard--copy-error': this.copyError + [`clipboard--${this.copyStatus}`]: true })} > @@ -94,7 +89,7 @@ export default class SlClipboard extends ShoelaceElement { - + diff --git a/src/components/clipboard/clipboard.styles.ts b/src/components/clipboard/clipboard.styles.ts index ef1f409a53..e99391b5fa 100644 --- a/src/components/clipboard/clipboard.styles.ts +++ b/src/components/clipboard/clipboard.styles.ts @@ -12,10 +12,10 @@ export default css` slot[name='copied'] { display: none; } - .clipboard--copy #default { + .clipboard--copied #default { display: none; } - .clipboard--copy slot[name='copied'] { + .clipboard--copied slot[name='copied'] { display: block; } @@ -31,13 +31,13 @@ export default css` } /* failed to copy */ - slot[name='copy-error'] { + slot[name='error'] { display: none; } - .clipboard--copy-error #default { + .clipboard--error #default { display: none; } - .clipboard--copy-error slot[name='copy-error'] { + .clipboard--error slot[name='error'] { display: block; } diff --git a/src/components/clipboard/clipboard.test.ts b/src/components/clipboard/clipboard.test.ts index 76a22a1b60..d80437452b 100644 --- a/src/components/clipboard/clipboard.test.ts +++ b/src/components/clipboard/clipboard.test.ts @@ -14,17 +14,16 @@ describe('', () => { await expect(el).to.be.accessible(); }); - it('should initially not be in copy state', () => { - expect(el.copy).to.false; + it('should initially be in the trigger status', () => { + expect(el.copyStatus).to.equal('trigger'); }); - it('should reset copy state after 2 seconds', async () => { - expect(el.copy).to.be.false; - el.copy = true; - await aTimeout(1000); - expect(el.copy).to.be.true; - await aTimeout(1100); + it('should reset copyStatus after 2 seconds', async () => { expect(el.copy).to.be.false; + await el.copy(); + expect(el.copyStatus).to.equal('copied'); + await aTimeout(2100); + expect(el.copyStatus).to.equal('trigger'); }); }); }); diff --git a/src/components/tree-item/tree-item.component.ts b/src/components/tree-item/tree-item.component.ts index 2fed06ced4..076b009bc8 100644 --- a/src/components/tree-item/tree-item.component.ts +++ b/src/components/tree-item/tree-item.component.ts @@ -12,7 +12,7 @@ import SlCheckbox from '../checkbox/checkbox.component.js'; import SlIcon from '../icon/icon.component.js'; import SlSpinner from '../spinner/spinner.component.js'; import styles from './tree-item.styles.js'; -import type { CSSResultGroup, PropertyValueMap } from 'lit'; +import type { CSSResultGroup, PropertyValues } from 'lit'; /** * @summary A tree item serves as a hierarchical node that lives inside a [tree](/components/tree). @@ -139,7 +139,7 @@ export default class SlTreeItem extends ShoelaceElement { this.isLeaf = !this.lazy && this.getChildrenItems().length === 0; } - protected willUpdate(changedProperties: PropertyValueMap | Map) { + protected willUpdate(changedProperties: PropertyValues | Map) { if (changedProperties.has('selected') && !changedProperties.has('indeterminate')) { this.indeterminate = false; } diff --git a/src/events/events.ts b/src/events/events.ts index 913ee98700..322b9c8d63 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -8,6 +8,8 @@ export type { default as SlChangeEvent } from './sl-change'; export type { default as SlClearEvent } from './sl-clear'; export type { default as SlCloseEvent } from './sl-close'; export type { default as SlCollapseEvent } from './sl-collapse'; +export type { SlCopyingEvent } from './sl-copying'; +export type { SlCopiedEvent } from './sl-copied'; export type { default as SlErrorEvent } from './sl-error'; export type { default as SlExpandEvent } from './sl-expand'; export type { default as SlFinishEvent } from './sl-finish'; diff --git a/src/events/sl-copied.ts b/src/events/sl-copied.ts new file mode 100644 index 0000000000..6293ba8e35 --- /dev/null +++ b/src/events/sl-copied.ts @@ -0,0 +1,7 @@ +export type SlCopiedEvent = CustomEvent>; + +declare global { + interface GlobalEventHandlersEventMap { + 'sl-copied': SlCopiedEvent; + } +} diff --git a/src/events/sl-copying.ts b/src/events/sl-copying.ts new file mode 100644 index 0000000000..33ad22adcc --- /dev/null +++ b/src/events/sl-copying.ts @@ -0,0 +1,7 @@ +export type SlCopyingEvent = CustomEvent>; + +declare global { + interface GlobalEventHandlersEventMap { + 'sl-copying': SlCopyingEvent; + } +} From edd25a48fa40ade4c17fd68591951760ea7938ea Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Tue, 1 Aug 2023 10:08:16 +0200 Subject: [PATCH 4/6] feat(clipboard): support inputs/textarea/links and shadow dom --- docs/pages/components/clipboard.md | 78 +++++++++++++++++++ .../clipboard/clipboard.component.ts | 11 ++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/docs/pages/components/clipboard.md b/docs/pages/components/clipboard.md index f42c96bc1a..6b7fcd8d57 100644 --- a/docs/pages/components/clipboard.md +++ b/docs/pages/components/clipboard.md @@ -102,6 +102,38 @@ const App = () => ( ); ``` +### Copy an input/textarea or link + +```html:preview + + +
+ + +
+Shoelace + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + +
+ + +
+ Shoelace + + +); +``` + ### Error if copy fails For example if a `for` target element is not found or if not using `https`. @@ -148,6 +180,52 @@ const App = () => ( ); ``` +### Supports Shadow Dom + +```html:preview + + + +``` + +```jsx:react +import { SlClipboard } from '@shoelace-style/shoelace/dist/react'; + +const App = () => ( + <> + + +); + +customElements.define('sl-copy-demo-el', class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.shadowRoot.innerHTML = ` +

copy me (inside shadow root)

+ + `; + } +}); +``` + ## Disclaimer The public API is partially inspired by https://github.com/github/clipboard-copy-element diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts index 97113d29dd..3f6a2eff75 100644 --- a/src/components/clipboard/clipboard.component.ts +++ b/src/components/clipboard/clipboard.component.ts @@ -49,9 +49,16 @@ export default class SlClipboard extends ShoelaceElement { /** Copies the clipboard */ async copy() { if (this.for) { - const target = document.getElementById(this.for)!; + const root = this.getRootNode() as ShadowRoot | Document; + const target = 'getElementById' in root ? root.getElementById(this.for) : false; if (target) { - this.value = target.textContent || ''; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + this.value = target.value; + } else if (target instanceof HTMLAnchorElement && target.hasAttribute('href')) { + this.value = target.href; + } else { + this.value = target.textContent || ''; + } } } if (this.value) { From 27245702d2c72333b60377de5f5ea30b4b36806c Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Tue, 1 Aug 2023 10:34:50 +0200 Subject: [PATCH 5/6] fix(clipboard): add area-live to announce copied --- src/components/clipboard/clipboard.component.ts | 1 + src/components/clipboard/clipboard.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts index 3f6a2eff75..9bb63d506d 100644 --- a/src/components/clipboard/clipboard.component.ts +++ b/src/components/clipboard/clipboard.component.ts @@ -81,6 +81,7 @@ export default class SlClipboard extends ShoelaceElement { return html`
', () => { }); it('should reset copyStatus after 2 seconds', async () => { - expect(el.copy).to.be.false; - await el.copy(); - expect(el.copyStatus).to.equal('copied'); + 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'); }); From 1439df2ca0131fb66848f3ab6e8f9159a5134b57 Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Tue, 1 Aug 2023 16:04:56 +0200 Subject: [PATCH 6/6] feat(clipboard): support any component with a value property --- docs/pages/components/clipboard.md | 10 ++++++++++ src/components/clipboard/clipboard.component.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/docs/pages/components/clipboard.md b/docs/pages/components/clipboard.md index 6b7fcd8d57..08e007db1b 100644 --- a/docs/pages/components/clipboard.md +++ b/docs/pages/components/clipboard.md @@ -108,12 +108,22 @@ const App = () => (
+
+ Shoelace +
+ + + +
+ + + ``` ```jsx:react diff --git a/src/components/clipboard/clipboard.component.ts b/src/components/clipboard/clipboard.component.ts index 9bb63d506d..0571020242 100644 --- a/src/components/clipboard/clipboard.component.ts +++ b/src/components/clipboard/clipboard.component.ts @@ -56,6 +56,8 @@ export default class SlClipboard extends ShoelaceElement { 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 || ''; }