/** * @component ct-progress * @description Progress indicator component that displays completion percentage or indeterminate loading state * * @tag ct-progress * * @attribute {number} value - Current progress value (0-100). Defaults to 0. * @attribute {number} max - Maximum value for the progress bar. Defaults to 100. * @attribute {boolean} indeterminate - Whether the progress bar is in indeterminate state (loading animation). * * @csspart base - The progress bar container element * @csspart indicator - The progress indicator element that shows the fill * * @example * ```html * * * * * * * * * ``` * * @accessibility * - Uses role="progressbar" with proper ARIA attributes * - Provides aria-valuenow, aria-valuemin, aria-valuemax * - Updates aria-valuetext with percentage or "Loading" for indeterminate state */ import { html, PropertyValues, unsafeCSS } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { styleMap } from "lit/directives/style-map.js"; import { BaseElement } from "../../core/base-element.ts"; import { progressStyles } from "./styles.ts"; export class CTProgress extends BaseElement { static override properties = { value: { type: Number }, max: { type: Number }, indeterminate: { type: Boolean, reflect: true }, }; static override styles = unsafeCSS(progressStyles); declare value: number; declare max: number; declare indeterminate: boolean; private _indicatorElement: HTMLElement | null = null; constructor() { super(); this.value = 0; this.max = 100; this.indeterminate = false; } get indicatorElement(): HTMLElement | null { if (!this._indicatorElement) { this._indicatorElement = this.shadowRoot?.querySelector(".indicator") as | HTMLElement | null; } return this._indicatorElement; } override connectedCallback() { super.connectedCallback(); // Set up ARIA attributes this.setAttribute("role", "progressbar"); this.setAttribute("aria-valuemin", "0"); } override willUpdate(changedProperties: PropertyValues) { // Clamp value within bounds if (changedProperties.has("value") || changedProperties.has("max")) { const clampedValue = Math.max(0, Math.min(this.value, this.max)); if (this.value !== clampedValue) { this.value = clampedValue; } } } override updated(changedProperties: PropertyValues) { if ( changedProperties.has("value") || changedProperties.has("max") || changedProperties.has("indeterminate") ) { this.updateProgress(); this.updateAriaAttributes(); } } override render() { const classes = { progress: true, indeterminate: this.indeterminate, }; const indicatorStyles = this.getIndicatorStyles(); return html`
`; } private getIndicatorStyles() { if (this.indeterminate) { return {}; } const percentage = this.getPercentage(); return { width: `${percentage}%`, }; } private updateAriaAttributes(): void { this.setAttribute("aria-valuemax", this.max.toString()); if (!this.indeterminate) { this.setAttribute("aria-valuenow", this.value.toString()); const percentage = this.getPercentage(); this.setAttribute("aria-valuetext", `${Math.round(percentage)}%`); } else { this.removeAttribute("aria-valuenow"); this.setAttribute("aria-valuetext", "Loading"); } } private getPercentage(): number { if (this.max === 0) return 0; return (this.value / this.max) * 100; } private updateProgress(): void { // Progress visual update is now handled by render method // This method is kept for compatibility with public API } /** * Set the progress value programmatically */ setValue(value: number): void { this.value = value; } /** * Get the current progress percentage */ getPercentageValue(): number { return this.getPercentage(); } /** * Set indeterminate state */ setIndeterminate(indeterminate: boolean): void { this.indeterminate = indeterminate; } /** * Check if progress is complete */ isComplete(): boolean { return !this.indeterminate && this.value >= this.max; } /** * Reset progress to 0 */ reset(): void { this.value = 0; this.indeterminate = false; } } globalThis.customElements.define("ct-progress", CTProgress);