import { html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { USWDSBaseComponent } from '../../utils/base-component.js'; // Import official USWDS compiled CSS import '../../styles/styles.css'; /** * USA List Web Component * * Simple presentational wrapper for USWDS list styling. * Organizes information into discrete sequential sections using only USWDS CSS classes. * * @element usa-list * * @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-list/src/styles/_usa-list.scss * @uswds-docs https://designsystem.digital.gov/components/list/ * @uswds-guidance https://designsystem.digital.gov/components/list/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/list/#accessibility * * @example * ```html * *
  • First item
  • *
  • Second item
  • *
  • Third item
  • *
    * ``` */ @customElement('usa-list') export class USAList extends USWDSBaseComponent { static override styles = css` :host { display: block; } :host([hidden]) { display: none; } `; @property({ type: String }) type: 'unordered' | 'ordered' = 'unordered'; @property({ type: Boolean, reflect: true }) unstyled = false; private slottedContent: string = ''; private reorganizeTimer: number | null = null; // Light DOM is handled by USWDSBaseComponent override connectedCallback() { super.connectedCallback(); // Set web component managed flag to prevent USWDS auto-initialization conflicts this.setAttribute('data-web-component-managed', 'true'); // Capture any initial content before render if (this.childNodes.length > 0) { this.slottedContent = this.innerHTML; this.innerHTML = ''; } // Set appropriate ARIA role if needed if (this.type === 'ordered') { this.setAttribute('role', 'list'); } // Force re-render if innerHTML was manipulated this.requestUpdate(); } 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.reorganizeListItems(); } override updated(changedProperties: Map) { super.updated(changedProperties); // Guard: Don't manipulate DOM if component is not connected if (!this.isConnected) { return; } if (changedProperties.has('type')) { // Set appropriate ARIA role if needed if (this.type === 'ordered') { this.setAttribute('role', 'list'); } else { this.removeAttribute('role'); } } // Apply captured content using DOM manipulation this.applySlottedContent(); // Always reorganize items after any update to catch dynamically added content this.reorganizeListItems(); } private applySlottedContent() { if (this.slottedContent) { const slotElement = this.querySelector('slot'); if (slotElement) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = this.slottedContent; slotElement.replaceWith(...Array.from(tempDiv.childNodes)); } } } private reorganizeListItems() { // Immediate check for the list element const listElement = this.querySelector(':scope > ul, :scope > ol'); // If no list element yet, defer to next tick if (!listElement) { // Clear any existing timer before setting a new one if (this.reorganizeTimer !== null) { clearTimeout(this.reorganizeTimer); } // Timer is cleaned up in disconnectedCallback() - prevents memory leaks this.reorganizeTimer = window.setTimeout(() => this.reorganizeListItems(), 10); return; } // Only reorganize if there are li elements that are direct children of this component // and not already in the list element const directLiElements = Array.from(this.children).filter( (child) => child.tagName === 'LI' && child.parentElement === this && !listElement.contains(child) ); this.debug('reorganizeListItems', { listElementFound: !!listElement, directLiElements: directLiElements.length, totalChildren: this.children.length, }); // If we don't have any misplaced li elements, don't reorganize if (directLiElements.length === 0) return; // Move them into the list element, preserving their order directLiElements.forEach((item) => { this.debug('Moving li element to list', { itemText: item.textContent }); listElement.appendChild(item); }); this.debug('Reorganization complete', { listElementChildren: listElement.children.length, componentChildren: this.children.length, }); } // Public method to force reorganization - useful for testing forceReorganize() { this.reorganizeListItems(); } override disconnectedCallback() { super.disconnectedCallback(); // Clear any pending reorganization timers if (this.reorganizeTimer !== null) { clearTimeout(this.reorganizeTimer); this.reorganizeTimer = null; } } // Use light DOM for USWDS compatibility protected override createRenderRoot(): HTMLElement { return this as any; } override render() { const classes = ['usa-list', this.unstyled ? 'usa-list--unstyled' : ''] .filter(Boolean) .join(' '); if (this.type === 'ordered') { return html`
    `; } else { return html` `; } } }