import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
numberAttribute,
output,
signal,
} from '@angular/core';
import { copyToClipboard } from '@africanies/africanies-core';
import { AfricaniesIconComponent } from '@africanies/africanies-icons';
import { ButtonComponent, type ButtonSize } from '../button/button.component';
import { ToastService } from '../toast/toast.service';
/**
* Icon button that copies {@link value} to the clipboard.
*
* Shows `copy` → `check` while feedback is visible. Optional toast when
* {@link ToastService} is registered via `provideAfricaniesToasts()`.
*
* @example
* ```html
*
*
*
* ```
*/
@Component({
selector: 'africanies-copy',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent, AfricaniesIconComponent],
template: `
`,
})
/**
* Icon button that copies {@link value} to the clipboard.
*/
export class CopyButtonComponent {
private readonly toastService = inject(ToastService, { optional: true });
/** Plain text written to the clipboard. */
readonly value = input.required();
/**
* Optional visible label beside the icon. When omitted, the control is
* icon-only (use {@link ariaLabel} for accessibility).
*/
readonly label = input(undefined);
/** Label while the success checkmark is showing. Defaults to `Copied`. */
readonly copiedLabel = input('Copied');
/**
* Accessible name. Defaults to `Copy` / `Copied` from the label state, or
* `Copy to clipboard` when icon-only.
*/
readonly ariaLabel = input(undefined);
/** Extra classes on the inner `africanies-button` (e.g. inverted code headers). */
readonly buttonClass = input('');
/** Button size. Defaults to `sm`. */
readonly size = input('sm');
/** How long the checkmark / “Copied” label stays (ms). Defaults to 1600. */
readonly feedbackMs = input(1600, { transform: numberAttribute });
/**
* When true and {@link ToastService} is available, show a success toast with
* the copied value (truncated when long).
*/
readonly announce = input(false, { transform: booleanAttribute });
/** Emitted after a successful clipboard write. */
readonly copied = output();
/** Emitted when the clipboard write fails. */
readonly failed = output();
protected readonly busy = signal(false);
protected readonly showCopied = signal(false);
private feedbackTimer: ReturnType | null = null;
protected readonly iconSize = computed(() => (this.size() === 'lg' ? 18 : 14));
protected readonly resolvedAriaLabel = computed(() => {
const explicit = this.ariaLabel();
if (explicit) {
return this.showCopied() ? 'Copied' : explicit;
}
const label = this.label();
if (label) {
return this.showCopied() ? this.copiedLabel() : label;
}
return this.showCopied() ? 'Copied' : 'Copy to clipboard';
});
/**
* Copy {@link value} and flip the icon to a checkmark on success.
*/
protected async onCopy(): Promise {
if (this.busy()) {
return;
}
const text = this.value();
this.busy.set(true);
try {
const ok = await copyToClipboard(text);
if (!ok) {
this.failed.emit();
this.toastService?.error('Could not copy to the clipboard.');
return;
}
this.flashCopied();
this.copied.emit(text);
if (this.announce() && this.toastService) {
const preview =
text.length > 64 ? `${text.slice(0, 61).trimEnd()}…` : text;
this.toastService.success(`Copied ${preview}`);
}
} finally {
this.busy.set(false);
}
}
private flashCopied(): void {
this.showCopied.set(true);
if (this.feedbackTimer) {
clearTimeout(this.feedbackTimer);
}
this.feedbackTimer = setTimeout(() => {
this.showCopied.set(false);
this.feedbackTimer = null;
}, this.feedbackMs());
}
}