import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts"; /** * CTLabel - Form field label with accessibility features * * @element ct-label * * @attr {string} for - ID of associated input element * @attr {boolean} required - Shows asterisk for required fields * @attr {boolean} disabled - Whether the label is disabled * * @slot - Default slot for label text * * @fires ct-label-click - Fired on click with detail: { targetId, targetElement } * * @example * Email Address * */ export class CTLabel extends BaseElement { static override styles = css` :host { display: inline-block; box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } .label { display: inline-flex; align-items: baseline; gap: 0.25rem; font-size: 0.875rem; font-weight: 500; line-height: 1.25rem; color: var(--foreground, hsl(0, 0%, 9%)); cursor: pointer; user-select: none; } .label.disabled { opacity: 0.5; cursor: not-allowed; } .required-indicator { color: var(--destructive, hsl(0, 100%, 50%)); font-weight: 600; line-height: 1; margin-left: 0.125rem; } /* When used with peer elements */ :host(:has(+ :disabled)), :host(:has(+ [disabled])) .label { opacity: 0.5; cursor: not-allowed; } `; static override properties = { for: { type: String }, required: { type: Boolean }, disabled: { type: Boolean }, }; declare for: string | null; declare required: boolean; declare disabled: boolean; constructor() { super(); this.for = null; this.required = false; this.disabled = false; } override connectedCallback() { super.connectedCallback(); this.addEventListener("click", this._handleClick); } override disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener("click", this._handleClick); } override render() { return html` `; } private _handleClick = (_event: Event): void => { // If label has a 'for' attribute, find and focus the associated element if (this.for && !this.disabled) { // Look for the element in the parent document const root = this.getRootNode() as Document | ShadowRoot; const targetElement = root.querySelector( `#${CSS.escape(this.for)}`, ) as HTMLElement; if (targetElement) { // Focus the element if it's focusable if ( "focus" in targetElement && typeof targetElement.focus === "function" ) { targetElement.focus(); // For custom elements, also try clicking them if (targetElement.tagName.includes("-")) { targetElement.click(); } } // Emit custom event this.emit("ct-label-click", { targetId: this.for, targetElement: targetElement, }); } } }; /** * Get the associated control element */ getControl(): HTMLElement | null { if (!this.for) return null; const root = this.getRootNode() as Document | ShadowRoot; return root.querySelector(`#${CSS.escape(this.for)}`) as HTMLElement; } /** * Focus the associated control element */ focusControl(): void { const control = this.getControl(); if (control && "focus" in control && typeof control.focus === "function") { control.focus(); } } } globalThis.customElements.define("ct-label", CTLabel);