import { LitElement, html, css } from 'lit'; import { customElement, property } from 'lit/decorators.js'; // Import official USWDS compiled CSS import '../../styles/styles.css'; /** * USA Checkbox Web Component * * A simple, accessible USWDS checkbox implementation as a custom element. * Uses official USWDS classes and styling with minimal custom code. * * @element usa-checkbox * @fires change - Dispatched when the checkbox state changes * @fires input - Dispatched when the checkbox state changes (for consistency) * * @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-checkbox/src/styles/_usa-checkbox.scss * @uswds-docs https://designsystem.digital.gov/components/checkbox/ * @uswds-guidance https://designsystem.digital.gov/components/checkbox/#guidance * @uswds-accessibility https://designsystem.digital.gov/components/checkbox/#accessibility */ @customElement('usa-checkbox') export class USACheckbox extends LitElement { static override styles = css` :host { display: block; } `; @property({ type: String }) name = ''; @property({ type: String }) value = ''; @property({ type: Boolean, reflect: true }) checked = false; @property({ type: String }) label = ''; @property({ type: String }) description = ''; @property({ type: String }) error = ''; @property({ type: Boolean, reflect: true }) disabled = false; @property({ type: Boolean, reflect: true }) required = false; @property({ type: Boolean, reflect: true }) indeterminate = false; @property({ type: Boolean, reflect: true }) tile = false; private checkboxElement?: HTMLInputElement; private _checkboxId?: string; private usingUSWDSEnhancement = false; // Store USWDS module for cleanup // 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'); // Don't set role on the host element - it will be on the input // Initialize progressive enhancement this.initializeUSWDSCheckbox(); } override firstUpdated() { // 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 // Get reference to the checkbox element after first render this.checkboxElement = this.querySelector('input[type="checkbox"]') as HTMLInputElement; if (this.checkboxElement) { this.updateCheckboxElement(); } } override updated(_changedProperties: Map) { // Update the checkbox element if it exists if (this.checkboxElement) { this.updateCheckboxElement(); } } private updateCheckboxElement() { if (!this.checkboxElement) return; // Update checkbox properties this.checkboxElement.name = this.name; this.checkboxElement.value = this.value; this.checkboxElement.checked = this.checked; this.checkboxElement.disabled = this.disabled; this.checkboxElement.required = this.required; this.checkboxElement.indeterminate = this.indeterminate; // Update classes // Remove existing USWDS classes const classesToRemove = Array.from(this.checkboxElement.classList).filter((className) => className.startsWith('usa-checkbox__input') ); classesToRemove.forEach((className) => this.checkboxElement?.classList.remove(className)); // Always add base usa-checkbox__input class this.checkboxElement.classList.add('usa-checkbox__input'); // Add tile class if needed if (this.tile) { this.checkboxElement.classList.add('usa-checkbox__input--tile'); } // Add error class if error exists if (this.error) { this.checkboxElement.classList.add('usa-input--error'); } // Update ARIA attributes if (this.error) { this.checkboxElement.setAttribute('aria-invalid', 'true'); } else { this.checkboxElement.removeAttribute('aria-invalid'); } // Update data-indeterminate attribute for USWDS CSS compatibility // USWDS CSS supports both :indeterminate pseudo-class and [data-indeterminate] attribute if (this.indeterminate) { this.checkboxElement.setAttribute('data-indeterminate', 'true'); } else { this.checkboxElement.removeAttribute('data-indeterminate'); } } private handleChange(e: Event) { const checkbox = e.target as HTMLInputElement; this.checked = checkbox.checked; // Dispatch both change and input events for consistency with other form elements this.dispatchEvent( new CustomEvent('change', { detail: { checked: this.checked, value: this.value, name: this.name, }, bubbles: true, composed: true, }) ); this.dispatchEvent( new CustomEvent('input', { detail: { checked: this.checked, value: this.value, name: this.name, }, bubbles: true, composed: true, }) ); } private get checkboxId() { // Always check for element id first, then use cached generated id if (this.id) { return this.id; } if (!this._checkboxId) { this._checkboxId = `checkbox-${Math.random().toString(36).substring(2, 11)}`; } return this._checkboxId; } private async initializeUSWDSCheckbox() { // Prevent multiple initializations if (this.usingUSWDSEnhancement) { return; } try { if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') { const USWDS = (window as any).USWDS; if (USWDS.checkbox && typeof USWDS.checkbox.on === 'function') { USWDS.checkbox.on(this); return; } } } catch (error) { // Silently continue with CSS-only behavior } } override disconnectedCallback() { super.disconnectedCallback(); this.cleanupUSWDS(); } /** * Clean up USWDS module on component destruction */ private cleanupUSWDS() { if (typeof window !== 'undefined' && typeof (window as any).USWDS !== 'undefined') { const USWDS = (window as any).USWDS; if (USWDS.checkbox?.off) { try { USWDS.checkbox.off(this); } catch (error) { // Silently handle cleanup errors } } } } private renderError(checkboxId: string) { if (!this.error) return ''; return html` Error: ${this.error} `; } private renderDescription(checkboxId: string) { if (!this.description) return ''; return html` ${this.description} `; } override render() { const checkboxId = this.checkboxId; const describedByIds: string[] = []; if (this.description) { describedByIds.push(`${checkboxId}-description`); } if (this.error) { describedByIds.push(`${checkboxId}-error`); } // Build wrapper classes const wrapperClasses = ['usa-checkbox', this.tile ? 'usa-checkbox--tile' : ''] .filter(Boolean) .join(' '); const ariaDescribedby = describedByIds.length > 0 ? describedByIds.join(' ') : undefined; return html` ${this.renderError(checkboxId)}
`; } }