import { html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { USWDSBaseComponent } from '../../utils/base-component.js'; import { initializeFooter } from './usa-footer-behavior.js'; // Import official USWDS compiled CSS import '../../styles/styles.css'; export interface FooterLink { label: string; href: string; } export interface FooterSection { title: string; links: FooterLink[]; } /** * USA Footer Web Component * * Minimal wrapper around USWDS footer functionality. * Uses USWDS-mirrored behavior pattern for 100% behavioral parity. * * IMPORTANT: The footer and identifier are separate components in USWDS. * Place AFTER on the page, not inside it. * * @example * ```html * * * ``` * * @element usa-footer * @fires footer-link-click - Dispatched when a footer 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-js-reference https://github.com/uswds/uswds/tree/develop/packages/usa-footer/src/index.js * @uswds-css-reference https://github.com/uswds/uswds/tree/develop/packages/usa-footer/src/styles/_usa-footer.scss * @uswds-docs https://designsystem.digital.gov/components/footer/ * @uswds-guidance https://designsystem.digital.gov/components/footer/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/footer/#accessibility */ @customElement('usa-footer') export class USAFooter extends USWDSBaseComponent { static override styles = css` :host { display: block; } :host([hidden]) { display: none; } `; @property({ type: String }) variant: 'slim' | 'medium' | 'big' = 'medium'; @property({ type: String }) agencyName = ''; @property({ type: Array }) sections: FooterSection[] = []; private slottedContent: string = ''; // Store cleanup function from behavior private cleanup?: () => void; override connectedCallback() { super.connectedCallback(); this.setAttribute('data-web-component-managed', 'true'); // Capture any initial content before render if (this.childNodes.length > 0 && this.sections.length === 0) { this.slottedContent = this.innerHTML; this.innerHTML = ''; } } override disconnectedCallback() { super.disconnectedCallback(); this.cleanup?.(); } override async 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 // ARCHITECTURE: USWDS-Mirrored Behavior Pattern // Uses dedicated behavior file (usa-footer-behavior.ts) that replicates USWDS source exactly super.firstUpdated(changedProperties); // Wait for DOM to be fully rendered await this.updateComplete; await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined))); // Initialize using mirrored USWDS behavior this.cleanup = initializeFooter(this); } override updated(changedProperties: Map) { super.updated(changedProperties); // Apply captured content using DOM manipulation this.applySlottedContent(); } private applySlottedContent() { if (this.slottedContent) { const slotElement = this.querySelector('slot'); if (slotElement && this.sections.length === 0) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = this.slottedContent; slotElement.replaceWith(...Array.from(tempDiv.childNodes)); } } } private handleLinkClick(link: FooterLink, e: Event) { const event = new CustomEvent('footer-link-click', { detail: { label: link.label, href: link.href, }, bubbles: true, composed: true, cancelable: true, }); this.dispatchEvent(event); // Only navigate if event wasn't cancelled and href is provided if (!event.defaultPrevented && link.href) { // Allow the default link behavior to handle navigation return; } else { e.preventDefault(); } } private renderFooterNav() { if (this.sections.length === 0) return ''; if (this.variant === 'big') { return this.renderBigFooterNav(); } else { return this.renderMediumFooterNav(); } } private renderMediumFooterNav() { return html` `; } private renderBigFooterNav() { return html` `; } private renderFooterSecondary() { // Only render secondary section for medium and big variants // This provides agency info and contact details per USWDS structure if (this.variant === 'slim') { return ''; } return html` `; } private renderMediumFooterSections() { return this.sections.map((section) => this.renderMediumFooterSection(section)); } private renderMediumFooterSection(section: FooterSection) { // For medium footer, render section title as primary link (USWDS pattern) const primaryLink = section.links.length > 0 ? section.links[0] : { label: section.title, href: '#' }; return html` `; } private renderBigFooterSections() { return this.sections.map((section) => this.renderBigFooterSection(section)); } private renderBigFooterSection(section: FooterSection) { // For big footer, render collapsible sections with headings and sublists (USWDS pattern) return html`
`; } private renderSectionLinks(links: FooterLink[]) { return links.map((link) => this.renderSectionLink(link)); } private renderSectionLink(link: FooterLink) { return html` `; } // Use light DOM for USWDS compatibility protected override createRenderRoot(): HTMLElement { return this as any; } override render() { const footerClasses = ['usa-footer', `usa-footer--${this.variant}`].filter(Boolean).join(' '); return html` `; } }