1
0
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:
Oscar Hinton
2025-01-20 11:43:10 +01:00
committed by GitHub
parent 43a6a93944
commit d820bfb691
8 changed files with 23 additions and 14 deletions

View 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);
}
}
}

View File

@@ -0,0 +1 @@
export * from "./a11y-title.directive";

View 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",
});
});
});

View 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,
});
}
}
}

View File

@@ -0,0 +1 @@
export * from "./copy-click.directive";

View File

@@ -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";