diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts index b65138dac3a..b5d35e2005e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-generator-dialog/vault-generator-dialog.component.spec.ts @@ -76,8 +76,10 @@ describe("VaultGeneratorDialogComponent", () => { component.onValueGenerated("test-password"); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe(undefined); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(false); }); it("should disable the button if no value has been generated", () => { @@ -88,8 +90,10 @@ describe("VaultGeneratorDialogComponent", () => { generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe("true"); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); }); it("should disable the button if no algorithm is selected", () => { @@ -100,8 +104,10 @@ describe("VaultGeneratorDialogComponent", () => { generator.valueGenerated.emit("test-password"); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe("true"); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); }); it("should update button text when algorithm is selected", () => { diff --git a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts index afb32738901..085a3d0d4b0 100644 --- a/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts +++ b/apps/web/src/app/vault/components/web-generator-dialog/web-generator-dialog.component.spec.ts @@ -70,8 +70,10 @@ describe("WebVaultGeneratorDialogComponent", () => { generator.valueGenerated.emit("test-password"); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe(undefined); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(false); }); it("should disable the button if no value has been generated", () => { @@ -82,8 +84,10 @@ describe("WebVaultGeneratorDialogComponent", () => { generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe("true"); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); }); it("should disable the button if no algorithm is selected", () => { @@ -94,8 +98,10 @@ describe("WebVaultGeneratorDialogComponent", () => { generator.valueGenerated.emit("test-password"); fixture.detectChanges(); - const button = fixture.debugElement.query(By.css("[data-testid='select-button']")); - expect(button.attributes["aria-disabled"]).toBe("true"); + const button = fixture.debugElement.query( + By.css("[data-testid='select-button']"), + ).nativeElement; + expect(button.disabled).toBe(true); }); it("should close with selected value when confirmed", () => { diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index 1651b6cf12a..6ddbc172803 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -34,25 +34,23 @@ describe("Button", () => { expect(buttonDebugElement.nativeElement.disabled).toBeFalsy(); }); - it("should be aria-disabled and not html attribute disabled when disabled is true", () => { + it("should be disabled when disabled is true", () => { testAppComponent.disabled = true; fixture.detectChanges(); - expect(buttonDebugElement.attributes["aria-disabled"]).toBe("true"); - expect(buttonDebugElement.nativeElement.disabled).toBeFalsy(); + + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); // Anchor tags cannot be disabled. }); - it("should be aria-disabled not html attribute disabled when attribute disabled is true", () => { - fixture.detectChanges(); - expect(disabledButtonDebugElement.attributes["aria-disabled"]).toBe("true"); - expect(disabledButtonDebugElement.nativeElement.disabled).toBeFalsy(); + it("should be disabled when attribute disabled is true", () => { + expect(disabledButtonDebugElement.nativeElement.disabled).toBeTruthy(); }); it("should be disabled when loading is true", () => { testAppComponent.loading = true; fixture.detectChanges(); - expect(buttonDebugElement.attributes["aria-disabled"]).toBe("true"); + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); }); }); diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index a1b35608f25..635c269bd0f 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,20 +1,9 @@ import { NgClass } from "@angular/common"; -import { - HostBinding, - Component, - model, - computed, - input, - ElementRef, - inject, - Signal, - booleanAttribute, -} from "@angular/core"; +import { input, HostBinding, Component, model, computed, booleanAttribute } from "@angular/core"; import { toObservable, toSignal } from "@angular/core/rxjs-interop"; import { debounce, interval } from "rxjs"; import { ButtonLikeAbstraction, ButtonType, ButtonSize } from "../shared/button-like.abstraction"; -import { ariaDisableElement } from "../utils"; const focusRing = [ "focus-visible:tw-ring-2", @@ -62,7 +51,7 @@ const buttonStyles: Record = { providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], imports: [NgClass], host: { - "[attr.aria-disabled]": "disabledAttr()", + "[attr.disabled]": "disabledAttr()", }, }) export class ButtonComponent implements ButtonLikeAbstraction { @@ -79,28 +68,27 @@ export class ButtonComponent implements ButtonLikeAbstraction { "focus:tw-outline-none", ] .concat(this.block() ? ["tw-w-full", "tw-block"] : ["tw-inline-block"]) + .concat(buttonStyles[this.buttonType() ?? "secondary"]) .concat( this.showDisabledStyles() || this.disabled() ? [ - "aria-disabled:!tw-bg-secondary-300", - "hover:tw-bg-secondary-300", - "aria-disabled:tw-border-secondary-300", - "hover:tw-border-secondary-300", - "aria-disabled:!tw-text-muted", - "hover:!tw-text-muted", - "aria-disabled:tw-cursor-not-allowed", - "hover:tw-no-underline", - "aria-disabled:tw-pointer-events-none", + "disabled:tw-bg-secondary-300", + "disabled:hover:tw-bg-secondary-300", + "disabled:tw-border-secondary-300", + "disabled:hover:tw-border-secondary-300", + "disabled:!tw-text-muted", + "disabled:hover:!tw-text-muted", + "disabled:tw-cursor-not-allowed", + "disabled:hover:tw-no-underline", ] : [], ) - .concat(buttonStyles[this.buttonType() ?? "secondary"]) .concat(buttonSizeStyles[this.size() || "default"]); } protected disabledAttr = computed(() => { const disabled = this.disabled() != null && this.disabled() !== false; - return disabled || this.loading() ? true : undefined; + return disabled || this.loading() ? true : null; }); /** @@ -139,10 +127,5 @@ export class ButtonComponent implements ButtonLikeAbstraction { toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))), ); - readonly disabled = model(false); - private el = inject(ElementRef); - - constructor() { - ariaDisableElement(this.el.nativeElement, this.disabledAttr as Signal); - } + disabled = model(false); } diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index ccea0546f3a..c4fd018b3ba 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -46,7 +46,7 @@
= { const disabledStyles: Record = { contrast: [ - "aria-disabled:tw-opacity-60", - "aria-disabled:hover:tw-border-transparent", - "aria-disabled:hover:tw-bg-transparent", + "disabled:tw-opacity-60", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", ], main: [ - "aria-disabled:!tw-text-secondary-300", - "aria-disabled:hover:tw-border-transparent", - "aria-disabled:hover:tw-bg-transparent", + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", ], muted: [ - "aria-disabled:!tw-text-secondary-300", - "aria-disabled:hover:tw-border-transparent", - "aria-disabled:hover:tw-bg-transparent", + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", ], primary: [ - "aria-disabled:tw-opacity-60", - "aria-disabled:hover:tw-border-primary-600", - "aria-disabled:hover:tw-bg-primary-600", + "disabled:tw-opacity-60", + "disabled:hover:tw-border-primary-600", + "disabled:hover:tw-bg-primary-600", ], secondary: [ - "aria-disabled:tw-opacity-60", - "aria-disabled:hover:tw-border-text-muted", - "aria-disabled:hover:tw-bg-transparent", - "aria-disabled:hover:!tw-text-muted", + "disabled:tw-opacity-60", + "disabled:hover:tw-border-text-muted", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-muted", ], danger: [ - "aria-disabled:!tw-text-secondary-300", - "aria-disabled:hover:tw-border-transparent", - "aria-disabled:hover:tw-bg-transparent", - "aria-disabled:hover:!tw-text-secondary-300", + "disabled:!tw-text-secondary-300", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", + "disabled:hover:!tw-text-secondary-300", ], light: [ - "aria-disabled:tw-opacity-60", - "aria-disabled:hover:tw-border-transparent", - "aria-disabled:hover:tw-bg-transparent", + "disabled:tw-opacity-60", + "disabled:hover:tw-border-transparent", + "disabled:hover:tw-bg-transparent", ], unstyled: [], }; @@ -173,7 +163,7 @@ const sizes: Record = { ], imports: [NgClass], host: { - "[attr.aria-disabled]": "disabledAttr()", + "[attr.disabled]": "disabledAttr()", }, }) export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @@ -245,10 +235,5 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE return this.elementRef.nativeElement; } - private elementRef = inject(ElementRef); - - constructor() { - const element = this.elementRef.nativeElement; - ariaDisableElement(element, this.disabledAttr as Signal); - } + constructor(private elementRef: ElementRef) {} } diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index aced89fc7b3..f2eb44bc3a4 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -1,6 +1,4 @@ -import { HostBinding, Directive, inject, ElementRef, input, booleanAttribute } from "@angular/core"; - -import { ariaDisableElement } from "../utils"; +import { input, HostBinding, Directive } from "@angular/core"; export type LinkType = "primary" | "secondary" | "contrast" | "light"; @@ -60,11 +58,6 @@ const commonStyles = [ "before:tw-transition", "focus-visible:before:tw-ring-2", "focus-visible:tw-z-10", - "aria-disabled:tw-no-underline", - "aria-disabled:tw-pointer-events-none", - "aria-disabled:!tw-text-secondary-300", - "aria-disabled:hover:!tw-text-secondary-300", - "aria-disabled:hover:tw-no-underline", ]; @Directive() @@ -95,19 +88,9 @@ export class AnchorLinkDirective extends LinkDirective { selector: "button[bitLink]", }) export class ButtonLinkDirective extends LinkDirective { - private el = inject(ElementRef); - - disabled = input(false, { transform: booleanAttribute }); - @HostBinding("class") get classList() { return ["before:-tw-inset-y-[0.25rem]"] .concat(commonStyles) .concat(linkStyles[this.linkType()] ?? []); } - - constructor() { - super(); - - ariaDisableElement(this.el.nativeElement, this.disabled); - } } diff --git a/libs/components/src/utils/aria-disable-element.ts b/libs/components/src/utils/aria-disable-element.ts deleted file mode 100644 index f7e02f2cdd1..00000000000 --- a/libs/components/src/utils/aria-disable-element.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Signal, effect } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { fromEvent } from "rxjs"; - -/** - * a11y helper util used to `aria-disable` elements as opposed to using the HTML `disabled` attr. - * - Removes HTML `disabled` attr and replaces it with `aria-disabled="true"` - * - Captures click events and prevents them from propagating - */ -export function ariaDisableElement(element: HTMLElement, isDisabled: Signal) { - effect(() => { - if (element.hasAttribute("disabled") || isDisabled()) { - // Remove native disabled and set aria-disabled. Capture click event - element.removeAttribute("disabled"); - - element.setAttribute("aria-disabled", "true"); - } - }); - - fromEvent(element, "click") - .pipe(takeUntilDestroyed()) - .subscribe((event: Event) => { - if (isDisabled()) { - event.stopPropagation(); - event.preventDefault(); - return false; - } - }); -} diff --git a/libs/components/src/utils/index.ts b/libs/components/src/utils/index.ts index 91fa71cf0e0..afadd6b3b41 100644 --- a/libs/components/src/utils/index.ts +++ b/libs/components/src/utils/index.ts @@ -1,3 +1,2 @@ -export * from "./aria-disable-element"; export * from "./function-to-observable"; export * from "./i18n-mock.service";