mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-17346] Move A11yTitle and CopyClick to CL (#12936)
* Move A11yTitle and CopyClick to CL
This commit is contained in:
38
libs/components/src/a11y/a11y-title.directive.ts
Normal file
38
libs/components/src/a11y/a11y-title.directive.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appA11yTitle]",
|
||||
standalone: true,
|
||||
})
|
||||
export class A11yTitleDirective implements OnInit {
|
||||
@Input() set appA11yTitle(title: string) {
|
||||
this.title = title;
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private title: string;
|
||||
private originalTitle: string | null;
|
||||
private originalAriaLabel: string | null;
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.originalTitle = this.el.nativeElement.getAttribute("title");
|
||||
this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private setAttributes() {
|
||||
if (this.originalTitle === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
|
||||
}
|
||||
if (this.originalAriaLabel === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/components/src/a11y/index.ts
Normal file
1
libs/components/src/a11y/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./a11y-title.directive";
|
||||
123
libs/components/src/copy-click/copy-click.directive.spec.ts
Normal file
123
libs/components/src/copy-click/copy-click.directive.spec.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Component, ElementRef, ViewChild } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { ToastService } from "../";
|
||||
|
||||
import { CopyClickDirective } from "./copy-click.directive";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<button type="button" appCopyClick="no toast shown" #noToast></button>
|
||||
<button type="button" appCopyClick="info toast shown" showToast="info" #infoToast></button>
|
||||
<button type="button" appCopyClick="success toast shown" showToast #successToast></button>
|
||||
<button
|
||||
type="button"
|
||||
appCopyClick="toast with label"
|
||||
showToast
|
||||
valueLabel="Content"
|
||||
#toastWithLabel
|
||||
></button>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [CopyClickDirective],
|
||||
})
|
||||
class TestCopyClickComponent {
|
||||
@ViewChild("noToast") noToastButton!: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("infoToast") infoToastButton!: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("successToast") successToastButton!: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("toastWithLabel") toastWithLabelButton!: ElementRef<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
describe("CopyClickDirective", () => {
|
||||
let fixture: ComponentFixture<TestCopyClickComponent>;
|
||||
const copyToClipboard = jest.fn();
|
||||
const showToast = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
copyToClipboard.mockClear();
|
||||
showToast.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TestCopyClickComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: (key: string, ...rest: string[]) => {
|
||||
if (rest?.length) {
|
||||
return `${key} ${rest.join("")}`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
},
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
|
||||
{ provide: ToastService, useValue: { showToast } },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestCopyClickComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("copies the the value for all variants of toasts ", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("no toast shown");
|
||||
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("info toast shown");
|
||||
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("success toast shown");
|
||||
});
|
||||
|
||||
it("does not show a toast when showToast is not present", () => {
|
||||
const noToastButton = fixture.componentInstance.noToastButton.nativeElement;
|
||||
|
||||
noToastButton.click();
|
||||
expect(showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a success toast when showToast is present", () => {
|
||||
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
|
||||
|
||||
successToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "success",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the toast variant when set with showToast", () => {
|
||||
const infoToastButton = fixture.componentInstance.infoToastButton.nativeElement;
|
||||
|
||||
infoToastButton.click();
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "copySuccessful",
|
||||
title: null,
|
||||
variant: "info",
|
||||
});
|
||||
});
|
||||
|
||||
it('includes label in toast message when "copyLabel" is set', () => {
|
||||
const toastWithLabelButton = fixture.componentInstance.toastWithLabelButton.nativeElement;
|
||||
|
||||
toastWithLabelButton.click();
|
||||
|
||||
expect(showToast).toHaveBeenCalledWith({
|
||||
message: "valueCopied Content",
|
||||
title: null,
|
||||
variant: "success",
|
||||
});
|
||||
});
|
||||
});
|
||||
70
libs/components/src/copy-click/copy-click.directive.ts
Normal file
70
libs/components/src/copy-click/copy-click.directive.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { ToastService, ToastVariant } from "../";
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyClick]",
|
||||
standalone: true,
|
||||
})
|
||||
export class CopyClickDirective {
|
||||
private _showToast = false;
|
||||
private toastVariant: ToastVariant = "success";
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@Input("appCopyClick") valueToCopy = "";
|
||||
|
||||
/**
|
||||
* When set, the toast displayed will show `<valueLabel> copied`
|
||||
* instead of the default messaging.
|
||||
*/
|
||||
@Input() valueLabel: string;
|
||||
|
||||
/**
|
||||
* When set without a value, a success toast will be shown when the value is copied
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast/></app-component>
|
||||
* ```
|
||||
* When set with a value, a toast with the specified variant will be shown when the value is copied
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
||||
* ```
|
||||
*/
|
||||
@Input() set showToast(value: ToastVariant | "") {
|
||||
// When the `showToast` is set without a value, an empty string will be passed
|
||||
if (value === "") {
|
||||
this._showToast = true;
|
||||
} else {
|
||||
this._showToast = true;
|
||||
this.toastVariant = value;
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||
|
||||
if (this._showToast) {
|
||||
const message = this.valueLabel
|
||||
? this.i18nService.t("valueCopied", this.valueLabel)
|
||||
: this.i18nService.t("copySuccessful");
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: this.toastVariant,
|
||||
title: null,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/components/src/copy-click/index.ts
Normal file
1
libs/components/src/copy-click/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./copy-click.directive";
|
||||
@@ -1,3 +1,5 @@
|
||||
export { ButtonType } from "./shared/button-like.abstraction";
|
||||
export * from "./a11y";
|
||||
export * from "./async-actions";
|
||||
export * from "./avatar";
|
||||
export * from "./badge-list";
|
||||
@@ -5,13 +7,13 @@ export * from "./badge";
|
||||
export * from "./banner";
|
||||
export * from "./breadcrumbs";
|
||||
export * from "./button";
|
||||
export { ButtonType } from "./shared/button-like.abstraction";
|
||||
export * from "./callout";
|
||||
export * from "./card";
|
||||
export * from "./checkbox";
|
||||
export * from "./chip-select";
|
||||
export * from "./color-password";
|
||||
export * from "./container";
|
||||
export * from "./copy-click";
|
||||
export * from "./dialog";
|
||||
export * from "./disclosure";
|
||||
export * from "./drawer";
|
||||
|
||||
Reference in New Issue
Block a user