import { LitElement, html, css, type CSSResultGroup } from 'lit';
import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import '../../VisuallyHidden/core/VisuallyHidden.js';
export interface BadgeProps {
variant?: 'default' | 'success' | 'warning' | 'danger' | 'primary' | 'info' | 'neutral' | 'monochrome';
size?: 'xs' | 'sm' | 'md';
dot?: boolean;
value?: number | null;
max?: number;
interactive?: boolean;
statusLabel?: string | null;
live?: 'off' | 'polite' | 'assertive';
hiddenFromAT?: boolean;
}
export class AgBadge extends LitElement implements BadgeProps {
static styles: CSSResultGroup = css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: var(--ag-radius-full);
padding: var(--ag-space-1) var(--ag-space-2);
font-size: var(--ag-font-size-xs);
line-height: 1;
white-space: nowrap;
transition: all var(--ag-motion-fast);
}
:host([dot]) .badge {
width: var(--ag-space-2);
height: var(--ag-space-2);
padding: 0;
border-radius: 50%;
}
:host([size="xs"]) .badge {
font-size: calc(var(--ag-font-size-xs) - 1px);
}
:host([size="xs"]:not([dot])) .badge {
padding: var(--ag-space-0) var(--ag-space-1);
}
:host([size="xs"][dot]) .badge {
width: var(--ag-space-1);
height: var(--ag-space-1);
}
:host([size="sm"]) .badge {
font-size: var(--ag-font-size-xs);
}
:host([size="sm"]:not([dot])) .badge {
padding: var(--ag-space-1) var(--ag-space-2);
}
:host([size="sm"][dot]) .badge {
width: var(--ag-space-2);
height: var(--ag-space-2);
}
:host([size="md"]) .badge {
font-size: var(--ag-font-size-sm);
}
:host([size="md"]:not([dot])) .badge {
padding: var(--ag-space-1) var(--ag-space-3);
}
:host([size="md"][dot]) .badge {
width: var(--ag-space-3);
height: var(--ag-space-3);
}
:host([variant="default"]) .badge {
background-color: var(--ag-background-tertiary);
color: var(--ag-text-primary);
}
:host([variant="primary"]) .badge {
background-color: var(--ag-primary);
color: var(--ag-white);
}
:host([variant="success"]) .badge {
background-color: var(--ag-success);
color: var(--ag-white);
}
:host([variant="warning"]) .badge {
background-color: var(--ag-warning);
color: var(--ag-black);
}
:host([variant="danger"]) .badge {
background-color: var(--ag-danger);
color: var(--ag-white);
}
:host([variant="info"]) .badge {
background-color: var(--ag-info-dark);
color: var(--ag-white);
}
:host([variant="neutral"]) .badge {
background-color: var(--ag-neutral-200);
color: var(--ag-neutral-800);
}
:host([variant="monochrome"]) .badge {
background-color: var(--ag-background-primary-inverted);
color: var(--ag-text-primary-inverted);
}
:host([interactive]) .badge {
cursor: pointer;
}
:host([interactive]) .badge:hover {
filter: brightness(1.1);
}
:host([interactive]) .badge:focus-visible {
outline: var(--ag-focus-width) solid rgba(var(--ag-focus), 0.5);
outline-offset: var(--ag-focus-offset);
}
:host([single-char]) .badge {
border-radius: 50%;
}
:host([size="xs"][single-char]) .badge {
padding: 1px;
width: calc(var(--ag-font-size-xs));
height: calc(var(--ag-font-size-xs));
}
:host([size="sm"][single-char]) .badge {
padding: 2px;
width: var(--ag-font-size-sm);
height: var(--ag-font-size-sm);
}
:host([size="md"][single-char]) .badge {
padding: 2px;
width: var(--ag-font-size-base);
height: var(--ag-font-size-base);
}
`;
@property({ type: String, reflect: true })
declare variant: 'default' | 'success' | 'warning' | 'primary' | 'danger' | 'info' | 'neutral' | 'monochrome';
@property({ type: String, reflect: true })
declare size: 'xs' | 'sm' | 'md';
@property({ type: Boolean, reflect: true })
declare dot: boolean;
@property({ type: Number })
declare value: number | null;
@property({ type: Number })
declare max: number;
@property({ type: Boolean, reflect: true })
declare interactive: boolean;
@property({ type: String, attribute: 'status-label' })
declare statusLabel: string | null;
@property({ type: String })
declare live: 'off' | 'polite' | 'assertive';
// Hidden from assistive technologies
@property({ type: Boolean, attribute: 'hidden-from-at' })
declare hiddenFromAT: boolean;
constructor() {
super();
this.variant = 'default';
this.size = 'md';
this.dot = false;
this.value = null;
this.max = 99;
this.interactive = false;
this.statusLabel = null;
this.live = 'off';
this.hiddenFromAT = false;
}
private get _displayValue() {
if (this.value === null || this.value === undefined) {
return '';
}
if (this.value > this.max) {
return `${this.max}+`;
}
return this.value.toString();
}
private _handleClick() {
if (this.interactive) {
this.dispatchEvent(new CustomEvent('badge:click', { bubbles: true, composed: true }));
}
}
private _handleKeydown(e: KeyboardEvent) {
if (this.interactive && (e.key === 'Enter' || e.key === ' ')) {
this.dispatchEvent(new CustomEvent('badge:click', { bubbles: true, composed: true }));
}
}
connectedCallback() {
super.connectedCallback();
if (this.live !== 'off') {
this._updateLiveRegion();
}
}
updated(changedProperties: Map) {
super.updated(changedProperties);
this._checkSingleChar();
if (changedProperties.has('value') && this.live !== 'off') {
this._updateLiveRegion();
}
}
private _checkSingleChar() {
let text = this._displayValue || '';
const slot = this.shadowRoot?.querySelector('slot');
if (slot) {
const nodes = slot.assignedNodes({ flatten: true });
text += nodes.map(node => node.textContent || '').join('');
}
text = text.replace(/\s+/g, '');
if (text.length === 1 && !this.dot) {
this.setAttribute('single-char', 'true');
} else {
this.removeAttribute('single-char');
}
}
private _onSlotChange() {
this._checkSingleChar();
}
private _updateLiveRegion() {
let liveRegion = document.getElementById('ag-badge-live');
if (!liveRegion) {
liveRegion = document.createElement('ag-visually-hidden');
liveRegion.id = 'ag-badge-live';
liveRegion.setAttribute('aria-live', this.live);
liveRegion.setAttribute('aria-atomic', 'true');
document.body.appendChild(liveRegion);
}
liveRegion.setAttribute('aria-live', this.live);
if (this.value !== null && this.value !== undefined) {
liveRegion.textContent = this.statusLabel ? this.statusLabel : `${this._displayValue} items`;
} else {
liveRegion.textContent = this.statusLabel || '';
}
}
willUpdate(changedProperties: Map) {
if (changedProperties.has('dot') || changedProperties.has('statusLabel')) {
this.hiddenFromAT = (this.dot && !this.statusLabel) || this.hiddenFromAT;
}
}
render() {
const tabindex = this.interactive ? '0' : undefined;
return html`
${this.dot ? '' : this._displayValue}
`;
}
}