import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { BootstrapElement, defineElement, type Variant } from '@bootstrap-wc/core'; /** * `` — Bootstrap progress bar. The host carries `.progress` so * Bootstrap's `.progress-stacked > .progress` selector can match when the * element is slotted inside `` through shadow-DOM * flattening. * * @slot - Optional content rendered inside the bar (e.g. label text). */ export class BsProgress extends BootstrapElement { @property({ type: Number }) value = 0; @property({ type: Number }) min = 0; @property({ type: Number }) max = 100; @property({ type: String }) variant?: Variant; /** Applies `.bg-{variant}` (solid) when set — Bootstrap's default background. */ @property({ type: String, attribute: 'bar-bg' }) barBg?: Variant; /** Applies `.text-bg-{variant}` so text and background are paired (Bootstrap's labels demo). */ @property({ type: String, attribute: 'bar-text-bg' }) barTextBg?: Variant; @property({ type: Boolean }) striped = false; @property({ type: Boolean }) animated = false; @property({ type: String }) label?: string; /** Accessible label for screen readers, applied as `aria-label` on the host. */ @property({ type: String, attribute: 'aria-label', reflect: true }) ariaLabelAttr?: string; private _pct(): number { const range = this.max - this.min; if (range <= 0) return 0; return Math.max(0, Math.min(100, ((this.value - this.min) / range) * 100)); } /** * True when this element is a direct child of ``. In * that case the host carries the width style, the inner `.progress-bar` * fills 100% of it, and Bootstrap's stacked container handles layout. */ private _isStackedSegment(): boolean { const parent = this.parentElement; return !!parent && parent.tagName === 'BS-PROGRESS-STACKED'; } override connectedCallback(): void { super.connectedCallback(); if (!this.hasAttribute('role')) this.setAttribute('role', 'progressbar'); } override updated(changed: Map): void { super.updated(changed); this.setAttribute('aria-valuenow', String(this.value)); this.setAttribute('aria-valuemin', String(this.min)); this.setAttribute('aria-valuemax', String(this.max)); if (this._isStackedSegment()) { // Stacked: segment width goes on the host so siblings add up correctly. this.style.width = `${this._pct()}%`; } else if (this.style.width) { this.style.removeProperty('width'); } } protected override hostClasses(): string { return 'progress'; } override render() { const stacked = this._isStackedSegment(); // Prefer the explicit bar-bg / bar-text-bg attrs when set, fall back to // `variant` for backwards compat (pre-0.4 it mapped to `.bg-{variant}`). const solidBg = this.barBg ?? this.variant; const textBg = this.barTextBg; const barClasses = classMap({ 'progress-bar': true, [`bg-${solidBg}`]: !!solidBg && !textBg, [`text-bg-${textBg}`]: !!textBg, 'progress-bar-striped': this.striped || this.animated, 'progress-bar-animated': this.animated, }); // In stacked mode the outer width lives on the host; bar fills 100% // (which is also Bootstrap's stacked CSS default — we mirror it inline so // the rule resolves even though it can't pierce our shadow boundary). const barStyle = stacked ? 'width: 100%' : `width: ${this._pct()}%`; return html`
${this.label ?? html``}
`; } } /** * `` — container for multiple `` segments. * Host carries `.progress-stacked` so Bootstrap's `.progress-stacked > .progress` * selectors match slotted `` children (whose hosts carry * `.progress`). */ export class BsProgressStacked extends BootstrapElement { protected override hostClasses(): string { return 'progress-stacked'; } override render() { return html``; } } defineElement('bs-progress', BsProgress); defineElement('bs-progress-stacked', BsProgressStacked); declare global { interface HTMLElementTagNameMap { 'bs-progress': BsProgress; 'bs-progress-stacked': BsProgressStacked; } }