import { html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; // Import base component and utils import { USWDSBaseComponent } from '../../utils/base-component.js'; // Import official USWDS compiled CSS import '../../styles/styles.css'; /** * USA Link Web Component * * A simple, accessible USWDS link implementation as a custom element. * Provides consistent link styling with support for external links, * different visual variants, and proper accessibility attributes. * Uses official USWDS classes and styling with minimal custom code. * * @element usa-link * @fires link-click - Dispatched when the link is clicked * * @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-link/src/styles/_usa-link.scss * @uswds-docs https://designsystem.digital.gov/components/link/ * @uswds-guidance https://designsystem.digital.gov/components/link/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/link/#accessibility */ @customElement('usa-link') export class USALink extends USWDSBaseComponent { // No shadow DOM styles since we use light DOM @property({ type: String }) href = ''; @property({ type: String }) target = ''; @property({ type: String }) rel = ''; @property({ type: String }) variant: 'default' | 'external' | 'alt' | 'unstyled' = 'default'; @property({ type: Boolean, reflect: true }) external = false; @property({ type: Boolean, reflect: true }) unstyled = false; @property({ type: String, attribute: 'aria-label' }) override ariaLabel = ''; @property({ type: String }) download = ''; override connectedCallback() { super.connectedCallback(); // Set web component managed flag to prevent USWDS auto-initialization conflicts this.setAttribute('data-web-component-managed', 'true'); this.debug('Link component connected', { href: this.href, variant: this.variant }); } override firstUpdated(changedProperties: Map) { // ARCHITECTURE: Script Tag Pattern // USWDS is loaded globally via script tag in .storybook/preview-head.html // Components just render HTML - USWDS enhances automatically via window.USWDS super.firstUpdated(changedProperties); this.setupEventListeners(); } private setupEventListeners() { const anchor = this.querySelector('a'); if (anchor) { anchor.addEventListener('click', this.handleLinkClick.bind(this)); } } private handleLinkClick(event: Event) { const isExternal = this.external || this.isExternalLink(this.href); this.dispatchEvent(new CustomEvent('link-click', { detail: { href: this.href, external: isExternal, target: isExternal ? '_blank' : this.target, event: event }, bubbles: true, composed: true })); } private isExternalLink(href: string): boolean { if (!href) return false; // Check if it's a different domain if (href.startsWith('http://') || href.startsWith('https://')) { try { const url = new URL(href); return url.hostname !== window.location.hostname; } catch { return false; } } return false; } private getLinkClasses(): string { const classes: string[] = []; // Base link class - always add unless explicitly unstyled if (!this.unstyled && this.variant !== 'unstyled') { classes.push('usa-link'); } // Variant classes if (this.external || this.variant === 'external' || this.isExternalLink(this.href)) { classes.push('usa-link--external'); } if (this.variant === 'alt') { classes.push('usa-link--alt'); } return classes.join(' '); } private getRel(): string { let relValue = this.rel; // Auto-add security attributes for external links if (this.external || this.variant === 'external' || this.isExternalLink(this.href)) { const relParts = relValue ? relValue.split(' ') : []; if (!relParts.includes('noopener')) { relParts.push('noopener'); } if (!relParts.includes('noreferrer')) { relParts.push('noreferrer'); } relValue = relParts.join(' '); } return relValue; } private getTarget(): string { // Auto-set target for external links if not specified if (this.target) { return this.target; } if (this.external || this.variant === 'external' || this.isExternalLink(this.href)) { return '_blank'; } return ''; } override disconnectedCallback() { super.disconnectedCallback(); // Clean up USWDS behavior try { if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') { // USWDS available but no setup needed } } catch (error) { console.warn('📋 Link: Cleanup failed:', error); } // Additional cleanup for event listeners would go here } // Use light DOM for USWDS compatibility protected override createRenderRoot(): HTMLElement { return this as any; } override render() { // Debug logging available with ?debug=true const linkClasses = this.getLinkClasses(); const target = this.getTarget(); const rel = this.getRel(); // In light DOM, we need to move the content into the anchor // We'll do this after render in override updated() return html` `; } override updated(changedProperties: Map) { super.updated(changedProperties); // Move the original content into the anchor element using base class helper this.moveChildrenToElement('a'); } // Public API methods override click() { const link = this.querySelector('a'); if (link) { link.click(); } } override focus() { const link = this.querySelector('a'); if (link) { link.focus(); } } override blur() { const link = this.querySelector('a'); if (link) { link.blur(); } } }