import { LitElement, html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; // Import official USWDS compiled CSS import '../../styles/styles.css'; /** * USA Tag Web Component * * A simple, accessible USWDS tag implementation as a custom element. * Uses official USWDS classes and styling with minimal custom code. * * @element usa-tag * @fires tag-remove - Dispatched when a removable tag is removed * * @see README.mdx - Complete API documentation, usage examples, and implementation notes * @see CHANGELOG.mdx - Component version history and breaking changes * @see TESTING.mdx - Testing documentation and coverage reports * * @uswds-css-reference https://github.com/uswds/uswds/tree/develop/packages/usa-tag/src/styles/_usa-tag.scss * @uswds-docs https://designsystem.digital.gov/components/tag/ * @uswds-guidance https://designsystem.digital.gov/components/tag/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/tag/#accessibility */ @customElement('usa-tag') export class USATag extends LitElement { static override styles = css` :host { display: inline-block; } /* Hide slotted elements that appear as direct children (light DOM slot workaround) */ :host > [slot] { display: none !important; } `; @property({ type: String }) text = ''; @property({ type: Boolean, reflect: true }) big = false; @property({ type: Boolean, reflect: true }) removable = false; @property({ type: String }) value = ''; // Use light DOM for USWDS compatibility protected override createRenderRoot(): HTMLElement { return this as any; } override connectedCallback() { super.connectedCallback(); // Set web component managed flag to prevent USWDS auto-initialization conflicts this.setAttribute('data-web-component-managed', 'true'); } override firstUpdated(changedProperties: Map) { super.firstUpdated(changedProperties); // Move slotted content into their slot placeholders (light DOM slot workaround) this.moveSlottedContent(); } private moveSlottedContent() { // In light DOM, slots don't automatically project content // We need to manually move slotted elements into their slot locations // Handle default slot (elements without slot attribute) const defaultSlot = this.querySelector('slot:not([name])'); if (defaultSlot) { // Get all direct children that should go in the default slot // Exclude: elements with slot attribute, STYLE tags, and elements already inside .usa-tag const defaultSlottedElements = Array.from(this.children).filter( (el) => !el.hasAttribute('slot') && el.tagName !== 'STYLE' && !el.classList.contains('usa-tag') ); if (defaultSlottedElements.length > 0) { // Create a document fragment to hold the slotted content const fragment = document.createDocumentFragment(); defaultSlottedElements.forEach((el) => { fragment.appendChild(el); }); // Replace the slot with the fragment defaultSlot.replaceWith(fragment); } else { // No default slot content, just remove the empty slot defaultSlot.remove(); } } } private handleRemove(e: Event) { e.stopPropagation(); // Dispatch remove event this.dispatchEvent( new CustomEvent('tag-remove', { detail: { text: this.text, value: this.value, }, bubbles: true, composed: true, }) ); // Remove the tag from DOM this.remove(); } override disconnectedCallback() { super.disconnectedCallback(); // Note: USWDS tags are purely presentational with no JavaScript behavior console.log('Tag: Cleanup complete (no USWDS JavaScript required)'); } private renderRemoveButton() { if (!this.removable) return ''; return html``; } override render() { // Determine CSS classes const tagClasses = [ 'usa-tag', this.big ? 'usa-tag--big' : '', this.removable ? 'usa-tag--removable' : '', ] .filter(Boolean) .join(' '); // prettier-ignore return html` ${this.text ? this.text : html``} ${this.renderRemoveButton()} `; } }