diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index c2185203034..1bf31c09923 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -113,7 +113,7 @@ export const WithTextButton: Story = { template: ` (args)}>

The content of the callout

- Visit the help center + Visit the help center
`, }), diff --git a/libs/components/src/link/index.ts b/libs/components/src/link/index.ts index a917912a2af..08617e813f5 100644 --- a/libs/components/src/link/index.ts +++ b/libs/components/src/link/index.ts @@ -1,3 +1,2 @@ export * from "./link.component"; -export * from "./link.directive"; export * from "./link.module"; diff --git a/libs/components/src/link/link.component.html b/libs/components/src/link/link.component.html index 8cddc0e4611..2a82a64e558 100644 --- a/libs/components/src/link/link.component.html +++ b/libs/components/src/link/link.component.html @@ -1,22 +1,12 @@ @let leadIcon = startIcon(); @let tailIcon = endIcon(); - +@if (leadIcon) { + +} + - - - - @if (leadIcon || tailIcon) { -
- @if (leadIcon) { - - } - - @if (tailIcon) { - - } -
- } @else { - - }
+@if (tailIcon) { + +} diff --git a/libs/components/src/link/link.component.ts b/libs/components/src/link/link.component.ts index 0f4c95545a4..3b922d2c25a 100644 --- a/libs/components/src/link/link.component.ts +++ b/libs/components/src/link/link.component.ts @@ -1,33 +1,132 @@ -import { NgTemplateOutlet } from "@angular/common"; -import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + computed, + input, + booleanAttribute, + inject, + ElementRef, +} from "@angular/core"; import { BitwardenIcon } from "../shared/icon"; +import { ariaDisableElement } from "../utils"; -import { getLinkClasses, LinkType } from "./link.directive"; +export type LinkType = "primary" | "secondary" | "contrast" | "light"; + +const linkStyles: Record = { + primary: [ + "!tw-text-primary-600", + "hover:!tw-text-primary-700", + "focus-visible:before:tw-ring-primary-600", + ], + secondary: ["!tw-text-main", "hover:!tw-text-main", "focus-visible:before:tw-ring-primary-600"], + contrast: [ + "!tw-text-contrast", + "hover:!tw-text-contrast", + "focus-visible:before:tw-ring-text-contrast", + ], + light: ["!tw-text-alt2", "hover:!tw-text-alt2", "focus-visible:before:tw-ring-text-alt2"], +}; + +const commonStyles = [ + "tw-text-unset", + "tw-leading-none", + "tw-px-0", + "tw-py-0.5", + "tw-font-semibold", + "tw-bg-transparent", + "tw-border-0", + "tw-border-none", + "tw-rounded", + "tw-inline-flex", + "tw-items-center", + "tw-gap-2", + "tw-transition", + "tw-no-underline", + "tw-cursor-pointer", + "hover:[&>span]:tw-underline", + "hover:[&>span]:tw-decoration-1", + "disabled:tw-no-underline", + "disabled:tw-cursor-not-allowed", + "disabled:!tw-text-secondary-300", + "disabled:hover:!tw-text-secondary-300", + "disabled:hover:tw-no-underline", + "focus-visible:tw-outline-none", + "focus-visible:[&>span]:tw-underline", + "focus-visible:[&>span]:tw-decoration-1", + + // Workaround for html button tag not being able to be set to `display: inline` + // and at the same time not being able to use `tw-ring-offset` because of box-shadow issue. + // https://github.com/w3c/csswg-drafts/issues/3226 + // Add `tw-inline`, add `tw-py-0.5` and use regular `tw-ring` if issue is fixed. + // + // https://github.com/tailwindlabs/tailwindcss/issues/3595 + // Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better: + // switch to `outline` with `outline-offset` when Safari supports border radius on outline. + // Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred. + "tw-relative", + "before:tw-content-['']", + "before:tw-block", + "before:tw-absolute", + "before:-tw-inset-x-[0.1em]", + "before:tw-rounded-md", + "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", +]; + +export function getLinkClasses({ + linkType, + verticalInset, +}: { + linkType: LinkType; + verticalInset: string; +}): string[] { + return [`before:-tw-inset-y-[${verticalInset}]`] + .concat(commonStyles) + .concat(linkStyles[linkType] ?? []); +} @Component({ - selector: "a[bitLink]", + selector: "a[bitLink], button[bitLink]", templateUrl: "./link.component.html", changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgTemplateOutlet], host: { "[class]": "classList()", + "[attr.bit-aria-disable]": "isButton() ? true : null", }, }) export class LinkComponent { + private el = inject(ElementRef); + readonly linkType = input("primary"); readonly startIcon = input(undefined); readonly endIcon = input(undefined); + readonly disabled = input(false, { transform: booleanAttribute }); + + protected readonly isButton = computed(() => this.el.nativeElement.tagName === "BUTTON"); readonly classList = computed(() => { - return getLinkClasses({ linkType: this.linkType(), verticalInset: "0.125rem" }); + const verticalInset = this.isButton() ? "0.25rem" : "0.125rem"; + return getLinkClasses({ linkType: this.linkType(), verticalInset }); }); readonly startIconClasses = computed(() => { - return ["bwi", this.startIcon()]; + return ["bwi", "!tw-no-underline", this.startIcon()]; }); readonly endIconClasses = computed(() => { - return ["bwi", this.endIcon()]; + return ["bwi", "!tw-no-underline", this.endIcon()]; }); + + constructor() { + if (this.isButton()) { + ariaDisableElement(this.el.nativeElement, this.disabled); + } + } } diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts deleted file mode 100644 index e66e158a2d3..00000000000 --- a/libs/components/src/link/link.directive.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { input, Directive, inject, ElementRef, booleanAttribute, computed } from "@angular/core"; - -import { AriaDisableDirective } from "../a11y"; -import { ariaDisableElement } from "../utils"; - -export type LinkType = "primary" | "secondary" | "contrast" | "light"; - -const linkStyles: Record = { - primary: [ - "!tw-text-primary-600", - "hover:!tw-text-primary-700", - "focus-visible:before:tw-ring-primary-600", - ], - secondary: ["!tw-text-main", "hover:!tw-text-main", "focus-visible:before:tw-ring-primary-600"], - contrast: [ - "!tw-text-contrast", - "hover:!tw-text-contrast", - "focus-visible:before:tw-ring-text-contrast", - ], - light: ["!tw-text-alt2", "hover:!tw-text-alt2", "focus-visible:before:tw-ring-text-alt2"], -}; - -const commonStyles = [ - "tw-text-unset", - "tw-leading-none", - "tw-px-0", - "tw-py-0.5", - "tw-font-semibold", - "tw-bg-transparent", - "tw-border-0", - "tw-border-none", - "tw-rounded", - "tw-transition", - "tw-no-underline", - "tw-cursor-pointer", - "hover:tw-underline", - "hover:tw-decoration-1", - "disabled:tw-no-underline", - "disabled:tw-cursor-not-allowed", - "disabled:!tw-text-secondary-300", - "disabled:hover:!tw-text-secondary-300", - "disabled:hover:tw-no-underline", - "focus-visible:tw-outline-none", - "focus-visible:tw-underline", - "focus-visible:tw-decoration-1", - - // Workaround for html button tag not being able to be set to `display: inline` - // and at the same time not being able to use `tw-ring-offset` because of box-shadow issue. - // https://github.com/w3c/csswg-drafts/issues/3226 - // Add `tw-inline`, add `tw-py-0.5` and use regular `tw-ring` if issue is fixed. - // - // https://github.com/tailwindlabs/tailwindcss/issues/3595 - // Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better: - // switch to `outline` with `outline-offset` when Safari supports border radius on outline. - // Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred. - "tw-relative", - "before:tw-content-['']", - "before:tw-block", - "before:tw-absolute", - "before:-tw-inset-x-[0.1em]", - "before:tw-rounded-md", - "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", -]; - -export function getLinkClasses({ - linkType, - verticalInset, -}: { - linkType: LinkType; - verticalInset: string; -}): string[] { - return [`before:-tw-inset-y-[${verticalInset}]`] - .concat(commonStyles) - .concat(linkStyles[linkType] ?? []); -} - -@Directive({ - selector: "button[bitLink]", - hostDirectives: [AriaDisableDirective], - host: { - "[class]": "classList()", - }, -}) -export class ButtonLinkDirective { - private el = inject(ElementRef); - - readonly linkType = input("primary"); - readonly disabled = input(false, { transform: booleanAttribute }); - - readonly classList = computed(() => { - return getLinkClasses({ linkType: this.linkType(), verticalInset: "0.25rem" }); - }); - - constructor() { - ariaDisableElement(this.el.nativeElement, this.disabled); - } -} diff --git a/libs/components/src/link/link.module.ts b/libs/components/src/link/link.module.ts index dfa97f2453f..87ad8daa7e1 100644 --- a/libs/components/src/link/link.module.ts +++ b/libs/components/src/link/link.module.ts @@ -1,10 +1,9 @@ import { NgModule } from "@angular/core"; import { LinkComponent } from "./link.component"; -import { ButtonLinkDirective } from "./link.directive"; @NgModule({ - imports: [LinkComponent, ButtonLinkDirective], - exports: [LinkComponent, ButtonLinkDirective], + imports: [LinkComponent], + exports: [LinkComponent], }) export class LinkModule {} diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index c50ef0aa000..4a76f77a1d2 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -3,7 +3,6 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; import { LinkComponent } from "./link.component"; -import { ButtonLinkDirective } from "./link.directive"; import { LinkModule } from "./link.module"; export default { @@ -27,12 +26,12 @@ export default { }, } as Meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { render: (args) => ({ template: /*html*/ ` - (args)}>Your text here + (args)}>Your text here `, }), }; @@ -77,14 +76,12 @@ export const Buttons: Story = {
-
-
@@ -108,14 +105,12 @@ export const Anchors: StoryObj = { Anchor @@ -144,14 +139,51 @@ export const Inline: Story = { }, }; -export const Disabled: Story = { +export const WithIcons: Story = { render: (args) => ({ props: args, template: /*html*/ ` - - +
+ + + +
+ +
+
+ +
+
+ +
+
+ `, + }), + args: { + linkType: "primary", + }, +}; + +export const Disabled: Story = { + render: (args) => ({ + props: { + ...args, + onClick: () => { + alert("Button clicked! (This should not appear when disabled)"); + }, + }, + template: /*html*/ ` + + Links can not be disabled +
- +
`, }), diff --git a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts index 3580b1fada8..04545730172 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/advanced-uri-option-dialog.component.ts @@ -3,13 +3,13 @@ import { Component, inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - ButtonLinkDirective, ButtonModule, + CenterPositionStrategy, DialogModule, + DialogRef, DialogService, DIALOG_DATA, - DialogRef, - CenterPositionStrategy, + LinkComponent, } from "@bitwarden/components"; export type AdvancedUriOptionDialogParams = { @@ -22,7 +22,7 @@ export type AdvancedUriOptionDialogParams = { // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "advanced-uri-option-dialog.component.html", - imports: [ButtonLinkDirective, ButtonModule, DialogModule, JslibModule], + imports: [LinkComponent, ButtonModule, DialogModule, JslibModule], }) export class AdvancedUriOptionDialogComponent { constructor(private dialogRef: DialogRef) {} diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index 3d0cc4c4414..05d2ecede72 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -12,9 +12,15 @@ - + {{ "changeAtRiskPassword" | i18n }} - diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 8132780ccf4..f827fdbf016 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -17,9 +17,9 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { BadgeModule, - ButtonLinkDirective, CardComponent, FormFieldModule, + LinkComponent, TypographyModule, } from "@bitwarden/components"; @@ -37,7 +37,7 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; TypographyModule, OrgIconDirective, FormFieldModule, - ButtonLinkDirective, + LinkComponent, BadgeModule, ], })