import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { initializeUSWDSComponent, cleanupUSWDSComponent } from '../../utils/uswds-loader.js';
// Import official USWDS compiled CSS
import '../../styles/styles.css';
export interface StepItem {
label: string;
status: 'complete' | 'current' | 'incomplete';
}
/**
* USA Step Indicator Web Component
*
* A simple, accessible USWDS step indicator implementation as a custom element.
* Uses official USWDS classes and styling with minimal custom code.
*
* @element usa-step-indicator
* @fires step-click - Dispatched when step 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-step-indicator/src/styles/_usa-step-indicator.scss
* @uswds-docs https://designsystem.digital.gov/components/step-indicator/
* @uswds-guidance https://designsystem.digital.gov/components/step-indicator/#guidance
* @uswds-accessibility https://designsystem.digital.gov/components/step-indicator/#accessibility
*/
@customElement('usa-step-indicator')
export class USAStepIndicator extends LitElement {
// Store USWDS module for cleanup
private uswdsModule: any = null;
static override styles = css`
:host {
display: block;
}
`;
@property({ type: Array })
steps: StepItem[] = [];
@property({ type: Number })
currentStep = 1;
@property({ type: Boolean })
showLabels = true;
@property({ type: Boolean })
counters = false;
@property({ type: Boolean })
center = false;
// Alias for center property to match USWDS naming
get centered() {
return this.center;
}
set centered(value: boolean) {
this.center = value;
}
@property({ type: Boolean })
small = false;
@property({ type: String })
heading = '';
@property({ type: String, attribute: 'aria-label' })
override ariaLabel = 'Step indicator';
private slottedContent: string = '';
// Use light DOM for USWDS compatibility
protected override createRenderRoot(): HTMLElement {
return this as any;
}
override connectedCallback() {
super.connectedCallback?.();
// Capture any initial content before render
if (this.childNodes.length > 0 && this.steps.length === 0) {
this.slottedContent = this.innerHTML;
this.innerHTML = '';
}
// Set web component managed flag to prevent USWDS auto-initialization conflicts
this.setAttribute('data-web-component-managed', 'true');
// If steps array is provided, sync currentStep with current status
this.syncCurrentStep();
}
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.initializeUSWDSStepIndicator();
}
override updated(changedProperties: Map) {
super.updated(changedProperties);
// Handle currentStep changes during the current update cycle
// Don't trigger additional updates that would cause re-render loops
if (changedProperties.has('currentStep') && !changedProperties.has('steps')) {
// Update the internal step statuses without triggering a new update
this.updateStepStatusesSync();
}
// Apply captured content using DOM manipulation
this.applySlottedContent();
}
private applySlottedContent() {
if (this.slottedContent) {
const slotElement = this.querySelector('slot');
if (slotElement && this.steps.length === 0) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = this.slottedContent;
slotElement.replaceWith(...Array.from(tempDiv.childNodes));
}
}
}
private updateStepStatusesSync() {
// Update step statuses synchronously without triggering new updates
// This prevents the "scheduled an update after update completed" warning
for (let i = 0; i < this.steps.length; i++) {
const newStatus =
i + 1 < this.currentStep
? ('complete' as const)
: i + 1 === this.currentStep
? ('current' as const)
: ('incomplete' as const);
// Only update if status actually changed to avoid unnecessary work
if (this.steps[i]!.status !== newStatus) {
// Create new object to ensure reactivity works properly
this.steps[i] = { ...this.steps[i]!, status: newStatus };
}
}
// Force a re-render since we modified the array in place
this.requestUpdate('steps');
}
private syncCurrentStep(): void {
// Find current step from steps array if not explicitly set
const currentIndex = this.steps.findIndex((step) => step.status === 'current');
if (currentIndex !== -1 && this.currentStep !== currentIndex + 1) {
this.currentStep = currentIndex + 1;
}
// Check if any updates are actually needed to avoid unnecessary re-renders
const needsUpdate = false;
if (needsUpdate) {
this.requestUpdate();
}
}
private renderStepLabel(step: StepItem) {
if (!this.showLabels) return '';
const isCurrent = step.status === 'current';
const isComplete = step.status === 'complete';
return html`
${step.label}
${isComplete ? 'completed' : isCurrent ? 'current' : 'not completed'}
`;
}
private renderStep(step: StepItem, _index: number): any {
const isCurrent = step.status === 'current';
const isComplete = step.status === 'complete';
const isIncomplete = step.status === 'incomplete';
const segmentClasses = [
'usa-step-indicator__segment',
isComplete ? 'usa-step-indicator__segment--complete' : '',
isCurrent ? 'usa-step-indicator__segment--current' : '',
isIncomplete ? 'usa-step-indicator__segment--incomplete' : '',
]
.filter(Boolean)
.join(' ');
return html`
${this.renderStepLabel(step)}
`;
}
private async initializeUSWDSStepIndicator() {
// Prevent duplicate initialization
if (this.uswdsModule) {
console.log('⚠️ Step Indicator: Already initialized, skipping duplicate initialization');
return;
}
try {
// Use standardized USWDS loader utility for consistency
await this.updateComplete;
const element = this.querySelector('.usa-step-indicator');
if (!element) {
console.warn('Step indicator element not found');
return;
}
// Let USWDS handle the component using standard loader
this.uswdsModule = await initializeUSWDSComponent(element, 'step-indicator');
console.log('✅ USWDS step indicator initialized successfully');
} catch (error) {
console.warn('🔧 Step Indicator: USWDS integration failed:', error);
}
}
override disconnectedCallback() {
super.disconnectedCallback();
// Clean up USWDS module using standardized loader
cleanupUSWDSComponent(this, this.uswdsModule);
this.uswdsModule = null;
}
private renderHeader() {
if (this.steps.length === 0) return '';
const currentStepData = this.steps[this.currentStep - 1];
const currentStepLabel = currentStepData ? currentStepData.label : '';
return html`
`;
}
override render() {
const containerClasses = [
'usa-step-indicator',
this.counters ? 'usa-step-indicator--counters' : '',
this.centered ? 'usa-step-indicator--center' : '',
this.small ? 'usa-step-indicator--small' : '',
!this.showLabels ? 'usa-step-indicator--no-labels' : '',
]
.filter(Boolean)
.join(' ');
return html`
${this.steps.length > 0
? this.steps.map((step, index) => this.renderStep(step, index))
: ''}
${this.renderHeader()}
`;
}
// Public API methods
nextStep() {
if (this.currentStep < this.steps.length) {
this.currentStep += 1;
}
}
previousStep() {
if (this.currentStep > 1) {
this.currentStep -= 1;
}
}
goToStep(stepNumber: number) {
if (stepNumber >= 1 && stepNumber <= this.steps.length) {
this.currentStep = stepNumber;
}
}
markStepComplete(stepNumber: number) {
if (stepNumber >= 1 && stepNumber <= this.steps.length) {
this.steps = this.steps.map((step, index) =>
index + 1 === stepNumber ? { ...step, status: 'complete' as const } : step
);
this.requestUpdate();
}
}
setCurrentStep(stepNumber: number) {
this.goToStep(stepNumber);
}
}