import { LitElement, html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; // Removed unsafeHTML import - using safer HTML content handling // Import official USWDS compiled CSS import '../../styles/styles.css'; export interface ProcessItem { heading: string; content: string; } /** * USA Process List Web Component * * A simple, accessible USWDS process list implementation as a custom element. * Uses official USWDS classes and styling with minimal custom code. * * @element usa-process-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-process-list/src/styles/_usa-process-list.scss * @uswds-docs https://designsystem.digital.gov/components/process-list/ * @uswds-guidance https://designsystem.digital.gov/components/process-list/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/process-list/#accessibility */ @customElement('usa-process-list') export class USAProcessList extends LitElement { static override styles = css` :host { display: block; } /* Hide slotted elements that appear as direct children (light DOM slot workaround) */ :host > [slot] { display: none !important; } `; @property({ type: Array }) items: ProcessItem[] = []; @property({ type: String }) headingLevel = 'h4'; // 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-process-list const defaultSlottedElements = Array.from(this.children).filter( (el) => !el.hasAttribute('slot') && el.tagName !== 'STYLE' && !el.classList.contains('usa-process-list') ); 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 renderItemHeading(heading: string) { switch (this.headingLevel) { case 'h1': return html`

${heading}

`; case 'h2': return html`

${heading}

`; case 'h3': return html`

${heading}

`; case 'h4': return html`

${heading}

`; case 'h5': return html`
${heading}
`; case 'h6': return html`
${heading}
`; default: return html`

${heading}

`; } } private renderProcessItem(item: ProcessItem): any { return html`
  • ${this.renderItemHeading(item.heading)}
  • `; } override disconnectedCallback() { super.disconnectedCallback(); // Clean up USWDS behavior try { if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') { const USWDS = (window as any).USWDS; if (USWDS['process-list'] && typeof USWDS['process-list'].off === 'function') { USWDS['process-list'].off(this); } } } catch (error) { console.warn('📋 ProcessList: Cleanup failed:', error); } // Additional cleanup for event listeners would go here } override render() { // Always render the container to avoid hierarchy errors with light DOM slots return html`
      ${this.items.length === 0 ? html`` : this.items.map((item) => this.renderProcessItem(item))}
    `; } }