diff --git a/src/cdk/clipboard/clipboard.md b/src/cdk/clipboard/clipboard.md
index 5293a1280a56..67d3b9b5c4db 100644
--- a/src/cdk/clipboard/clipboard.md
+++ b/src/cdk/clipboard/clipboard.md
@@ -55,3 +55,10 @@ class HeroProfile {
}
}
```
+
+If you're using the `cdkCopyToClipboard` you can pass in the `cdkCopyToClipboardAttempts` input
+to automatically attempt to copy some text a certain number of times.
+
+```html
+
+```
diff --git a/src/cdk/clipboard/copy-to-clipboard.spec.ts b/src/cdk/clipboard/copy-to-clipboard.spec.ts
index a3537ca3c1ae..34df05595ce4 100644
--- a/src/cdk/clipboard/copy-to-clipboard.spec.ts
+++ b/src/cdk/clipboard/copy-to-clipboard.spec.ts
@@ -1,8 +1,9 @@
-import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {Component} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {Clipboard} from './clipboard';
import {ClipboardModule} from './clipboard-module';
+import {PendingCopy} from './pending-copy';
const COPY_CONTENT = 'copy content';
@@ -11,17 +12,18 @@ const COPY_CONTENT = 'copy content';
template: `
`,
+ [cdkCopyToClipboardAttempts]="attempts"
+ (cdkCopyToClipboardCopied)="copied($event)">`,
})
class CopyToClipboardHost {
- @Input() content = '';
- @Output() copied = new EventEmitter();
+ content = '';
+ attempts = 1;
+ copied = jasmine.createSpy('copied spy');
}
describe('CdkCopyToClipboard', () => {
let fixture: ComponentFixture;
- let mockCopy: jasmine.Spy;
- let copiedOutput: jasmine.Spy;
+ let clipboard: Clipboard;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
@@ -37,31 +39,69 @@ describe('CdkCopyToClipboard', () => {
const host = fixture.componentInstance;
host.content = COPY_CONTENT;
- copiedOutput = jasmine.createSpy('copied');
- host.copied.subscribe(copiedOutput);
- mockCopy = spyOn(TestBed.get(Clipboard), 'copy');
-
+ clipboard = TestBed.get(Clipboard);
fixture.detectChanges();
});
it('copies content to clipboard upon click', () => {
+ spyOn(clipboard, 'copy');
fixture.nativeElement.querySelector('button')!.click();
-
- expect(mockCopy).toHaveBeenCalledWith(COPY_CONTENT);
+ expect(clipboard.copy).toHaveBeenCalledWith(COPY_CONTENT);
});
it('emits copied event true when copy succeeds', fakeAsync(() => {
- mockCopy.and.returnValue(true);
- fixture.nativeElement.querySelector('button')!.click();
+ spyOn(clipboard, 'copy').and.returnValue(true);
+ fixture.nativeElement.querySelector('button')!.click();
- expect(copiedOutput).toHaveBeenCalledWith(true);
- }));
+ expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
+ }));
it('emits copied event false when copy fails', fakeAsync(() => {
- mockCopy.and.returnValue(false);
- fixture.nativeElement.querySelector('button')!.click();
- tick();
+ spyOn(clipboard, 'copy').and.returnValue(false);
+ fixture.nativeElement.querySelector('button')!.click();
+ tick();
+
+ expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
+ }));
+
+ it('should be able to attempt multiple times before succeeding', fakeAsync(() => {
+ const maxAttempts = 3;
+ let attempts = 0;
+ spyOn(clipboard, 'beginCopy').and.returnValue({
+ copy: () => ++attempts >= maxAttempts,
+ destroy: () => {}
+ } as PendingCopy);
+ fixture.componentInstance.attempts = maxAttempts;
+ fixture.detectChanges();
+
+ fixture.nativeElement.querySelector('button')!.click();
+ fixture.detectChanges();
+ tick();
+
+ expect(attempts).toBe(maxAttempts);
+ expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
+ expect(fixture.componentInstance.copied).toHaveBeenCalledWith(true);
+ }));
+
+ it('should be able to attempt multiple times before failing', fakeAsync(() => {
+ const maxAttempts = 3;
+ let attempts = 0;
+ spyOn(clipboard, 'beginCopy').and.returnValue({
+ copy: () => {
+ attempts++;
+ return false;
+ },
+ destroy: () => {}
+ } as PendingCopy);
+ fixture.componentInstance.attempts = maxAttempts;
+ fixture.detectChanges();
- expect(copiedOutput).toHaveBeenCalledWith(false);
- }));
+ fixture.nativeElement.querySelector('button')!.click();
+ fixture.detectChanges();
+ tick();
+
+ expect(attempts).toBe(maxAttempts);
+ expect(fixture.componentInstance.copied).toHaveBeenCalledTimes(1);
+ expect(fixture.componentInstance.copied).toHaveBeenCalledWith(false);
+ }));
});
diff --git a/src/cdk/clipboard/copy-to-clipboard.ts b/src/cdk/clipboard/copy-to-clipboard.ts
index 869fb8ee3469..9a6f90560a5c 100644
--- a/src/cdk/clipboard/copy-to-clipboard.ts
+++ b/src/cdk/clipboard/copy-to-clipboard.ts
@@ -6,10 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directive, EventEmitter, Input, Output} from '@angular/core';
-
+import {
+ Directive,
+ EventEmitter,
+ Input,
+ Output,
+ NgZone,
+ InjectionToken,
+ Inject,
+ Optional,
+} from '@angular/core';
import {Clipboard} from './clipboard';
+/** Object that can be used to configure the default options for `CdkCopyToClipboard`. */
+export interface CdkCopyToClipboardConfig {
+ /** Default number of attempts to make when copying text to the clipboard. */
+ attempts?: number;
+}
+
+/** Injection token that can be used to provide the default options to `CdkCopyToClipboard`. */
+export const CKD_COPY_TO_CLIPBOARD_CONFIG =
+ new InjectionToken('CKD_COPY_TO_CLIPBOARD_CONFIG');
+
/**
* Provides behavior for a button that when clicked copies content into user's
* clipboard.
@@ -24,6 +42,12 @@ export class CdkCopyToClipboard {
/** Content to be copied. */
@Input('cdkCopyToClipboard') text: string = '';
+ /**
+ * How many times to attempt to copy the text. This may be necessary for longer text, because
+ * the browser needs time to fill an intermediate textarea element and copy the content.
+ */
+ @Input('cdkCopyToClipboardAttempts') attempts: number = 1;
+
/**
* Emits when some text is copied to the clipboard. The
* emitted value indicates whether copying was successful.
@@ -38,10 +62,42 @@ export class CdkCopyToClipboard {
*/
@Output('copied') _deprecatedCopied = this.copied;
- constructor(private readonly _clipboard: Clipboard) {}
+ constructor(
+ private _clipboard: Clipboard,
+ /**
+ * @deprecated _ngZone parameter to become required.
+ * @breaking-change 10.0.0
+ */
+ private _ngZone?: NgZone,
+ @Optional() @Inject(CKD_COPY_TO_CLIPBOARD_CONFIG) config?: CdkCopyToClipboardConfig) {
+
+ if (config && config.attempts != null) {
+ this.attempts = config.attempts;
+ }
+ }
/** Copies the current text to the clipboard. */
- copy() {
- this.copied.emit(this._clipboard.copy(this.text));
+ copy(attempts: number = this.attempts): void {
+ if (attempts > 1) {
+ let remainingAttempts = attempts;
+ const pending = this._clipboard.beginCopy(this.text);
+ const attempt = () => {
+ const successful = pending.copy();
+ if (!successful && --remainingAttempts) {
+ // @breaking-change 10.0.0 Remove null check for `_ngZone`.
+ if (this._ngZone) {
+ this._ngZone.runOutsideAngular(() => setTimeout(attempt));
+ } else {
+ setTimeout(attempt);
+ }
+ } else {
+ pending.destroy();
+ this.copied.emit(successful);
+ }
+ };
+ attempt();
+ } else {
+ this.copied.emit(this._clipboard.copy(this.text));
+ }
}
}
diff --git a/tools/public_api_guard/cdk/clipboard.d.ts b/tools/public_api_guard/cdk/clipboard.d.ts
index 8a62e784eff1..0373a94b92db 100644
--- a/tools/public_api_guard/cdk/clipboard.d.ts
+++ b/tools/public_api_guard/cdk/clipboard.d.ts
@@ -1,13 +1,21 @@
export declare class CdkCopyToClipboard {
_deprecatedCopied: EventEmitter;
+ attempts: number;
copied: EventEmitter;
text: string;
- constructor(_clipboard: Clipboard);
- copy(): void;
- static ɵdir: i0.ɵɵDirectiveDefWithMeta;
+ constructor(_clipboard: Clipboard,
+ _ngZone?: NgZone | undefined, config?: CdkCopyToClipboardConfig);
+ copy(attempts?: number): void;
+ static ɵdir: i0.ɵɵDirectiveDefWithMeta;
static ɵfac: i0.ɵɵFactoryDef;
}
+export interface CdkCopyToClipboardConfig {
+ attempts?: number;
+}
+
+export declare const CKD_COPY_TO_CLIPBOARD_CONFIG: InjectionToken;
+
export declare class Clipboard {
constructor(document: any);
beginCopy(text: string): PendingCopy;