import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { BootstrapElement, defineElement, type Size, type Variant } from '@bootstrap-wc/core'; export type ButtonType = 'button' | 'submit' | 'reset'; export type ButtonStyle = 'solid' | 'outline' | 'link'; /** * `` — Bootstrap button as a web component. * * The host element IS the button. Bootstrap's `.btn`, `.btn-{variant}`, size * and state classes are applied to the host, so that container components * (`.btn-group > .btn + .btn`) can style it correctly across shadow * boundaries via slot flattening. A shadow slot simply projects the author's * content; form submission, activation, and link navigation are handled on * the host. * * Set `variant="none"` to render the bare `.btn` base class without a color * variant. Set `toggle` to have user activation flip the `active` state * (matches Bootstrap's `data-bs-toggle="button"` plugin behavior). * * @slot - Button content (text and/or icon). * @fires bs-click - Bubbles on user activation (suppressed when disabled). */ export class BsButton extends BootstrapElement { static formAssociated = true; @property({ type: String }) variant: Variant | 'none' = 'primary'; @property({ type: String, attribute: 'button-style' }) buttonStyle: ButtonStyle = 'solid'; @property({ type: String }) size?: Size; @property({ type: String }) type: ButtonType = 'button'; @property({ type: Boolean, reflect: true }) disabled = false; @property({ type: Boolean, reflect: true }) active = false; @property({ type: Boolean, reflect: true }) toggle = false; @property({ type: String }) href?: string; @property({ type: String }) target?: string; @property({ type: String }) rel?: string; private _internals?: ElementInternals; constructor() { super(); if (typeof this.attachInternals === 'function') this._internals = this.attachInternals(); } override connectedCallback(): void { super.connectedCallback(); if (!this.hasAttribute('role')) this.setAttribute('role', this.href ? 'link' : 'button'); if (!this.hasAttribute('tabindex')) this.tabIndex = this.disabled ? -1 : 0; this.addEventListener('click', this._onClick); this.addEventListener('keydown', this._onKeydown); } override disconnectedCallback(): void { super.disconnectedCallback(); this.removeEventListener('click', this._onClick); this.removeEventListener('keydown', this._onKeydown); } protected override hostClasses(): string { const parts = ['btn']; if (this.buttonStyle === 'link') { parts.push('btn-link'); } else if (this.variant !== 'none') { const prefix = this.buttonStyle === 'outline' ? 'btn-outline-' : 'btn-'; parts.push(`${prefix}${this.variant}`); } if (this.size && this.size !== 'md') parts.push(`btn-${this.size}`); if (this.active) parts.push('active'); if (this.disabled) parts.push('disabled'); return parts.join(' '); } override updated(changed: Map): void { super.updated(changed); if (changed.has('disabled')) { this.tabIndex = this.disabled ? -1 : 0; this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); } if (changed.has('active')) { this.setAttribute('aria-pressed', this.active ? 'true' : 'false'); } if (changed.has('href')) { this.setAttribute('role', this.href ? 'link' : 'button'); } } private _onClick = (ev: MouseEvent) => { if (this.disabled) { ev.preventDefault(); ev.stopImmediatePropagation(); return; } if (this.toggle) { this.active = !this.active; } if (this.href && ev.target === this) { const target = this.target || '_self'; if (target === '_self') window.location.href = this.href; else window.open(this.href, target, this.rel ?? ''); return; } if (this.type === 'submit' && this._internals?.form) { this._internals.form.requestSubmit(); } else if (this.type === 'reset' && this._internals?.form) { this._internals.form.reset(); } this.dispatchEvent( new CustomEvent('bs-click', { bubbles: true, composed: true, detail: { originalEvent: ev }, }), ); }; private _onKeydown = (ev: KeyboardEvent) => { if (this.disabled) return; if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); this.click(); } }; override render() { return html`${nothing}`; } } defineElement('bs-button', BsButton); declare global { interface HTMLElementTagNameMap { 'bs-button': BsButton; } }