mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 04:03:29 +00:00
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
import { CommonModule } from "@angular/common";
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
computed,
|
|
ElementRef,
|
|
inject,
|
|
input,
|
|
} from "@angular/core";
|
|
|
|
import { FocusableElement } from "../shared/focusable-element";
|
|
|
|
export type BadgeVariant =
|
|
| "primary"
|
|
| "secondary"
|
|
| "success"
|
|
| "danger"
|
|
| "warning"
|
|
| "info"
|
|
| "notification";
|
|
|
|
const styles: Record<BadgeVariant, string[]> = {
|
|
primary: ["tw-bg-primary-100", "tw-border-primary-700", "!tw-text-primary-700"],
|
|
secondary: ["tw-bg-secondary-100", "tw-border-secondary-700", "!tw-text-secondary-700"],
|
|
success: ["tw-bg-success-100", "tw-border-success-700", "!tw-text-success-700"],
|
|
danger: ["tw-bg-danger-100", "tw-border-danger-700", "!tw-text-danger-700"],
|
|
warning: ["tw-bg-warning-100", "tw-border-warning-700", "!tw-text-warning-700"],
|
|
info: ["tw-bg-info-100", "tw-border-info-700", "!tw-text-info-700"],
|
|
notification: [
|
|
"tw-bg-notification-100",
|
|
"tw-border-notification-600",
|
|
"!tw-text-notification-600",
|
|
],
|
|
};
|
|
|
|
const hoverStyles: Record<BadgeVariant, string[]> = {
|
|
primary: ["hover:tw-bg-primary-600", "hover:tw-border-primary-600", "hover:!tw-text-contrast"],
|
|
secondary: [
|
|
"hover:tw-bg-secondary-600",
|
|
"hover:tw-border-secondary-600",
|
|
"hover:!tw-text-contrast",
|
|
],
|
|
success: ["hover:tw-bg-success-600", "hover:tw-border-success-600", "hover:!tw-text-contrast"],
|
|
danger: ["hover:tw-bg-danger-600", "hover:tw-border-danger-600", "hover:!tw-text-contrast"],
|
|
warning: ["hover:tw-bg-warning-600", "hover:tw-border-warning-600", "hover:!tw-text-black"],
|
|
info: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"],
|
|
notification: [
|
|
"hover:tw-bg-notification-600",
|
|
"hover:tw-border-notification-600",
|
|
"hover:!tw-text-contrast",
|
|
],
|
|
};
|
|
/**
|
|
* Badges are primarily used as labels, counters, and small buttons.
|
|
* Typically Badges are only used with text set to `text-xs`. If additional sizes are needed, the component configurations may be reviewed and adjusted.
|
|
*
|
|
* The Badge directive can be used on a `<span>` (non clickable events), or an `<a>` or `<button>` tag
|
|
*
|
|
* > `NOTE:` The Focus and Hover states only apply to badges used for interactive events.
|
|
*
|
|
* > `NOTE:` The `disabled` state only applies to buttons.
|
|
*/
|
|
@Component({
|
|
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
|
|
providers: [{ provide: FocusableElement, useExisting: BadgeComponent }],
|
|
imports: [CommonModule],
|
|
templateUrl: "badge.component.html",
|
|
host: {
|
|
"[class]": "classList()",
|
|
"[attr.title]": "titleAttr()",
|
|
},
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class BadgeComponent implements FocusableElement {
|
|
private readonly el = inject(ElementRef<HTMLElement>);
|
|
|
|
private readonly hasHoverEffects = this.el.nativeElement.nodeName !== "SPAN";
|
|
|
|
/**
|
|
* Optional override for the automatic badge title attribute when truncating.
|
|
* When truncating is enabled and this is not provided, the badge will automatically
|
|
* use its text content as the title.
|
|
*/
|
|
readonly title = input<string>();
|
|
|
|
/**
|
|
* Visual variant that determines the badge's color scheme.
|
|
*/
|
|
readonly variant = input<BadgeVariant>("primary");
|
|
|
|
/**
|
|
* Whether to truncate long text with ellipsis when it exceeds maxWidthClass.
|
|
* When enabled, a title attribute is automatically added for accessibility.
|
|
*/
|
|
readonly truncate = input(true);
|
|
|
|
/**
|
|
* Tailwind max-width class to apply when truncating is enabled.
|
|
* Must be a valid Tailwind max-width utility class (e.g., "tw-max-w-40", "tw-max-w-xs").
|
|
*/
|
|
readonly maxWidthClass = input<`tw-max-w-${string}`>("tw-max-w-40");
|
|
|
|
protected readonly classList = computed(() => {
|
|
return [
|
|
"tw-inline-block",
|
|
"tw-py-1",
|
|
"tw-px-2",
|
|
"tw-font-medium",
|
|
"tw-text-center",
|
|
"tw-align-text-top",
|
|
"tw-rounded-full",
|
|
"tw-border-[0.5px]",
|
|
"tw-border-solid",
|
|
"tw-box-border",
|
|
"tw-whitespace-nowrap",
|
|
"tw-text-xs",
|
|
"hover:tw-no-underline",
|
|
"focus-visible:tw-outline-none",
|
|
"focus-visible:tw-ring-2",
|
|
"focus-visible:tw-ring-offset-2",
|
|
"focus-visible:tw-ring-primary-600",
|
|
"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",
|
|
]
|
|
.concat(styles[this.variant()])
|
|
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant()], "tw-min-w-10"] : [])
|
|
.concat(this.truncate() ? this.maxWidthClass() : []);
|
|
});
|
|
|
|
protected readonly titleAttr = computed(() => {
|
|
const title = this.title();
|
|
if (title !== undefined) {
|
|
return title;
|
|
}
|
|
return this.truncate() ? this.el.nativeElement?.textContent?.trim() : null;
|
|
});
|
|
|
|
getFocusTarget() {
|
|
return this.el.nativeElement;
|
|
}
|
|
}
|