import { css, html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { property } from "lit/decorators.js";
import { BaseElement } from "../../core/base-element.ts";
import { consume } from "@lit/context";
import {
applyThemeToElement,
type CFTheme,
cfThemeContext,
type ComponentSize,
defaultTheme,
} from "../theme-context.ts";
import { type CellHandle } from "@commonfabric/runtime-client";
import { stringSchema } from "@commonfabric/runner/schemas";
import { type InputTimingOptions } from "../../core/input-timing-controller.ts";
import { createStringCellController } from "../../core/cell-controller.ts";
import { createFormFieldController } from "../../core/form-field-controller.ts";
/**
* CFInput - Enhanced input field with support for various types, validation patterns, and reactive data binding
*
* @element cf-input
*
* @attr {string} type - Input type: "text" | "email" | "password" | "number" | "search" | "tel" | "url" | "date" | "time" | "datetime-local" | "month" | "week" | "color" | "file" | "range" | "hidden"
* @attr {string} placeholder - Placeholder text
* @attr {string|CellHandle} value - Input value (supports both plain string and CellHandle)
* @attr {boolean} disabled - Whether the input is disabled
* @attr {boolean} readonly - Whether the input is read-only
* @attr {boolean} required - Whether the input is required
* @attr {string} name - Name attribute for form submission
* @attr {string|number} min - Minimum value (for number, date, range inputs)
* @attr {string|number} max - Maximum value (for number, date, range inputs)
* @attr {string|number} step - Step value (for number, range inputs)
* @attr {string} pattern - Custom validation pattern (regex)
* @attr {string} validationPattern - Predefined pattern: "email" | "url" | "tel-us" | "tel-intl" | "credit-card" | "zip-us" | "alphanumeric" | "letters" | "numbers"
* @attr {string} autocomplete - Autocomplete hint
* @attr {string} inputmode - Virtual keyboard mode: "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url"
* @attr {string} size - Component size variant: "xs" | "sm" | "md" | "lg" | "xl" (default: "md")
* @attr {number} length - Width of input in characters
* @attr {boolean} multiple - Allow multiple files (file input only)
* @attr {string} accept - File types to accept (file input only)
* @attr {string} list - ID of datalist element for suggestions
* @attr {string} spellcheck - Enable/disable spellcheck
* @attr {boolean} showValidation - Show validation state visually
* @attr {boolean} error - Manual error state override
* @attr {string} timingStrategy - Input timing strategy: "immediate" | "debounce" | "throttle" | "blur"
* @attr {number} timingDelay - Delay in milliseconds for debounce/throttle (default: 300)
*
* @fires cf-change - Fired when value changes (timing depends on strategy) with detail: { value, oldValue, name, files? }
* @fires cf-focus - Fired on focus with detail: { value, name }
* @fires cf-blur - Fired on blur with detail: { value, name }
* @fires cf-keydown - Fired on keydown with detail: { key, value, shiftKey, ctrlKey, metaKey, altKey, name }
* @fires cf-submit - Fired on Enter key with detail: { value, name }
* @fires cf-invalid - Fired on validation failure with detail: { value, name, validationMessage, validity }
*
* @example
*
*
* @example
*
*
* @example
*
*
* @example
*
*
*
* @example
*
*
*/
export type InputType =
| "text"
| "password"
| "email"
| "number"
| "tel"
| "url"
| "search"
| "date"
| "time"
| "datetime-local"
| "month"
| "week"
| "color"
| "file"
| "range"
| "hidden";
export type InputMode =
| "none"
| "text"
| "decimal"
| "numeric"
| "tel"
| "search"
| "email"
| "url";
// Common validation patterns for different input types
// Note: Patterns must be compatible with both legacy and Unicode Sets (/v flag) regex modes
// In /v mode, hyphens in character classes must be at start/end or escaped
export const INPUT_PATTERNS = {
// Email pattern (basic validation) - hyphen at end of character class for /v compatibility
email: "[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}",
// URL pattern (http/https)
url: "https?://.+",
// US Phone pattern (various formats)
"tel-us": "\\+?1?[-.]?\\(?([0-9]{3})\\)?[-.]?([0-9]{3})[-.]?([0-9]{4})",
// International phone
"tel-intl":
"\\+?[0-9]{1,4}?[-.]?\\(?([0-9]{1,4})\\)?[-.]?([0-9]{1,4})[-.]?([0-9]{1,9})",
// Credit card (basic - digits with optional spaces/dashes)
"credit-card": "[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}[-\\s]?[0-9]{4}",
// ZIP code (US 5 or 9 digit)
"zip-us": "[0-9]{5}(-[0-9]{4})?",
// Alphanumeric only
alphanumeric: "[a-zA-Z0-9]+",
// Letters only
letters: "[a-zA-Z]+",
// Numbers only
numbers: "[0-9]+",
} as const;
export class CFInput extends BaseElement {
static formAssociated = true;
static override styles = css`
:host {
--cf-input-color-text: var(--cf-theme-color-text, #111827);
--cf-input-color-background: var(--cf-theme-color-background, #ffffff);
--cf-input-color-border: var(--cf-theme-color-border, #e5e7eb);
--cf-input-color-border-hover: var(--cf-theme-color-border-muted, #d1d5db);
--cf-input-color-primary: var(--cf-theme-color-primary, #3b82f6);
--cf-input-color-ring: rgba(59, 130, 246, 0.15);
--cf-input-color-surface: var(--cf-theme-color-surface, #f1f5f9);
--cf-input-color-text-muted: var(--cf-theme-color-text-muted, #6b7280);
--cf-input-color-error: var(--cf-theme-color-error, #dc2626);
--cf-input-color-error-ring: rgba(220, 38, 38, 0.15);
--cf-input-color-success: var(--cf-theme-color-success, #16a34a);
--cf-input-border-radius: var(
--cf-theme-border-radius,
var(--cf-border-radius-md, 0.375rem)
);
--cf-input-animation-duration: var(--cf-theme-animation-duration, 150ms);
--cf-input-font-family: var(--cf-theme-font-family, inherit);
/* Sizing scale defaults (size="md") */
--input-height: var(--cf-size-md-height, 32px);
--input-padding-x: var(--cf-size-md-padding-h, 8px);
--input-padding-y: var(--cf-size-md-padding-v, 8px);
--input-font-size: var(--cf-size-md-font-size, 12px);
--input-line-height: var(--cf-size-md-line-height, 16px);
--input-border-radius: var(--cf-size-md-radius, 8px);
display: block;
box-sizing: border-box;
}
:host([size="xs"]) {
--input-height: var(--cf-size-xs-height, 16px);
--input-padding-x: var(--cf-size-xs-padding-h, 4px);
--input-padding-y: var(--cf-size-xs-padding-v, 2px);
--input-font-size: var(--cf-size-xs-font-size, 9px);
--input-line-height: var(--cf-size-xs-line-height, 12px);
--input-border-radius: var(--cf-size-xs-radius, 4px);
}
:host([size="sm"]) {
--input-height: var(--cf-size-sm-height, 24px);
--input-padding-x: var(--cf-size-sm-padding-h, 6px);
--input-padding-y: var(--cf-size-sm-padding-v, 4px);
--input-font-size: var(--cf-size-sm-font-size, 11px);
--input-line-height: var(--cf-size-sm-line-height, 16px);
--input-border-radius: var(--cf-size-sm-radius, 5px);
}
:host([size="lg"]) {
--input-height: var(--cf-size-lg-height, 40px);
--input-padding-x: var(--cf-size-lg-padding-h, 12px);
--input-padding-y: var(--cf-size-lg-padding-v, 8px);
--input-font-size: var(--cf-size-lg-font-size, 16px);
--input-line-height: var(--cf-size-lg-line-height, 20px);
--input-border-radius: var(--cf-size-lg-radius, 9px);
}
:host([size="xl"]) {
--input-height: var(--cf-size-xl-height, 48px);
--input-padding-x: var(--cf-size-xl-padding-h, 16px);
--input-padding-y: var(--cf-size-xl-padding-v, 12px);
--input-font-size: var(--cf-size-xl-font-size, 18px);
--input-line-height: var(--cf-size-xl-line-height, 24px);
--input-border-radius: var(--cf-size-xl-radius, 10px);
}
*,
*::before,
*::after {
box-sizing: inherit;
}
input {
display: block;
width: 100%;
min-height: var(--input-height);
height: auto;
padding: var(--input-padding-y) var(--input-padding-x);
font-size: var(--input-font-size);
line-height: var(--input-line-height);
color: var(--cf-input-color-text, #111827);
background-color: var(--cf-input-color-background, #ffffff);
border: 1px solid var(--cf-input-color-border, #e5e7eb);
border-radius: var(--input-border-radius);
transition: all var(--cf-input-animation-duration, 150ms)
var(--cf-transition-timing-ease);
font-family: var(--cf-input-font-family, inherit);
}
input::placeholder {
color: var(--cf-input-color-text-muted, #6b7280);
}
input:hover:not(:disabled):not(:focus) {
border-color: var(--cf-input-color-border-hover, #d1d5db);
}
input:focus {
outline: none;
border-color: var(--cf-input-color-primary, #3b82f6);
box-shadow: 0 0 0 3px var(--cf-input-color-ring, rgba(59, 130, 246, 0.15));
}
input:disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: var(--cf-input-color-surface, #f1f5f9);
}
input[readonly] {
background-color: var(--cf-input-color-surface, #f1f5f9);
}
input.error {
border-color: var(--cf-input-color-error, #dc2626);
}
input.error:focus {
border-color: var(--cf-input-color-error, #dc2626);
box-shadow: 0 0 0 3px
var(--cf-input-color-error-ring, rgba(220, 38, 38, 0.15));
}
/* Remove spinner buttons from number inputs in Chrome/Safari/Edge */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Remove spinner from number inputs in Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
/* Style file input */
input[type="file"] {
padding: 0.375rem 0.75rem;
cursor: pointer;
}
input[type="file"]::file-selector-button {
margin-right: 0.5rem;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--cf-input-color-background, hsl(0, 0%, 100%));
background-color: var(--cf-input-color-primary, hsl(212, 100%, 47%));
border: none;
border-radius: var(--input-border-radius);
cursor: pointer;
}
input[type="file"]::file-selector-button:hover {
opacity: 0.9;
}
/* Date/time inputs */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"],
input[type="week"] {
cursor: pointer;
}
/* Color input */
input[type="color"] {
padding: 0.25rem;
cursor: pointer;
}
/* Search input */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/* Range input */
input[type="range"] {
padding: 0.5rem 0;
cursor: pointer;
}
input[type="range"]::-webkit-slider-track {
width: 100%;
height: 4px;
background: var(--cf-input-color-surface, #f1f5f9);
border-radius: 2px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: var(--cf-input-color-primary, #3b82f6);
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-track {
width: 100%;
height: 4px;
background: var(--cf-input-color-surface, #f1f5f9);
border-radius: 2px;
}
input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
background: var(--cf-input-color-primary, #3b82f6);
border-radius: 50%;
border: none;
cursor: pointer;
}
/* Hidden input */
input[type="hidden"] {
display: none;
}
/* Valid state (when showValidation is true) */
input:valid:not(:placeholder-shown) {
border-color: var(--cf-input-color-success, #16a34a);
}
input:valid:not(:placeholder-shown):focus {
border-color: var(--cf-input-color-success, #16a34a);
box-shadow: 0 0 0 3px
var(--cf-input-color-success, rgba(22, 163, 74, 0.15));
}
`;
static override properties = {
type: { type: String },
placeholder: { type: String },
value: { type: String },
disabled: { type: Boolean },
readonly: { type: Boolean },
error: { type: Boolean },
name: { type: String },
required: { type: Boolean },
autofocus: { type: Boolean },
autocomplete: { type: String },
min: { type: String },
max: { type: String },
step: { type: String },
pattern: { type: String },
maxlength: { type: String },
minlength: { type: String },
inputmode: { type: String },
size: { type: String, reflect: true },
length: { type: Number, attribute: "length" },
multiple: { type: Boolean },
accept: { type: String },
list: { type: String },
spellcheck: { type: Boolean },
validationPattern: { type: String },
showValidation: { type: Boolean },
timingStrategy: { type: String },
timingDelay: { type: Number },
};
declare type: InputType;
declare placeholder: string;
declare value: CellHandle | string;
declare disabled: boolean;
declare readonly: boolean;
declare error: boolean;
declare name: string;
declare required: boolean;
declare autofocus: boolean;
declare autocomplete: string;
declare min: string;
declare max: string;
declare step: string;
declare pattern: string;
declare maxlength: string;
declare minlength: string;
declare inputmode: InputMode;
declare size: ComponentSize;
declare length: number;
declare multiple: boolean;
declare accept: string;
declare list: string;
declare spellcheck: boolean;
declare validationPattern: keyof typeof INPUT_PATTERNS | "";
declare showValidation: boolean;
declare timingStrategy: InputTimingOptions["strategy"];
declare timingDelay: number;
private _input: HTMLInputElement | null = null;
private _generatedAriaLabel: string | null = null;
#internals: ElementInternals;
private _cellController = createStringCellController(this, {
timing: {
strategy: "debounce",
delay: 300,
},
onChange: (newValue: string, oldValue: string) => {
// cf-change is emitted via timing controller to honor timingStrategy
this.emit("cf-change", {
value: newValue,
oldValue,
name: this.name,
files: this.type === "file" ? this._input?.files : undefined,
});
},
});
// Form field controller handles buffering when in cf-form context
private _formField = createFormFieldController(this, {
cellController: this._cellController,
validate: () => ({
valid: this.checkValidity(),
message: this.validationMessage,
}),
});
constructor() {
super();
this.#internals = this.attachInternals();
this.type = "text";
this.placeholder = "";
this.value = "";
this.disabled = false;
this.readonly = false;
this.error = false;
this.name = "";
this.required = false;
this.autofocus = false;
this.autocomplete = "";
this.min = "";
this.max = "";
this.step = "";
this.pattern = "";
this.maxlength = "";
this.minlength = "";
this.inputmode = "text";
this.size = "md";
this.length = 0;
this.multiple = false;
this.accept = "";
this.list = "";
this.spellcheck = true;
this.validationPattern = "";
this.showValidation = false;
this.timingStrategy = "debounce";
this.timingDelay = 300;
this.addEventListener("focus", this._forwardFocusToInput);
}
// Theme consumption
@consume({ context: cfThemeContext, subscribe: true })
@property({ attribute: false })
// deno-lint-ignore no-explicit-any
accessor theme: CFTheme = defaultTheme;
private get input(): HTMLInputElement | null {
if (!this._input) {
this._input = this.shadowRoot?.querySelector("input") || null;
}
return this._input;
}
private getValue(): string {
return this._formField.getValue();
}
private setValue(newValue: string): void {
this._formField.setValue(newValue);
}
private getPattern(): string {
// Use custom pattern if provided
if (this.pattern) {
return this.pattern;
}
// Use validation pattern if specified
if (
this.validationPattern && this.validationPattern in INPUT_PATTERNS
) {
return INPUT_PATTERNS[this.validationPattern];
}
// Use default patterns for specific types
if (this.type === "email" && !this.pattern) {
return INPUT_PATTERNS.email;
}
if (this.type === "url" && !this.pattern) {
return INPUT_PATTERNS.url;
}
return "";
}
private getInputMode(): InputMode {
// Use explicit inputmode if provided
if (this.inputmode && this.inputmode !== "text") {
return this.inputmode;
}
// Return appropriate inputmode based on type
switch (this.type) {
case "email":
return "email";
case "tel":
return "tel";
case "url":
return "url";
case "number":
return "numeric";
case "search":
return "search";
default:
return "text";
}
}
private getValidationClass(): string {
if (!this.showValidation) {
return this.error ? "error" : "";
}
// Check native validation
const isValid = this.checkValidity();
return isValid ? "" : "error";
}
override disconnectedCallback() {
super.disconnectedCallback();
// Controllers handle cleanup automatically via ReactiveController
}
override willUpdate(changedProperties: Map) {
super.willUpdate(changedProperties);
// If the value property itself changed (e.g., switched to a different cell)
if (changedProperties.has("value")) {
// Bind the new cell first so getValue() returns the new value
this._cellController.bind(this.value, stringSchema);
// Then clear buffer - this captures the new cell's value as baseline for reset/dirty
this._formField.clearBuffer();
}
}
override updated(changedProperties: Map) {
super.updated(changedProperties);
// If value changed, ensure the DOM input is synchronized
if (changedProperties.has("value") && this.input) {
const currentValue = this.getValue();
if (this.input.value !== currentValue) {
this.input.value = currentValue;
}
}
// Update timing controller if timing options changed
if (
changedProperties.has("timingStrategy") ||
changedProperties.has("timingDelay")
) {
this._cellController.updateTimingOptions({
strategy: this.timingStrategy,
delay: this.timingDelay,
});
}
if (changedProperties.has("theme")) {
applyThemeToElement(this, this.theme ?? defaultTheme);
}
if (
changedProperties.has("disabled") ||
changedProperties.has("readonly") ||
changedProperties.has("required") ||
changedProperties.has("error") ||
changedProperties.has("showValidation") ||
changedProperties.has("placeholder") ||
changedProperties.has("type") ||
changedProperties.has("value")
) {
this._updateAccessibilityAttributes();
}
}
override firstUpdated() {
// Cache the input element reference
this._input = this.shadowRoot?.querySelector("input") || null;
// Bind the initial value to the cell controller
this._cellController.bind(this.value, stringSchema);
// Update timing options to match current properties
this._cellController.updateTimingOptions({
strategy: this.timingStrategy,
delay: this.timingDelay,
});
// Register with form after binding is complete
this._formField.register(this.name);
if (this.autofocus) {
this._input?.focus();
}
// Apply theme after first render
applyThemeToElement(this, this.theme ?? defaultTheme);
this._updateAccessibilityAttributes();
}
override connectedCallback() {
super.connectedCallback();
this._updateAccessibilityAttributes();
}
override render() {
const pattern = this.getPattern();
const inputMode = this.getInputMode();
const validationClass = this.getValidationClass();
// For file inputs, we can't set the value programmatically
const inputValue = this.type === "file" ? undefined : this.getValue();
// The host element carries the ARIA role and tabindex for accessibility.
// The inner input is removed from the sequential tab order; when the host
// receives focus, we forward focus here so typing and selection work.
// Avoid delegatesFocus: it can make the shadow control appear to be the
// active tab stop instead of the host that owns the ARIA surface.
return html`
`;
}
private _handleInput(event: Event) {
const input = event.target as HTMLInputElement;
const oldValue = this.getValue();
// For file inputs, we can't set the value programmatically
if (this.type !== "file") {
this.setValue(input.value);
} else {
// For file inputs, still emit the event with files
this.setValue("");
}
// Emit cf-input event directly for non-cell interop
this.emit("cf-input", {
value: this.type === "file" ? "" : input.value,
oldValue,
name: this.name,
files: this.type === "file" ? input.files : undefined,
});
}
private _handleChange(event: Event) {
const input = event.target as HTMLInputElement;
// Update value through form field controller
// cf-change is emitted by the cell controller's onChange callback
// which honors the configured timingStrategy (debounce/throttle/blur)
if (this.type !== "file") {
this.setValue(input.value);
} else {
this.setValue("");
}
}
private _handleFocus(_event: Event) {
this._cellController.onFocus();
this.emit("cf-focus", {
value: this.getValue(),
name: this.name,
});
}
private _handleBlur(_event: Event) {
this._cellController.onBlur();
this.emit("cf-blur", {
value: this.getValue(),
name: this.name,
});
}
private _handleKeyDown(event: KeyboardEvent) {
this.emit("cf-keydown", {
key: event.key,
value: this.getValue(),
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
altKey: event.altKey,
name: this.name,
});
// Special handling for Enter key
if (event.key === "Enter") {
this.emit("cf-submit", {
value: this.getValue(),
name: this.name,
});
}
}
private _forwardFocusToInput = () => {
if (this.disabled) return;
this.input?.focus();
};
private _handleInvalid(event: Event) {
event.preventDefault(); // Prevent browser's default validation UI
const input = event.target as HTMLInputElement;
this.emit("cf-invalid", {
value: this.getValue(),
name: this.name,
validationMessage: input.validationMessage,
validity: input.validity,
});
// Update visual state if showValidation is enabled
if (this.showValidation) {
this.requestUpdate();
}
this._updateAccessibilityAttributes();
}
private _updateAccessibilityAttributes() {
this._syncHostRole();
if (!this.hasAttribute("exportparts")) {
this.setAttribute("exportparts", "input");
}
this.tabIndex = this.disabled ? -1 : 0;
this.setAttribute("aria-disabled", String(this.disabled));
this.setAttribute("aria-readonly", String(this.readonly));
this.setAttribute("aria-required", String(this.required));
this._updateGeneratedAriaLabel();
// Read .validity.valid directly instead of checkValidity() to avoid
// firing the 'invalid' event, which would re-enter _handleInvalid.
const nativeValid = this.input?.validity?.valid ?? true;
this.setAttribute(
"aria-invalid",
String(this.error || !nativeValid),
);
this._syncValueAttributes();
this._syncInternals();
}
/** Sync aria-valuemin/max/now for spinbutton and slider roles. */
private _syncValueAttributes() {
const role = this.getAttribute("role");
if (role === "spinbutton" || role === "slider") {
const min = this.min || (this.input?.min ?? "");
const max = this.max || (this.input?.max ?? "");
const val = this.input?.value ?? this.getValue();
if (min) this.setAttribute("aria-valuemin", min);
else this.removeAttribute("aria-valuemin");
if (max) this.setAttribute("aria-valuemax", max);
else this.removeAttribute("aria-valuemax");
if (val) this.setAttribute("aria-valuenow", val);
else this.removeAttribute("aria-valuenow");
} else {
this.removeAttribute("aria-valuemin");
this.removeAttribute("aria-valuemax");
this.removeAttribute("aria-valuenow");
}
}
/** Sync value and validity to ElementInternals for native form participation. */
private _syncInternals() {
this.#internals.setFormValue(this.getValue());
if (this.input) {
this.#internals.setValidity(
this.input.validity,
this.input.validationMessage,
this.input,
);
}
}
/** Map input type to the appropriate ARIA role on the host. */
private _syncHostRole() {
// Respect author-provided roles
if (
this.hasAttribute("role") &&
this.getAttribute("role") !== this._lastGeneratedRole
) {
return;
}
const role = this._roleForType(this.type);
if (role) {
this.setAttribute("role", role);
this._lastGeneratedRole = role;
} else if (this._lastGeneratedRole) {
this.removeAttribute("role");
this._lastGeneratedRole = null;
}
}
private _lastGeneratedRole: string | null = null;
private _roleForType(type: InputType): string | null {
switch (type) {
case "text":
case "email":
case "password":
case "search":
case "tel":
case "url":
return "textbox";
case "number":
return "spinbutton";
case "range":
return "slider";
default:
// date, time, datetime-local, month, week, color, file, hidden
// — no widely-supported ARIA role; leave unset
return null;
}
}
private _updateGeneratedAriaLabel() {
const ariaLabel = this.getAttribute("aria-label");
const hasAuthorProvidedName = this.hasAttribute("aria-labelledby") ||
(ariaLabel !== null && ariaLabel !== this._generatedAriaLabel);
if (hasAuthorProvidedName) {
this._generatedAriaLabel = null;
return;
}
if (this.placeholder) {
this.setAttribute("aria-label", this.placeholder);
this._generatedAriaLabel = this.placeholder;
return;
}
if (
this._generatedAriaLabel !== null &&
ariaLabel === this._generatedAriaLabel
) {
this.removeAttribute("aria-label");
this._generatedAriaLabel = null;
}
}
override focus(options?: FocusOptions): void {
if (this.disabled) return;
const input = this.input;
if (input) {
input.focus(options);
return;
}
// If focus is requested before the first render, keep focus on the
// semantic host now, then forward it once the native input exists.
super.focus(options);
void this.updateComplete.then(() => {
if (this.disabled || this.ownerDocument.activeElement !== this) {
return;
}
this.input?.focus(options);
});
}
/**
* Blur the input programmatically
*/
override blur(): void {
this.input?.blur();
}
/**
* Select all text in the input
*/
select(): void {
this.input?.select();
}
/**
* Set selection range in the input
*/
setSelectionRange(
start: number,
end: number,
direction?: "forward" | "backward" | "none",
): void {
this.input?.setSelectionRange(start, end, direction);
}
/**
* Check validity of the input
*/
checkValidity(): boolean {
return this.input?.checkValidity() ?? true;
}
/**
* Report validity of the input
*/
reportValidity(): boolean {
return this.input?.reportValidity() ?? true;
}
/**
* Get the validity state
*/
get validity(): ValidityState | undefined {
return this.input?.validity;
}
/**
* Get validation message
*/
get validationMessage(): string {
return this.input?.validationMessage || "";
}
/**
* Set custom validity message
*/
setCustomValidity(message: string): void {
this.input?.setCustomValidity(message);
}
}