From a0d331ed7cc201880c5668f15c05738258b56a1d Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 11 May 2026 22:24:23 +0530 Subject: [PATCH 01/39] feat(core/date-input): add clear method to reset value and validation state Co-authored-by: Copilot --- .../fix-input-validation-clear-novalidate.md | 10 + packages/angular/src/components.ts | 2 +- packages/angular/standalone/src/components.ts | 2 +- packages/core/src/components.d.ts | 5 + .../src/components/date-input/date-input.tsx | 64 ++++ .../date-input/tests/date-input.ct.ts | 295 ++++++++++++++++++ .../core/src/components/input/input.util.ts | 68 +++- 7 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-input-validation-clear-novalidate.md diff --git a/.changeset/fix-input-validation-clear-novalidate.md b/.changeset/fix-input-validation-clear-novalidate.md new file mode 100644 index 00000000000..4bb5729a434 --- /dev/null +++ b/.changeset/fix-input-validation-clear-novalidate.md @@ -0,0 +1,10 @@ +--- +'@siemens/ix': minor +--- + +Added `clear()` method to `ix-date-input` to reset the value and validation state to pristine. + +Fixed validation behavior: +- Required fields set to empty programmatically now correctly show as invalid +- `novalidate` forms no longer show validation errors on blur or invalid input +- Calling `clear()` removes all validation errors even after the field has been touched diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 0d20f5473bc..c1385a6f8e5 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -631,7 +631,7 @@ The event payload contains information about the selected date range. @ProxyCmp({ inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['openPicker', 'getNativeInputElement', 'focusInput'] + methods: ['openPicker', 'clear', 'getNativeInputElement', 'focusInput'] }) @Component({ selector: 'ix-date-input', diff --git a/packages/angular/standalone/src/components.ts b/packages/angular/standalone/src/components.ts index 2e5d091bb25..28cf8545f14 100644 --- a/packages/angular/standalone/src/components.ts +++ b/packages/angular/standalone/src/components.ts @@ -734,7 +734,7 @@ The event payload contains information about the selected date range. @ProxyCmp({ defineCustomElementFn: defineIxDateInput, inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['openPicker', 'getNativeInputElement', 'focusInput'] + methods: ['openPicker', 'clear', 'getNativeInputElement', 'focusInput'] }) @Component({ selector: 'ix-date-input', diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 0242d7390a5..0234a73674c 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -987,6 +987,11 @@ export namespace Components { * @default 'Previous month' */ "ariaLabelPreviousMonthButton"?: string; + /** + * Clears the input value and resets the touched state. Unlike clearing the value directly, this method restores the initial, non-invalid state and removes visible validation errors. + * @since 5.1.0 + */ + "clear": () => Promise; /** * Disabled attribute. * @default false diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 393d6522e4d..e9769438ef7 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -25,6 +25,7 @@ import { import { DateTime } from 'luxon'; import { SlotEnd, SlotStart } from '../input/input.fc'; import { + clearInputValue, DisposableChangesAndVisibilityObservers, PickerValidityStateTracker, addDisposableChangesAndVisibilityObservers, @@ -41,6 +42,7 @@ import { ValidationResults, createClassMutationObserver, getValidationText, + shouldSuppressInternalValidation, } from '../utils/input'; import { closeDropdown as closeDropdownUtil, @@ -94,6 +96,9 @@ export class DateInput @Prop({ reflect: true, mutable: true }) value?: string = ''; @Watch('value') watchValuePropHandler(newValue: string) { + if (!newValue && this.required && !this.isClearing) { + this.touched = true; + } this.onInput(newValue); } @@ -129,6 +134,11 @@ export class DateInput */ @Prop() required?: boolean; + @Watch('required') + onRequiredChange() { + this.syncValidationClasses(); + } + /** * Helper text below the input field. */ @@ -276,6 +286,8 @@ export class DateInput public touched = false; + private isClearing = false; + public validityTracker: PickerValidityStateTracker = createPickerValidityStateTracker(); @@ -332,6 +344,44 @@ export class DateInput this.from = this.value; } + /** + * Clears the input value and resets the touched state. + * + * Unlike clearing the value directly, this method restores the initial, + * non-invalid state and removes visible validation errors. + * + * @since 5.1.0 + */ + @Method() + async clear(): Promise { + return clearInputValue(this, { + setClearing: (isClearing) => { + this.isClearing = isClearing; + }, + syncValidationClasses: () => this.syncValidationClasses(), + additionalCleanup: () => { + this.from = undefined; + }, + }); + } + + private async syncValidationClasses(): Promise { + const skipValidation = await shouldSuppressInternalValidation(this); + if (skipValidation) { + return; + } + + const hasValue = !!this.value; + if (this.required) { + this.hostElement.classList.toggle( + 'ix-invalid--required', + !hasValue && this.touched + ); + } else { + this.hostElement.classList.remove('ix-invalid--required'); + } + } + /** @internal */ @Method() hasValidValue(): Promise { @@ -359,6 +409,20 @@ export class DateInput return; } + const suppressValidation = await shouldSuppressInternalValidation(this); + if (suppressValidation) { + this.isInputInvalid = false; + this.invalidReason = undefined; + this.updateFormInternalValue(value); + this.closeDropdown(); + if (hasKeyboardMode()) { + this.inputElementRef.current?.focus(); + } + this.emitValidityStateChangeIfChanged(); + this.valueChange.emit(value); + return; + } + const date = DateTime.fromFormat(value, this.format); const minDate = DateTime.fromFormat(this.minDate, this.format); const maxDate = DateTime.fromFormat(this.maxDate, this.format); diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 078ff03482e..5104ad4de34 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -289,3 +289,298 @@ regressionTest.describe('keyboard navigation', () => { await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); }); + +regressionTest.describe('date-input validation scenarios', () => { + regressionTest( + 'Required input: Invalid input > Removing value with keyboard > Stays invalid', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Required input: Invalid input > Remove touched state (clear) > Valid again', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Required input: Invalid input > Programmatically setting to empty > Stays invalid', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Required input: Valid input > Removing value with keyboard > It is invalid', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Required input: Valid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Required input: Valid input > Programmatically setting to empty > It is invalid', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Not required input: Invalid input > Removing value with keyboard > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Not required input: Invalid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Not required input: Invalid input > Programmatically setting to empty > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Not required input: Valid input > Removing value with keyboard > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Not required input: Valid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'Not required input: Valid input > Programmatically setting to empty > Valid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.blur(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'novalidate form suppresses validation for required field', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Validation works after switching between required and non-required', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.required = true; + }); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.required = false; + }); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + } + ); +}); diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 82e81cdfd99..337132e84f8 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -55,6 +55,11 @@ export async function checkInternalValidity( comp: IxFormComponent, input: HTMLInputElement | HTMLTextAreaElement ) { + const skipValidation = await shouldSuppressInternalValidation(comp); + if (skipValidation) { + return; + } + const validityState = input.validity; const currentValidityState = !comp.hostElement.classList.contains( 'ix-invalid--validity-invalid' @@ -77,11 +82,6 @@ export async function checkInternalValidity( return; } - const skipValidation = await shouldSuppressInternalValidation(comp); - if (skipValidation) { - return; - } - const { valid } = validityState; comp.hostElement.classList.toggle('ix-invalid--validity-invalid', !valid); } @@ -323,3 +323,61 @@ export async function emitPickerValidityStateChangeIfChanged( invalidReason: context.invalidReason, }); } + +export interface ClearableInputComponent { + value?: T; + hostElement: HTMLElement; + touched?: boolean; + isInputInvalid?: boolean; + invalidReason?: string; + updateFormInternalValue?: (value: T) => void; + valueChange?: { emit: (value: T) => void }; +} + +export async function clearInputValue( + comp: ClearableInputComponent, + options?: { + defaultValue?: T; + additionalCleanup?: () => void; + emitValueChange?: boolean; + setClearing?: (isClearing: boolean) => void; + syncValidationClasses?: () => void | Promise; + } +): Promise { + const emptyValue = options?.defaultValue ?? ('' as T); + + options?.setClearing?.(true); + + if ('touched' in comp) { + comp.touched = false; + } + + if ('isInputInvalid' in comp) { + comp.isInputInvalid = false; + } + + if ('invalidReason' in comp) { + comp.invalidReason = undefined; + } + + comp.hostElement.classList.remove( + 'ix-invalid--required', + 'ix-invalid--validity-invalid', + 'ix-invalid--validity-patternMismatch' + ); + + options?.additionalCleanup?.(); + + comp.updateFormInternalValue?.(emptyValue); + comp.value = emptyValue; + + if (options?.emitValueChange) { + comp.valueChange?.emit(emptyValue); + } + + if (typeof options?.syncValidationClasses === 'function') { + await options.syncValidationClasses(); + } + + options?.setClearing?.(false); +} From 0c3d0ef86aef954e09b1fef73bace76c877e26fe Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 12 May 2026 05:26:55 +0530 Subject: [PATCH 02/39] fix(core/date-input): handle empty input as valid and suppress visual validation Co-authored-by: Copilot --- .../src/components/date-input/date-input.tsx | 104 +++++++++++------- .../date-input/tests/date-input.ct.ts | 35 +++++- .../utils/input/picker-input.util.ts | 23 ++++ 3 files changed, 123 insertions(+), 39 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index e9769438ef7..0674f2ee250 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -47,8 +47,10 @@ import { import { closeDropdown as closeDropdownUtil, createValidityState, + focusInputIfKeyboardMode, handleIconClick, openDropdown as openDropdownUtil, + resetPickerValueIfInvalid, } from '../utils/input/picker-input.util'; import { MakeRef, makeRef } from '../utils/make-ref'; import type { DateInputValidityState } from './date-input.types'; @@ -57,7 +59,6 @@ import { InputPickerMixin, InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; -import { hasKeyboardMode } from '../utils/internal/mixins/setup.mixin'; /** * @form-ready @@ -98,6 +99,7 @@ export class DateInput @Watch('value') watchValuePropHandler(newValue: string) { if (!newValue && this.required && !this.isClearing) { this.touched = true; + this.syncValidationClasses(); } this.onInput(newValue); } @@ -135,8 +137,8 @@ export class DateInput @Prop() required?: boolean; @Watch('required') - onRequiredChange() { - this.syncValidationClasses(); + async onRequiredChange() { + await this.syncValidationClasses(); } /** @@ -394,57 +396,85 @@ export class DateInput return Promise.resolve(this.formInternals.form); } - async onInput(value: string | undefined) { - this.value = value; - if (!value) { - this.isInputInvalid = false; - this.invalidReason = undefined; - this.emitValidityStateChangeIfChanged(); - this.updateFormInternalValue(value); - this.valueChange.emit(value); - return; - } + private getDateValidation(value: string): { + isValid: boolean; + invalidReason?: string; + } { + const date = DateTime.fromFormat(value, this.format); + const minDate = DateTime.fromFormat(this.minDate, this.format); + const maxDate = DateTime.fromFormat(this.maxDate, this.format); - if (!this.format) { - return; - } + return { + isValid: date.isValid && !(date < minDate) && !(date > maxDate), + invalidReason: date.invalidReason ?? undefined, + }; + } - const suppressValidation = await shouldSuppressInternalValidation(this); - if (suppressValidation) { - this.isInputInvalid = false; - this.invalidReason = undefined; - this.updateFormInternalValue(value); - this.closeDropdown(); - if (hasKeyboardMode()) { - this.inputElementRef.current?.focus(); + private handleEmptyInput(value: string | undefined): void { + this.isInputInvalid = false; + this.invalidReason = undefined; + this.emitValidityStateChangeIfChanged(); + this.updateFormInternalValue(value); + this.valueChange.emit(value); + } + + private handleSuppressedValidationInput(value: string): void { + this.isInputInvalid = false; + this.invalidReason = undefined; + this.updateFormInternalValue(value); + + resetPickerValueIfInvalid( + value, + (currentValue) => this.getDateValidation(currentValue).isValid, + () => { + this.from = undefined; } - this.emitValidityStateChangeIfChanged(); - this.valueChange.emit(value); - return; - } + ); - const date = DateTime.fromFormat(value, this.format); - const minDate = DateTime.fromFormat(this.minDate, this.format); - const maxDate = DateTime.fromFormat(this.maxDate, this.format); + this.closeDropdown(); + focusInputIfKeyboardMode(this.inputElementRef.current); + this.emitValidityStateChangeIfChanged(); + this.valueChange.emit(value); + } + + private handleValidatedInput(value: string): void { + const validation = this.getDateValidation(value); - this.isInputInvalid = !date.isValid || date < minDate || date > maxDate; + this.isInputInvalid = !validation.isValid; if (this.isInputInvalid) { - this.invalidReason = date.invalidReason ?? undefined; + this.invalidReason = validation.invalidReason; this.from = undefined; } else { this.updateFormInternalValue(value); this.closeDropdown(); - - if (hasKeyboardMode()) { - this.inputElementRef.current?.focus(); - } + focusInputIfKeyboardMode(this.inputElementRef.current); } this.emitValidityStateChangeIfChanged(); this.valueChange.emit(value); } + async onInput(value: string | undefined) { + this.value = value; + if (!value) { + this.handleEmptyInput(value); + return; + } + + if (!this.format) { + return; + } + + const suppressValidation = await shouldSuppressInternalValidation(this); + if (suppressValidation) { + this.handleSuppressedValidationInput(value); + return; + } + + this.handleValidatedInput(value); + } + onCalenderClick(event: Event) { handleIconClick( event, diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 5104ad4de34..7f768980045 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -36,6 +36,14 @@ const createDateInputAccessor = async (dateInput: Locator) => { return handle; }; +const expectNoVisualValidation = async ( + dateInput: Locator, + input: Locator +) => { + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); +}; + regressionTest('renders', async ({ mount, page }) => { await mount(``); const dateInputElement = page.locator('ix-date-input'); @@ -550,8 +558,31 @@ regressionTest.describe('date-input validation scenarios', () => { await input.press('Delete'); await input.blur(); - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + await expectNoVisualValidation(dateInput, input); + } + ); + + regressionTest( + 'novalidate form suppresses visual validation for invalid date input and deselects previously selected date in picker', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = page.locator('input'); + const dateInputAccessor = await createDateInputAccessor(dateInput); + + await input.fill('2025/10/10/10'); + await input.blur(); + + await expectNoVisualValidation(dateInput, input); + + await dateInputAccessor.openByCalender(); + + await expect(dateInput.locator('.calendar-item.selected')).toHaveCount(0); } ); diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 862f82c84b5..b7980ef1b6e 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -8,6 +8,29 @@ */ import { dropdownController } from '../../dropdown/dropdown-controller'; +import { hasKeyboardMode } from '../internal/mixins/setup.mixin'; + +export function focusInputIfKeyboardMode( + inputElement: HTMLInputElement | null | undefined +): void { + if (hasKeyboardMode()) { + inputElement?.focus(); + } +} + +export function resetPickerValueIfInvalid( + value: string, + isValid: (value: string) => boolean, + resetPickerValue: () => void +): boolean { + const valid = isValid(value); + + if (!valid) { + resetPickerValue(); + } + + return valid; +} export async function openDropdown(dropdownElementRef: any) { const dropdownElement = await dropdownElementRef.waitForCurrent(); From 7b267e67c91b961a5af851c97f7a11af3316c32d Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 12 May 2026 05:44:37 +0530 Subject: [PATCH 03/39] fix(date-input): remove openPicker method from ProxyCmp configuration --- packages/angular/src/components.ts | 2 +- packages/angular/standalone/src/components.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index 92793268c4d..1778acd992d 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -631,7 +631,7 @@ The event payload contains information about the selected date range. @ProxyCmp({ inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['openPicker', 'clear', 'getNativeInputElement', 'focusInput'] + methods: ['clear', 'getNativeInputElement', 'focusInput'] }) @Component({ selector: 'ix-date-input', diff --git a/packages/angular/standalone/src/components.ts b/packages/angular/standalone/src/components.ts index 6d9d3bbb6a0..76dca543a8b 100644 --- a/packages/angular/standalone/src/components.ts +++ b/packages/angular/standalone/src/components.ts @@ -734,7 +734,7 @@ The event payload contains information about the selected date range. @ProxyCmp({ defineCustomElementFn: defineIxDateInput, inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['openPicker', 'clear', 'getNativeInputElement', 'focusInput'] + methods: ['clear', 'getNativeInputElement', 'focusInput'] }) @Component({ selector: 'ix-date-input', From ec038c25815d7aa60e5a9e3c96f6708a2171d8e4 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 12 May 2026 17:46:03 +0530 Subject: [PATCH 04/39] fix(date-input): simplify validation tests and update test descriptions Co-authored-by: Copilot --- .../src/components/date-input/tests/date-input.ct.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 7f768980045..60947aff238 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -36,10 +36,7 @@ const createDateInputAccessor = async (dateInput: Locator) => { return handle; }; -const expectNoVisualValidation = async ( - dateInput: Locator, - input: Locator -) => { +const expectNoVisualValidation = async (dateInput: Locator, input: Locator) => { await expect(input).not.toHaveClass(/is-invalid/); await expect(dateInput).not.toHaveClass(/ix-invalid--required/); }; @@ -563,7 +560,7 @@ regressionTest.describe('date-input validation scenarios', () => { ); regressionTest( - 'novalidate form suppresses visual validation for invalid date input and deselects previously selected date in picker', + 'novalidate form suppresses visual validation for invalid date input', async ({ page, mount }) => { await mount(`
@@ -573,16 +570,11 @@ regressionTest.describe('date-input validation scenarios', () => { const dateInput = page.locator('ix-date-input'); const input = page.locator('input'); - const dateInputAccessor = await createDateInputAccessor(dateInput); await input.fill('2025/10/10/10'); await input.blur(); await expectNoVisualValidation(dateInput, input); - - await dateInputAccessor.openByCalender(); - - await expect(dateInput.locator('.calendar-item.selected')).toHaveCount(0); } ); From c33e0128d0cdbf86d1799f65129fd330f7a154b3 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 12 May 2026 20:07:50 +0530 Subject: [PATCH 05/39] fix(date-input): improve date validation logic to handle empty min/max dates Co-authored-by: Copilot --- packages/core/src/components/date-input/date-input.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 0674f2ee250..4eeecb47d27 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -405,7 +405,10 @@ export class DateInput const maxDate = DateTime.fromFormat(this.maxDate, this.format); return { - isValid: date.isValid && !(date < minDate) && !(date > maxDate), + isValid: + date.isValid && + (!minDate.isValid || date >= minDate) && + (!maxDate.isValid || date <= maxDate), invalidReason: date.invalidReason ?? undefined, }; } From 6d5f7d64d414f0af4cc571d099c97c091759e7c7 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 3 Jun 2026 18:35:10 +0530 Subject: [PATCH 06/39] feat(date-input): enhance validation handling and error messaging - Introduced a new `reportValidity` method to explicitly trigger validation and surface errors immediately, regardless of user interaction. - Updated validation logic to ensure required fields show appropriate error messages when empty. - Improved internal state management for invalid inputs, ensuring visual feedback aligns with user interactions. - Refactored validation classes and error messaging to provide clearer user feedback, including handling of required fields and parse errors. - Added tests to cover new validation scenarios, including behavior in `novalidate` forms and programmatic value changes. --- .../fix-input-validation-clear-novalidate.md | 17 +- packages/angular/src/components.ts | 6 +- packages/angular/standalone/src/components.ts | 6 +- packages/core/src/components.d.ts | 19 + .../src/components/date-input/date-input.tsx | 261 ++++++--- .../date-input/tests/date-input.ct.ts | 499 ++++++++++++++++-- .../core/src/components/input/input.util.ts | 30 +- .../components/input/tests/form-ready.ct.ts | 2 +- .../utils/input/picker-input.util.ts | 59 +++ .../src/components/utils/input/validation.ts | 31 +- packages/react/src/components.server.ts | 1 + packages/vue/src/components.ts | 1 + 12 files changed, 810 insertions(+), 122 deletions(-) diff --git a/.changeset/fix-input-validation-clear-novalidate.md b/.changeset/fix-input-validation-clear-novalidate.md index 4bb5729a434..7034224810a 100644 --- a/.changeset/fix-input-validation-clear-novalidate.md +++ b/.changeset/fix-input-validation-clear-novalidate.md @@ -2,9 +2,16 @@ '@siemens/ix': minor --- -Added `clear()` method to `ix-date-input` to reset the value and validation state to pristine. +Added `clear()` method to `ix-date-input` to reset the value and all validation state to its initial pristine condition. Unlike setting the `value` property directly, `clear()` removes visible error indicators even after the field has been touched. -Fixed validation behavior: -- Required fields set to empty programmatically now correctly show as invalid -- `novalidate` forms no longer show validation errors on blur or invalid input -- Calling `clear()` removes all validation errors even after the field has been touched +Added `reportValidity()` method to `ix-date-input` to programmatically trigger validation and show visual error state immediately — equivalent to calling `reportValidity()` on a native `` element. + +Added `i18nErrorRequired` prop (`i18n-error-required`, default `"This field is required"`) to `ix-date-input`. When a required field is emptied after `reportValidity()` has surfaced an error, the error text now switches from "Date is not valid" to the required-missing message instead of disappearing — keeping both the red border and the text description visible. + +Fixed validation behavior for `ix-date-input`: + +- Emptying a required field (via keyboard or programmatically) now correctly shows `ix-invalid--required` and a required-missing error message after the field has been touched +- After `reportValidity()` surfaces an error, setting a valid value programmatically now correctly clears all error state (red border and message) — previously the field stayed red despite holding a valid date +- Clicking a calendar day while the calendar is open no longer causes a momentary red-border flash on the input +- `novalidate` forms now fully suppress visual validation feedback (red border and error messages); date parsing continues internally to keep the calendar picker state consistent +- Dynamically toggling the `required` attribute now immediately reflects the correct validation state diff --git a/packages/angular/src/components.ts b/packages/angular/src/components.ts index f360ff5648b..5282b475e15 100644 --- a/packages/angular/src/components.ts +++ b/packages/angular/src/components.ts @@ -630,15 +630,15 @@ The event payload contains information about the selected date range. @ProxyCmp({ - inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['clear', 'getNativeInputElement', 'focusInput'] + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'i18nErrorRequired', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], + methods: ['clear', 'getNativeInputElement', 'focusInput', 'reportValidity'] }) @Component({ selector: 'ix-date-input', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'i18nErrorRequired', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], outputs: ['valueChange', 'validityStateChange', 'ixChange'], standalone: false }) diff --git a/packages/angular/standalone/src/components.ts b/packages/angular/standalone/src/components.ts index b9e4dba3e90..8a3644c60a7 100644 --- a/packages/angular/standalone/src/components.ts +++ b/packages/angular/standalone/src/components.ts @@ -733,15 +733,15 @@ The event payload contains information about the selected date range. @ProxyCmp({ defineCustomElementFn: defineIxDateInput, - inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], - methods: ['clear', 'getNativeInputElement', 'focusInput'] + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'i18nErrorRequired', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], + methods: ['clear', 'getNativeInputElement', 'focusInput', 'reportValidity'] }) @Component({ selector: 'ix-date-input', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], + inputs: ['ariaLabelCalendarButton', 'ariaLabelNextMonthButton', 'ariaLabelPreviousMonthButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'i18nErrorDateUnparsable', 'i18nErrorRequired', 'infoText', 'invalidText', 'label', 'locale', 'maxDate', 'minDate', 'name', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'showWeekNumbers', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText', 'weekStartIndex'], outputs: ['valueChange', 'validityStateChange', 'ixChange'], }) export class IxDateInput { diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index df199879bb5..64eaff1a37c 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1030,6 +1030,12 @@ export namespace Components { * @default 'Date is not valid' */ "i18nErrorDateUnparsable": string; + /** + * I18n string for the error message when a required field is empty. + * @since 5.1.0 + * @default 'This field is required' + */ + "i18nErrorRequired": string; /** * Info text below the input field. */ @@ -1074,6 +1080,12 @@ export namespace Components { * @default false */ "readonly": boolean; + /** + * Triggers validation and shows visual error state immediately, regardless of whether the user has interacted with the field. Use this when submitting via AJAX (no HTML form) or when you need to programmatically surface validation errors — equivalent to calling `reportValidity()` on a native `` element. Unlike form submit, this explicit validation call is not suppressed by a surrounding `` and will still surface errors. + * @returns `true` if the field is valid, `false` otherwise. + * @since 5.1.0 + */ + "reportValidity": () => Promise; /** * Required attribute. */ @@ -7530,6 +7542,12 @@ declare namespace LocalJSX { * @default 'Date is not valid' */ "i18nErrorDateUnparsable"?: string; + /** + * I18n string for the error message when a required field is empty. + * @since 5.1.0 + * @default 'This field is required' + */ + "i18nErrorRequired"?: string; /** * Info text below the input field. */ @@ -11631,6 +11649,7 @@ declare namespace LocalJSX { "validText": string; "showTextAsTooltip": boolean; "i18nErrorDateUnparsable": string; + "i18nErrorRequired": string; "showWeekNumbers": boolean; "weekStartIndex": number; "ariaLabelPreviousMonthButton": string; diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 4eeecb47d27..ce5dc0c498a 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -15,6 +15,7 @@ import { Event, EventEmitter, Host, + Listen, Method, Mixin, Prop, @@ -34,6 +35,7 @@ import { emitPickerValidityState, handleSubmitOnEnterKeydown, onInputBlurWithChange, + syncRequiredValidationClass, } from '../input/input.util'; import { ClassMutationObserver, @@ -41,7 +43,7 @@ import { IxInputFieldComponent, ValidationResults, createClassMutationObserver, - getValidationText, + reportFieldValidity, shouldSuppressInternalValidation, } from '../utils/input'; import { @@ -49,8 +51,11 @@ import { createValidityState, focusInputIfKeyboardMode, handleIconClick, + handlePickerHostFocusout, + handlePickerInputBlur, openDropdown as openDropdownUtil, resetPickerValueIfInvalid, + syncCustomInputValidity, } from '../utils/input/picker-input.util'; import { MakeRef, makeRef } from '../utils/make-ref'; import type { DateInputValidityState } from './date-input.types'; @@ -97,10 +102,6 @@ export class DateInput @Prop({ reflect: true, mutable: true }) value?: string = ''; @Watch('value') watchValuePropHandler(newValue: string) { - if (!newValue && this.required && !this.isClearing) { - this.touched = true; - this.syncValidationClasses(); - } this.onInput(newValue); } @@ -138,7 +139,7 @@ export class DateInput @Watch('required') async onRequiredChange() { - await this.syncValidationClasses(); + await syncRequiredValidationClass(this.hostElement, this); } /** @@ -200,6 +201,14 @@ export class DateInput @Prop({ attribute: 'i18n-error-date-unparsable' }) i18nErrorDateUnparsable = 'Date is not valid'; + /** + * I18n string for the error message when a required field is empty. + * + * @since 5.1.0 + */ + @Prop({ attribute: 'i18n-error-required' }) i18nErrorRequired = + 'This field is required'; + /** * Shows week numbers displayed on the left side of the date picker. * @@ -282,19 +291,43 @@ export class DateInput private classObserver?: ClassMutationObserver; + /** + * Tracks actual parse/format invalidity regardless of touched state. + * Used for internal validity queries (getValidityState, reportValidity). + * Visual feedback is gated on `touched`. + */ + private _hasInvalidInput = false; + + /** + * Set to `true` by `onBlur` when a genuine (non-suppressed) external blur + * runs validation. Read and reset by `onFocusout` to avoid re-running the + * same validation path a second time (onBlur → onFocusout both fire for a + * direct tab-away). + */ + private _blurHandledValidation = false; + public initialValue?: string; public invalidReason?: string; public touched = false; - private isClearing = false; - public validityTracker: PickerValidityStateTracker = createPickerValidityStateTracker(); private disposableChangesAndVisibilityObservers?: DisposableChangesAndVisibilityObservers; + private syncFormInternalsValidity(): void { + syncCustomInputValidity( + this.formInternals, + this._hasInvalidInput, + this.required, + this.value, + this.invalidReason ?? this.i18nErrorDateUnparsable, + this.i18nErrorRequired + ); + } + updateFormInternalValue(value: string | undefined): void { if (value) { this.formInternals.setFormValue(value); @@ -316,9 +349,9 @@ export class DateInput ); } - override componentWillLoad(): void { - this.onInput(this.value); - if (this.isInputInvalid) { + override async componentWillLoad(): Promise { + await this.onInput(this.value); + if (this._hasInvalidInput) { this.from = null; } else { this.watchValue(); @@ -326,6 +359,7 @@ export class DateInput this.checkClassList(); this.updateFormInternalValue(this.value); + this.syncFormInternalsValidity(); } private updatePaddings() { @@ -346,6 +380,16 @@ export class DateInput this.from = this.value; } + @Listen('invalid') + async onInvalid(event: Event) { + // Prevent the browser's native validation tooltip — the component provides + // its own styled error message via ix-field-wrapper. Calling preventDefault() + // suppresses the bubble but does not affect the validity result returned by + // form.reportValidity(). + event.preventDefault(); + await reportFieldValidity(this, this._hasInvalidInput); + } + /** * Clears the input value and resets the touched state. * @@ -356,32 +400,13 @@ export class DateInput */ @Method() async clear(): Promise { - return clearInputValue(this, { - setClearing: (isClearing) => { - this.isClearing = isClearing; - }, - syncValidationClasses: () => this.syncValidationClasses(), + this._hasInvalidInput = false; + await clearInputValue(this, { additionalCleanup: () => { this.from = undefined; }, }); - } - - private async syncValidationClasses(): Promise { - const skipValidation = await shouldSuppressInternalValidation(this); - if (skipValidation) { - return; - } - - const hasValue = !!this.value; - if (this.required) { - this.hostElement.classList.toggle( - 'ix-invalid--required', - !hasValue && this.touched - ); - } else { - this.hostElement.classList.remove('ix-invalid--required'); - } + this.syncFormInternalsValidity(); } /** @internal */ @@ -414,16 +439,41 @@ export class DateInput } private handleEmptyInput(value: string | undefined): void { + this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; - this.emitValidityStateChangeIfChanged(); + // Remove any stale parse-error host classes from a prior reportValidity() + // call. The value is now empty so those errors no longer apply. + this.hostElement.classList.remove( + 'ix-invalid--validity-invalid', + 'ix-invalid--validity-patternMismatch' + ); + // If the user has already interacted (touched via reportValidity or blur) + // and the field is required, the required-missing error must stay visible — + // clearing the value does not fix a required constraint. + if (this.touched && this.required) { + this.hostElement.classList.add('ix-invalid--required'); + } this.updateFormInternalValue(value); + this.syncFormInternalsValidity(); + emitPickerValidityState(this); this.valueChange.emit(value); } private handleSuppressedValidationInput(value: string): void { + this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; + // Clear the isInvalid @State and host invalid classes that may have been + // set by a prior reportValidity() call. Without this, setting a valid value + // programmatically after reportValidity() showed red would leave the field + // red even though the value is now valid. + this.isInvalid = false; + this.hostElement.classList.remove( + 'ix-invalid--required', + 'ix-invalid--validity-invalid', + 'ix-invalid--validity-patternMismatch' + ); this.updateFormInternalValue(value); resetPickerValueIfInvalid( @@ -436,25 +486,30 @@ export class DateInput this.closeDropdown(); focusInputIfKeyboardMode(this.inputElementRef.current); - this.emitValidityStateChangeIfChanged(); + this.syncFormInternalsValidity(); + emitPickerValidityState(this); this.valueChange.emit(value); } private handleValidatedInput(value: string): void { const validation = this.getDateValidation(value); - this.isInputInvalid = !validation.isValid; + this._hasInvalidInput = !validation.isValid; + // Only show visual invalid state when the user has interacted + this.isInputInvalid = this._hasInvalidInput && this.touched; - if (this.isInputInvalid) { + if (this._hasInvalidInput) { this.invalidReason = validation.invalidReason; this.from = undefined; } else { + this.invalidReason = undefined; this.updateFormInternalValue(value); this.closeDropdown(); focusInputIfKeyboardMode(this.inputElementRef.current); } - this.emitValidityStateChangeIfChanged(); + this.syncFormInternalsValidity(); + emitPickerValidityState(this); this.valueChange.emit(value); } @@ -469,6 +524,13 @@ export class DateInput return; } + // Set _hasInvalidInput synchronously BEFORE the async suppression check so + // that if blur fires during the microtask gap, it always reads the correct + // state. handleValidatedInput and handleSuppressedValidationInput will + // overwrite it with the same (or reset) value afterwards. + const validation = this.getDateValidation(value); + this._hasInvalidInput = !validation.isValid; + const suppressValidation = await shouldSuppressInternalValidation(this); if (suppressValidation) { this.handleSuppressedValidationInput(value); @@ -542,15 +604,21 @@ export class DateInput onFocus={async () => { this.ixFocus.emit(); }} - onBlur={() => { - this.touched = true; - onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); - this.emitValidityStateChangeIfChanged(); - }} + onBlur={(e: FocusEvent) => + handlePickerInputBlur(e, this.show, this.hostElement, () => { + this.touched = true; + this.isInputInvalid = this._hasInvalidInput; + onInputBlurWithChange( + this, + this.inputElementRef.current, + this.value + ); + emitPickerValidityState(this); + // Signal to onFocusout (which fires right after) that validation + // has already been committed so it should not repeat it. + this._blurHandledValidation = true; + }) + } onKeyDown={(event) => this.handleInputKeyDown(event)} style={{ textAlign: this.textAlignment, @@ -590,15 +658,21 @@ export class DateInput this.isWarning = isWarning; } - private emitValidityStateChangeIfChanged() { - return emitPickerValidityState(this); - } - /** @internal */ @Method() getValidityState(): Promise { + // Gate patternMismatch on touched — same as isInputInvalid. + // HookValidationLifecycle reads this to set ix-invalid--validity-patternMismatch + // on the host, which drives visual feedback. We must not show errors before + // the user has interacted. + // The actual form validity for form.reportValidity() is handled separately + // by syncFormInternalsValidity() which always reflects the true parse state. return Promise.resolve( - createValidityState(this.isInputInvalid, !!this.required, this.value) + createValidityState( + this._hasInvalidInput && this.touched, + !!this.required && this.touched, + this.value + ) ); } @@ -627,16 +701,53 @@ export class DateInput return Promise.resolve(this.touched); } + /** + * Triggers validation and shows visual error state immediately, regardless + * of whether the user has interacted with the field. + * + * Use this when submitting via AJAX (no HTML form) or when you need to + * programmatically surface validation errors — equivalent to calling + * `reportValidity()` on a native `` element. + * + * Unlike form submit, this explicit validation call is not suppressed by a + * surrounding `` and will still surface errors. + * + * @returns `true` if the field is valid, `false` otherwise. + * + * @since 5.1.0 + */ + @Method() + async reportValidity(): Promise { + const hasInvalidInput = + !!this.value && + !!this.format && + !this.getDateValidation(this.value).isValid; + + // Sync _hasInvalidInput so that subsequent blur events preserve the error + // state surfaced by this explicit call. Without this, blur resets + // isInputInvalid back to _hasInvalidInput (which is false in novalidate + // forms due to suppression), causing the error message to disappear while + // the red border stays. + this._hasInvalidInput = hasInvalidInput; + + return reportFieldValidity(this, hasInvalidInput); + } + getPickerElement(): MakeRef | null { return this.dropdownElementRef; } override render() { - const invalidText = getValidationText( - this.isInputInvalid, - this.invalidText, - this.i18nErrorDateUnparsable - ); + // Error text priority: + // 1. Parse error — "Date is not valid" (or i18n override / custom invalidText) + // 2. Required empty — "This field is required" (or custom invalidText) + // 3. Consumer-supplied invalidText only + const isRequiredEmpty = !!this.required && !this.value && this.touched; + const invalidText = this.isInputInvalid + ? (this.invalidText ?? this.i18nErrorDateUnparsable) + : isRequiredEmpty + ? (this.invalidText ?? this.i18nErrorRequired) + : this.invalidText; return ( { - const relatedTarget = e.relatedTarget as Node; + onFocusout={(e: FocusEvent) => + handlePickerHostFocusout(e, this.hostElement, (hasRelatedTarget) => { + // Only close the dropdown when focus went to a known external + // element. When relatedTarget is null (focus to or a + // programmatic blur from tests) the dropdown's closeBehavior + // handles it, and closing here risks a flicker during rerenders. + if (hasRelatedTarget) { + this.closeDropdown(); + } - // Related target might be null during rerenders, which would cause the dropdown to close unexpectedly - if (!relatedTarget) { - return; - } + // When the input's onBlur was suppressed (picker was open, + // focus moved to an internal element) we must now run the full + // validation — the user has truly left the component. + // When onBlur already ran validation (direct tab-away), skip it + // here to avoid double-emitting ixBlur / ixChange. + if (this._blurHandledValidation) { + this._blurHandledValidation = false; + return; + } - this.closeDropdown(); - }} + this.touched = true; + this.isInputInvalid = this._hasInvalidInput; + onInputBlurWithChange( + this, + this.inputElementRef.current, + this.value + ); + emitPickerValidityState(this); + }) + } > { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } +); + +regressionTest( + 'programmatic invalid value does not show visual error before user interaction, shows after blur, setting valid value clears error', async ({ page, mount }) => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); - await dateInput.evaluateHandle((el) => { - el.setAttribute('value', 'invalid-date'); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'invalid-date'; }); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + await input.focus(); + await input.blur(); await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid/); - await dateInput.evaluateHandle((el) => { - el.setAttribute('value', '2024/05/05'); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = '2024/05/05'; }); - await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); } ); @@ -297,18 +315,14 @@ regressionTest.describe('keyboard navigation', () => { regressionTest.describe('date-input validation scenarios', () => { regressionTest( - 'Required input: Invalid input > Removing value with keyboard > Stays invalid', + 'Required input: Initial invalid input > Removing value with keyboard > Stays invalid', async ({ page, mount }) => { await mount( - `` + `` ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); - - await input.focus(); - await input.fill('invalid-date'); - await input.blur(); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.selectText(); @@ -320,14 +334,14 @@ regressionTest.describe('date-input validation scenarios', () => { ); regressionTest( - 'Required input: Invalid input > Remove touched state (clear) > Valid again', + 'Required input: Enter invalid input > Remove touched state (clear) > Valid again', async ({ page, mount }) => { await mount( `` ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.fill('invalid-date'); @@ -341,22 +355,24 @@ regressionTest.describe('date-input validation scenarios', () => { ); regressionTest( - 'Required input: Invalid input > Programmatically setting to empty > Stays invalid', + 'Required input: Initial invalid input > Programmatically setting to empty > Stays invalid (no immediate red, shows after blur)', async ({ page, mount }) => { await mount( `` ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); - - await input.focus(); - await input.blur(); + const input = dateInput.getByRole('textbox'); await dateInput.evaluate((el: HTMLIxDateInputElement) => { el.value = ''; }); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + + await input.focus(); + await input.blur(); + await expect(dateInput).toHaveClass(/ix-invalid--required/); } ); @@ -369,7 +385,7 @@ regressionTest.describe('date-input validation scenarios', () => { ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.selectText(); @@ -388,7 +404,7 @@ regressionTest.describe('date-input validation scenarios', () => { ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.blur(); @@ -401,21 +417,62 @@ regressionTest.describe('date-input validation scenarios', () => { ); regressionTest( - 'Required input: Valid input > Programmatically setting to empty > It is invalid', + 'Required input: Valid input > Programmatically setting to empty > It is invalid (no immediate red, shows after blur)', async ({ page, mount }) => { await mount( `` ); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + // onInput() is async — wait for it to complete before asserting + await expect + .poll(() => + dateInput.evaluate((el: HTMLIxDateInputElement) => el.value) + ) + .toBe(''); + + // Programmatic empty must NOT immediately show the required error + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + // After next blur the required error must appear await input.focus(); await input.blur(); + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'Required input: Programmatically setting to empty > Error shows on form reportValidity', + async ({ page, mount }) => { + await mount(` + + + + `); + + const dateInput = page.locator('ix-date-input'); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { el.value = ''; }); + // onInput() is async — poll until the value settles before asserting. + await expect + .poll(() => + dateInput.evaluate((el: HTMLIxDateInputElement) => el.value) + ) + .toBe(''); + + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + + await page.evaluate(() => { + (document.getElementById('f') as HTMLFormElement).reportValidity(); + }); await expect(dateInput).toHaveClass(/ix-invalid--required/); } @@ -427,7 +484,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.fill('invalid-date'); @@ -449,7 +506,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.fill('invalid-date'); @@ -468,7 +525,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.blur(); @@ -488,7 +545,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.selectText(); @@ -506,7 +563,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.blur(); @@ -524,7 +581,7 @@ regressionTest.describe('date-input validation scenarios', () => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.blur(); @@ -548,7 +605,7 @@ regressionTest.describe('date-input validation scenarios', () => { `); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.selectText(); @@ -569,7 +626,7 @@ regressionTest.describe('date-input validation scenarios', () => { `); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.fill('2025/10/10/10'); await input.blur(); @@ -578,13 +635,46 @@ regressionTest.describe('date-input validation scenarios', () => { } ); + regressionTest( + 'novalidate form: submit event fires even when required field is empty', + async ({ page, mount }) => { + // HTML spec §4.10.22.3 step 5.4: when the no-validate state is true + // (novalidate present) the browser skips constraint validation entirely, + // so the submit event MUST fire regardless of the field's validity state. + await mount(` +
+ + +
+ `); + + const submitPromise = page.locator('#f').evaluate( + (form) => + new Promise((resolve) => { + form.addEventListener('submit', (e) => { + e.preventDefault(); + resolve(true); + }); + }) + ); + + await page.locator('button[type="submit"]').click(); + const submitted = await submitPromise; + expect(submitted).toBe(true); + + // novalidate: browser skips constraint validation → no visual errors shown + const dateInput = page.locator('ix-date-input'); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + regressionTest( 'Validation works after switching between required and non-required', async ({ page, mount }) => { await mount(``); const dateInput = page.locator('ix-date-input'); - const input = page.locator('input'); + const input = dateInput.getByRole('textbox'); await input.focus(); await input.selectText(); @@ -606,4 +696,345 @@ regressionTest.describe('date-input validation scenarios', () => { await expect(dateInput).not.toHaveClass(/ix-invalid--required/); } ); + + regressionTest( + 'reportValidity returns false and shows error for invalid date without prior interaction', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // No user interaction yet — field should be visually clean + await expect(input).not.toHaveClass(/is-invalid/); + + // reportValidity() should surface the error immediately and return false + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(false); + // Inner input border must be red + await expect(input).toHaveClass(/is-invalid/); + // Host must carry ix-invalid--validity-invalid so the title also turns red + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'reportValidity returns false and shows required error for empty required field', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // No user interaction yet — field should be visually clean + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(false); + // Host must show required error class (drives title and field wrapper styling) + await expect(dateInput).toHaveClass(/ix-invalid--required/); + // Inner input must NOT show is-invalid for a required-but-empty field + // (required is a separate concern from parse/format error) + await expect(input).not.toHaveClass(/is-invalid/); + } + ); + + regressionTest( + 'reportValidity returns true for a valid field', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(true); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + + regressionTest( + 'novalidate form: reportValidity() on required field still validates explicitly', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // Explicit reportValidity() must still surface validation even in a + // novalidate form, matching native input behavior. + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(false); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'novalidate form: reportValidity() on optional invalid value still validates explicitly', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(false); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'form.reportValidity() with invalid date (parse error) shows field and title red', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // No interaction — clean + await expect(input).not.toHaveClass(/is-invalid/); + + await page.evaluate(() => { + (document.getElementById('f') as HTMLFormElement).reportValidity(); + }); + + // Both field border and title must turn red + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'after reportValidity() shows red, correcting value clears the error', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + + // Correct the value — error must clear immediately (touched=true after reportValidity) + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = '2024/05/05'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'after reportValidity() shows red, clear() resets to pristine', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + // Error is shown + await expect(input).toHaveClass(/is-invalid/); + + // clear() must reset touched and all visual error state + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + + regressionTest( + 'novalidate form: reportValidity() error persists after setting the same invalid value again', + async ({ page, mount }) => { + // Scenario: invalid set → reportValidity() → invalid set again + // The error must remain because the value is still invalid. + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // Set invalid value programmatically — novalidate suppresses visual error + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + + // reportValidity() surfaces the error + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + expect(isValid).toBe(false); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + + // Set the same invalid value again — error must remain (value is still invalid) + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'novalidate form: reportValidity() error clears after setting a valid value', + async ({ page, mount }) => { + // Scenario: invalid set → reportValidity() → valid set + // Error must clear because the value is now valid. + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // Set invalid, then surface error via reportValidity() + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + + // Set a valid value — error must clear immediately + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = '2024/05/05'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + + regressionTest( + 'novalidate form: reportValidity() error survives blur — red border and message stay visible', + async ({ page, mount }) => { + // After reportValidity() surfaces an error, manually blurring the field + // must NOT remove the error text message — the text description must remain visible. + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + + // Blur the field — error state must be fully preserved + await input.focus(); + await input.blur(); + + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'novalidate form: emptying the field after reportValidity() switches error message to required', + async ({ page, mount }) => { + // Scenario: invalid set → reportValidity() → value cleared via "Empty" button + // + // After clearing, the parse-error ("Date is not valid") must go away because + // the field is no longer holding a bad date. However, because the field is + // required and the user has already interacted (touched=true after + // reportValidity()), the required-missing error must now be shown instead — + // both as a red border AND as a text message ("This field is required"). + // + // Showing only a red border without text is not sufficient — + // errors must be described in text and color must not be the only indicator. + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // 1. Set an invalid value — novalidate suppresses visual error + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + + // 2. reportValidity() — parse error surfaces with text message + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'Date is not valid' }) + ).toBeVisible(); + + // 3. Clear the value — parse error must be replaced by required-missing error + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + // Red border is still shown (required + touched) + await expect(dateInput).toHaveClass(/ix-invalid--required/); + await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); + + // Error TEXT message now shows "This field is required" + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'This field is required' }) + ).toBeVisible(); + } + ); }); diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 337132e84f8..5b25a953779 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -324,6 +324,26 @@ export async function emitPickerValidityStateChangeIfChanged( }); } +export async function syncRequiredValidationClass( + hostElement: HTMLElement, + comp: IxFormComponent & { required?: boolean; touched: boolean } +): Promise { + const skipValidation = await shouldSuppressInternalValidation(comp); + if (skipValidation) { + return; + } + + const hasValue = !!comp.value; + if (comp.required) { + hostElement.classList.toggle( + 'ix-invalid--required', + !hasValue && comp.touched + ); + } else { + hostElement.classList.remove('ix-invalid--required'); + } +} + export interface ClearableInputComponent { value?: T; hostElement: HTMLElement; @@ -340,14 +360,10 @@ export async function clearInputValue( defaultValue?: T; additionalCleanup?: () => void; emitValueChange?: boolean; - setClearing?: (isClearing: boolean) => void; - syncValidationClasses?: () => void | Promise; } ): Promise { const emptyValue = options?.defaultValue ?? ('' as T); - options?.setClearing?.(true); - if ('touched' in comp) { comp.touched = false; } @@ -374,10 +390,4 @@ export async function clearInputValue( if (options?.emitValueChange) { comp.valueChange?.emit(emptyValue); } - - if (typeof options?.syncValidationClasses === 'function') { - await options.syncValidationClasses(); - } - - options?.setClearing?.(false); } diff --git a/packages/core/src/components/input/tests/form-ready.ct.ts b/packages/core/src/components/input/tests/form-ready.ct.ts index 06b0b56a512..53be4ec4792 100644 --- a/packages/core/src/components/input/tests/form-ready.ct.ts +++ b/packages/core/src/components/input/tests/form-ready.ct.ts @@ -34,7 +34,7 @@ regressionTest(`form-ready - ix-input`, async ({ mount, page }) => { const inputTags = [ { tag: 'ix-input', fill: 'abc' }, { tag: 'ix-number-input', fill: '123' }, - { tag: 'ix-date-input', fill: '2025-09-25' }, + { tag: 'ix-date-input', fill: '2025/09/25' }, { tag: 'ix-time-input', fill: '13:45:30' }, ]; diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index b7980ef1b6e..4af91ae182d 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -99,3 +99,62 @@ export function createValidityState( valueMissing: !!required && !value, }; } + +function isInternalFocusTarget( + hostElement: HTMLElement, + relatedTarget: Node | null +): boolean { + if (!relatedTarget) { + return false; + } + return ( + hostElement.contains(relatedTarget) || + (hostElement.shadowRoot?.contains(relatedTarget) ?? false) + ); +} + +export function handlePickerInputBlur( + e: FocusEvent, + show: boolean, + hostElement: HTMLElement, + onBlur: () => void +): void { + const relatedTarget = e.relatedTarget as Node | null; + if (show && isInternalFocusTarget(hostElement, relatedTarget)) { + return; + } + onBlur(); +} + +export function handlePickerHostFocusout( + e: FocusEvent, + hostElement: HTMLElement, + onExternalFocusout: (hasRelatedTarget: boolean) => void +): void { + const relatedTarget = e.relatedTarget as Node | null; + if (isInternalFocusTarget(hostElement, relatedTarget)) { + return; + } + onExternalFocusout(relatedTarget !== null); +} + +export function syncCustomInputValidity( + formInternals: ElementInternals, + hasInvalidInput: boolean, + required: boolean | undefined, + value: string | undefined, + invalidMessage: string, + requiredMessage: string = 'Please fill out this field.' +): void { + if (hasInvalidInput) { + formInternals.setValidity({ patternMismatch: true }, invalidMessage); + return; + } + + if (required && !value) { + formInternals.setValidity({ valueMissing: true }, requiredMessage); + return; + } + + formInternals.setValidity({}); +} diff --git a/packages/core/src/components/utils/input/validation.ts b/packages/core/src/components/utils/input/validation.ts index 3cee851e630..5ccb8cf5181 100644 --- a/packages/core/src/components/utils/input/validation.ts +++ b/packages/core/src/components/utils/input/validation.ts @@ -24,8 +24,12 @@ export async function isTouched(host: IxFormComponent) { } } +export interface HasAssociatedForm { + getAssociatedFormElement(): Promise; +} + export async function shouldSuppressInternalValidation( - host: IxFormComponent + host: IxFormComponent | HasAssociatedForm ) { if ( host.getAssociatedFormElement && @@ -252,3 +256,28 @@ export function getValidationText( } return customInvalidText; } + +export function reportFieldValidity( + comp: IxFormComponent & { + touched: boolean; + isInputInvalid?: boolean; + hostElement: HTMLElement; + }, + hasInvalidInput: boolean +): boolean { + comp.touched = true; + + if ('isInputInvalid' in comp) { + comp.isInputInvalid = hasInvalidInput; + } + + comp.hostElement.classList.toggle( + 'ix-invalid--validity-invalid', + hasInvalidInput + ); + + const isRequiredMissing = !!comp.required && !comp.value; + comp.hostElement.classList.toggle('ix-invalid--required', isRequiredMissing); + + return !hasInvalidInput && !isRequiredMissing; +} diff --git a/packages/react/src/components.server.ts b/packages/react/src/components.server.ts index aa4251631d0..206a040b487 100644 --- a/packages/react/src/components.server.ts +++ b/packages/react/src/components.server.ts @@ -559,6 +559,7 @@ export const IxDateInput: StencilReactComponent Date: Fri, 5 Jun 2026 13:34:05 +0530 Subject: [PATCH 07/39] refactor(date-input): reorganize regression tests for clarity and consistency - Moved and restructured tests related to initial invalid values and user interactions. - Consolidated required and optional field behavior tests for better readability. - Enhanced test descriptions for improved understanding of scenarios. - Ensured consistent handling of visual validation states across tests. --- .../src/components/date-input/date-input.tsx | 95 +- .../date-input/tests/date-input.ct.ts | 1182 ++++++++--------- 2 files changed, 610 insertions(+), 667 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index ce5dc0c498a..9a0f68aaa31 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -306,6 +306,15 @@ export class DateInput */ private _blurHandledValidation = false; + /** + * Set to `true` only by an explicit `reportValidity()` call — NOT by blur. + * Used as the gate in novalidate forms to keep showing errors after an + * explicit validation request until the value is actually corrected. + * Regular blur in a novalidate form must never set this flag so that normal + * novalidate suppression is preserved. + */ + private _reportValidityCalled = false; + public initialValue?: string; public invalidReason?: string; @@ -401,6 +410,7 @@ export class DateInput @Method() async clear(): Promise { this._hasInvalidInput = false; + this._reportValidityCalled = false; await clearInputValue(this, { additionalCleanup: () => { this.from = undefined; @@ -438,7 +448,7 @@ export class DateInput }; } - private handleEmptyInput(value: string | undefined): void { + private async handleEmptyInput(value: string | undefined): Promise { this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; @@ -448,11 +458,19 @@ export class DateInput 'ix-invalid--validity-invalid', 'ix-invalid--validity-patternMismatch' ); - // If the user has already interacted (touched via reportValidity or blur) - // and the field is required, the required-missing error must stay visible — - // clearing the value does not fix a required constraint. - if (this.touched && this.required) { + // In a regular form, show the required-missing error once the user has + // interacted (touched via blur or reportValidity). + // In a novalidate form, only show it when reportValidity() was explicitly + // called — a plain blur must not surface errors (novalidate intent). + const suppress = await shouldSuppressInternalValidation(this); + const shouldShowRequired = suppress + ? this._reportValidityCalled && !!this.required + : this.touched && !!this.required; + + if (shouldShowRequired) { this.hostElement.classList.add('ix-invalid--required'); + } else { + this.hostElement.classList.remove('ix-invalid--required'); } this.updateFormInternalValue(value); this.syncFormInternalsValidity(); @@ -460,14 +478,19 @@ export class DateInput this.valueChange.emit(value); } - private handleSuppressedValidationInput(value: string): void { + private emitSuppressedValidationChange(value: string): void { + this.syncFormInternalsValidity(); + emitPickerValidityState(this); + this.valueChange.emit(value); + } + + // Clears all error state and accepts the value as valid. + // Used for normal novalidate input AND when the user corrects a + // reportValidity() error. + private acceptSuppressedValidationValue(value: string): void { this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; - // Clear the isInvalid @State and host invalid classes that may have been - // set by a prior reportValidity() call. Without this, setting a valid value - // programmatically after reportValidity() showed red would leave the field - // red even though the value is now valid. this.isInvalid = false; this.hostElement.classList.remove( 'ix-invalid--required', @@ -486,9 +509,47 @@ export class DateInput this.closeDropdown(); focusInputIfKeyboardMode(this.inputElementRef.current); - this.syncFormInternalsValidity(); - emitPickerValidityState(this); - this.valueChange.emit(value); + this.emitSuppressedValidationChange(value); + } + + // Keeps the parse-error classes and message visible after reportValidity(). + private keepSuppressedValidationErrorVisible( + value: string, + invalidReason?: string + ): void { + this.invalidReason = invalidReason; + this.from = undefined; + this.isInvalid = true; + this.hostElement.classList.remove('ix-invalid--required'); + this.hostElement.classList.add('ix-invalid--validity-invalid'); + this.emitSuppressedValidationChange(value); + } + + private handleSuppressedValidationInput(value: string): void { + // When reportValidity() was called explicitly, keep validating in the + // novalidate form until the value is actually fixed (WCAG 1.4.1 / 3.3.1). + // NOTE: `touched` is intentionally NOT used — it is set by plain blur too, + // and blur in a novalidate form must never surface errors. + if (!this._reportValidityCalled) { + this.acceptSuppressedValidationValue(value); + return; + } + + const validation = this.getDateValidation(value); + this._hasInvalidInput = !validation.isValid; + this.isInputInvalid = this._hasInvalidInput; + + if (this._hasInvalidInput) { + this.keepSuppressedValidationErrorVisible( + value, + validation.invalidReason + ); + return; + } + + // Value is now valid — reset the flag so normal novalidate suppression resumes. + this._reportValidityCalled = false; + this.acceptSuppressedValidationValue(value); } private handleValidatedInput(value: string): void { @@ -516,7 +577,7 @@ export class DateInput async onInput(value: string | undefined) { this.value = value; if (!value) { - this.handleEmptyInput(value); + await this.handleEmptyInput(value); return; } @@ -730,6 +791,12 @@ export class DateInput // the red border stays. this._hasInvalidInput = hasInvalidInput; + // Mark that an explicit validation call was made. This allows + // handleSuppressedValidationInput and handleEmptyInput to keep showing + // errors in novalidate forms until the value is corrected — while normal + // blur events in novalidate forms remain suppressed. + this._reportValidityCalled = true; + return reportFieldValidity(this, hasInvalidInput); } diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index e392d7ff094..ad115f1d008 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -194,19 +194,6 @@ regressionTest( } ); -regressionTest( - 'initial invalid value does not show visual error before user interaction', - async ({ page, mount }) => { - await mount(``); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } -); - regressionTest( 'programmatic invalid value does not show visual error before user interaction, shows after blur, setting valid value clears error', async ({ page, mount }) => { @@ -234,6 +221,19 @@ regressionTest( } ); +regressionTest( + 'initial invalid value does not show visual error before user interaction', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } +); + regressionTest( 'invalidText property takes precedence over i18n error message', async ({ mount, page }) => { @@ -314,727 +314,603 @@ regressionTest.describe('keyboard navigation', () => { }); regressionTest.describe('date-input validation scenarios', () => { - regressionTest( - 'Required input: Initial invalid input > Removing value with keyboard > Stays invalid', - async ({ page, mount }) => { - await mount( - `` - ); + regressionTest.describe('required field behavior', () => { + regressionTest( + 'Required input: Invalid input > Removing value with keyboard > Stays invalid', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + regressionTest( + 'Required input: Enter invalid input > Remove touched state (clear) > Valid again', + async ({ page, mount }) => { + await mount(``); - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).toHaveClass(/ix-invalid--required/); - } - ); + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); - regressionTest( - 'Required input: Enter invalid input > Remove touched state (clear) > Valid again', - async ({ page, mount }) => { - await mount( - `` - ); + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await expectNoVisualValidation(dateInput, input); + await expect(input).toHaveValue(''); + await expect(dateInput).toHaveAttribute('value', ''); + } + ); - await input.focus(); - await input.fill('invalid-date'); - await input.blur(); + regressionTest( + 'Required input: Invalid input > Programmatically setting to empty > Stays invalid (no immediate red, shows after blur)', + async ({ page, mount }) => { + await mount( + `` + ); - await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); - regressionTest( - 'Required input: Initial invalid input > Programmatically setting to empty > Stays invalid (no immediate red, shows after blur)', - async ({ page, mount }) => { - await mount( - `` - ); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await input.focus(); + await input.blur(); - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + regressionTest( + 'Required input: Valid input > Removing value with keyboard > It is invalid', + async ({ page, mount }) => { + await mount( + `` + ); - await input.focus(); - await input.blur(); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).toHaveClass(/ix-invalid--required/); - } - ); + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); - regressionTest( - 'Required input: Valid input > Removing value with keyboard > It is invalid', - async ({ page, mount }) => { - await mount( - `` - ); + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + regressionTest( + 'Required input: Valid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount( + `` + ); - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).toHaveClass(/ix-invalid--required/); - } - ); + await input.focus(); + await input.blur(); - regressionTest( - 'Required input: Valid input > Remove touched state (clear) > Valid', - async ({ page, mount }) => { - await mount( - `` - ); + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await expectNoVisualValidation(dateInput, input); + await expect(input).toHaveValue(''); + await expect(dateInput).toHaveAttribute('value', ''); + } + ); - await input.focus(); - await input.blur(); + regressionTest( + 'Required input: Valid input > Programmatically setting to empty > It is invalid (no immediate red, shows after blur)', + async ({ page, mount }) => { + await mount( + `` + ); - await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); - regressionTest( - 'Required input: Valid input > Programmatically setting to empty > It is invalid (no immediate red, shows after blur)', - async ({ page, mount }) => { - await mount( - `` - ); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); - // onInput() is async — wait for it to complete before asserting - await expect - .poll(() => - dateInput.evaluate((el: HTMLIxDateInputElement) => el.value) - ) - .toBe(''); - - // Programmatic empty must NOT immediately show the required error - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - - // After next blur the required error must appear - await input.focus(); - await input.blur(); - - await expect(dateInput).toHaveClass(/ix-invalid--required/); - } - ); + await expect(dateInput).toHaveAttribute('value', ''); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - regressionTest( - 'Required input: Programmatically setting to empty > Error shows on form reportValidity', - async ({ page, mount }) => { - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); - // onInput() is async — poll until the value settles before asserting. - await expect - .poll(() => - dateInput.evaluate((el: HTMLIxDateInputElement) => el.value) - ) - .toBe(''); - - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - - await page.evaluate(() => { - (document.getElementById('f') as HTMLFormElement).reportValidity(); - }); - - await expect(dateInput).toHaveClass(/ix-invalid--required/); - } - ); + await input.focus(); + await input.blur(); - regressionTest( - 'Not required input: Invalid input > Removing value with keyboard > Valid', - async ({ page, mount }) => { - await mount(``); + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + regressionTest( + 'Required input: Programmatically setting to empty > Error shows on form reportValidity', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); - await input.focus(); - await input.fill('invalid-date'); - await input.blur(); + const dateInput = page.locator('ix-date-input'); - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await page.evaluate(() => { + (document.getElementById('form') as HTMLFormElement).reportValidity(); + }); - regressionTest( - 'Not required input: Invalid input > Remove touched state (clear) > Valid', - async ({ page, mount }) => { - await mount(``); + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + regressionTest( + 'Validation works after switching between required and non-required', + async ({ page, mount }) => { + await mount(``); - await input.focus(); - await input.fill('invalid-date'); - await input.blur(); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - regressionTest( - 'Not required input: Invalid input > Programmatically setting to empty > Valid', - async ({ page, mount }) => { - await mount(``); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.required = true; + }); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await expect(dateInput).toHaveClass(/ix-invalid--required/); - await input.focus(); - await input.blur(); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.required = false; + }); - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + } + ); + }); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + regressionTest.describe('optional field behavior', () => { + regressionTest( + 'Not required input: Invalid input > Removing value with keyboard > Valid', + async ({ page, mount }) => { + await mount(``); - regressionTest( - 'Not required input: Valid input > Removing value with keyboard > Valid', - async ({ page, mount }) => { - await mount(``); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await expectNoVisualValidation(dateInput, input); + } + ); - regressionTest( - 'Not required input: Valid input > Remove touched state (clear) > Valid', - async ({ page, mount }) => { - await mount(``); + regressionTest( + 'Not required input: Invalid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount(``); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await input.focus(); - await input.blur(); + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); - await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await expectNoVisualValidation(dateInput, input); + await expect(input).toHaveValue(''); + await expect(dateInput).toHaveAttribute('value', ''); + } + ); - regressionTest( - 'Not required input: Valid input > Programmatically setting to empty > Valid', - async ({ page, mount }) => { - await mount(``); + regressionTest( + 'Not required input: Invalid input > Programmatically setting to empty > Valid', + async ({ page, mount }) => { + await mount(``); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await input.focus(); - await input.blur(); + await input.focus(); + await input.fill('invalid-date'); + await input.blur(); - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + await expectNoVisualValidation(dateInput, input); + } + ); - regressionTest( - 'novalidate form suppresses validation for required field', - async ({ page, mount }) => { - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); - - await expectNoVisualValidation(dateInput, input); - } - ); + regressionTest( + 'Not required input: Valid input > Removing value with keyboard > Valid', + async ({ page, mount }) => { + await mount(``); - regressionTest( - 'novalidate form suppresses visual validation for invalid date input', - async ({ page, mount }) => { - await mount(` -
- -
- `); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); - await input.fill('2025/10/10/10'); - await input.blur(); + await expectNoVisualValidation(dateInput, input); + } + ); - await expectNoVisualValidation(dateInput, input); - } - ); + regressionTest( + 'Not required input: Valid input > Remove touched state (clear) > Valid', + async ({ page, mount }) => { + await mount(``); - regressionTest( - 'novalidate form: submit event fires even when required field is empty', - async ({ page, mount }) => { - // HTML spec §4.10.22.3 step 5.4: when the no-validate state is true - // (novalidate present) the browser skips constraint validation entirely, - // so the submit event MUST fire regardless of the field's validity state. - await mount(` -
- - -
- `); - - const submitPromise = page.locator('#f').evaluate( - (form) => - new Promise((resolve) => { - form.addEventListener('submit', (e) => { - e.preventDefault(); - resolve(true); - }); - }) - ); - - await page.locator('button[type="submit"]').click(); - const submitted = await submitPromise; - expect(submitted).toBe(true); - - // novalidate: browser skips constraint validation → no visual errors shown - const dateInput = page.locator('ix-date-input'); - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } - ); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - regressionTest( - 'Validation works after switching between required and non-required', - async ({ page, mount }) => { - await mount(``); + await input.focus(); + await input.blur(); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); - await input.focus(); - await input.selectText(); - await input.press('Delete'); - await input.blur(); + await expectNoVisualValidation(dateInput, input); + await expect(input).toHaveValue(''); + await expect(dateInput).toHaveAttribute('value', ''); + } + ); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); + regressionTest( + 'Not required input: Valid input > Programmatically setting to empty > Valid', + async ({ page, mount }) => { + await mount(``); - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.required = true; - }); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - await expect(dateInput).toHaveClass(/ix-invalid--required/); + await input.focus(); + await input.blur(); - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.required = false; - }); + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - } - ); + await expectNoVisualValidation(dateInput, input); + } + ); + }); - regressionTest( - 'reportValidity returns false and shows error for invalid date without prior interaction', - async ({ page, mount }) => { - await mount(``); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // No user interaction yet — field should be visually clean - await expect(input).not.toHaveClass(/is-invalid/); - - // reportValidity() should surface the error immediately and return false - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - - expect(isValid).toBe(false); - // Inner input border must be red - await expect(input).toHaveClass(/is-invalid/); - // Host must carry ix-invalid--validity-invalid so the title also turns red - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + regressionTest.describe('novalidate form behavior', () => { + regressionTest( + 'novalidate form suppresses validation for required field', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await input.focus(); + await input.selectText(); + await input.press('Delete'); + await input.blur(); + + await expectNoVisualValidation(dateInput, input); + } + ); - regressionTest( - 'reportValidity returns false and shows required error for empty required field', - async ({ page, mount }) => { - await mount(``); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // No user interaction yet — field should be visually clean - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); - - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - - expect(isValid).toBe(false); - // Host must show required error class (drives title and field wrapper styling) - await expect(dateInput).toHaveClass(/ix-invalid--required/); - // Inner input must NOT show is-invalid for a required-but-empty field - // (required is a separate concern from parse/format error) - await expect(input).not.toHaveClass(/is-invalid/); - } - ); + regressionTest( + 'novalidate form suppresses visual validation for invalid date input', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); - regressionTest( - 'reportValidity returns true for a valid field', - async ({ page, mount }) => { - await mount(``); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); + await input.fill('2025/10/10/10'); + await input.blur(); - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); + await expectNoVisualValidation(dateInput, input); + } + ); - expect(isValid).toBe(true); - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } - ); + regressionTest( + 'novalidate form: submit event fires even when required field is empty', + async ({ page, mount }) => { + await mount(` +
+ + +
+ `); + + const submitPromise = page.locator('#form').evaluate( + (form) => + new Promise((resolve) => { + form.addEventListener('submit', (e) => { + e.preventDefault(); + resolve(true); + }); + }) + ); + + await page.locator('button[type="submit"]').click(); + const submitted = await submitPromise; + expect(submitted).toBe(true); + + const dateInput = page.locator('ix-date-input'); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); - regressionTest( - 'novalidate form: reportValidity() on required field still validates explicitly', - async ({ page, mount }) => { - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // Explicit reportValidity() must still surface validation even in a - // novalidate form, matching native input behavior. - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - - expect(isValid).toBe(false); - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + regressionTest( + 'novalidate form: reportValidity() validates invalid date', + async ({ page, mount }) => { + await mount(` +
+ + +
+ `); + + const dateInputs = page.locator('ix-date-input'); + + for (const index of [0, 1]) { + const dateInput = dateInputs.nth(index); + const input = dateInput.getByRole('textbox'); + + const isValid = await dateInput.evaluate( + (el: HTMLIxDateInputElement) => el.reportValidity() + ); + + expect(isValid).toBe(false); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + } + ); - regressionTest( - 'novalidate form: reportValidity() on optional invalid value still validates explicitly', - async ({ page, mount }) => { - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - - expect(isValid).toBe(false); - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + regressionTest( + 'novalidate form: reportValidity() error persists when value remains invalid, clears when fixed', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'still-bad-date'; + }); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = '2024/05/05'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); - regressionTest( - 'form.reportValidity() with invalid date (parse error) shows field and title red', - async ({ page, mount }) => { - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // No interaction — clean - await expect(input).not.toHaveClass(/is-invalid/); - - await page.evaluate(() => { - (document.getElementById('f') as HTMLFormElement).reportValidity(); - }); - - // Both field border and title must turn red - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + regressionTest( + 'novalidate form: emptying the field after reportValidity() switches error message to required', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = 'bad-date'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'Date is not valid' }) + ).toBeVisible(); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = ''; + }); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); + + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'This field is required' }) + ).toBeVisible(); + } + ); + }); - regressionTest( - 'after reportValidity() shows red, correcting value clears the error', - async ({ page, mount }) => { - await mount(``); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - await expect(input).toHaveClass(/is-invalid/); - - // Correct the value — error must clear immediately (touched=true after reportValidity) - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = '2024/05/05'; - }); - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); - } - ); + regressionTest.describe('reportValidity behavior', () => { + regressionTest( + 'reportValidity returns false and shows error for invalid date without prior interaction', + async ({ page, mount }) => { + await mount(``); - regressionTest( - 'after reportValidity() shows red, clear() resets to pristine', - async ({ page, mount }) => { - await mount( - `` - ); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - // Error is shown - await expect(input).toHaveClass(/is-invalid/); - - // clear() must reset touched and all visual error state - await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } - ); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); - regressionTest( - 'novalidate form: reportValidity() error persists after setting the same invalid value again', - async ({ page, mount }) => { - // Scenario: invalid set → reportValidity() → invalid set again - // The error must remain because the value is still invalid. - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // Set invalid value programmatically — novalidate suppresses visual error - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = 'bad-date'; - }); - await expect(input).not.toHaveClass(/is-invalid/); - - // reportValidity() surfaces the error - const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - expect(isValid).toBe(false); - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - - // Set the same invalid value again — error must remain (value is still invalid) - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = 'bad-date'; - }); - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + await expect(input).not.toHaveClass(/is-invalid/); - regressionTest( - 'novalidate form: reportValidity() error clears after setting a valid value', - async ({ page, mount }) => { - // Scenario: invalid set → reportValidity() → valid set - // Error must clear because the value is now valid. - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // Set invalid, then surface error via reportValidity() - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = 'bad-date'; - }); - await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - await expect(input).toHaveClass(/is-invalid/); - - // Set a valid value — error must clear immediately - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = '2024/05/05'; - }); - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } - ); + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); - regressionTest( - 'novalidate form: reportValidity() error survives blur — red border and message stay visible', - async ({ page, mount }) => { - // After reportValidity() surfaces an error, manually blurring the field - // must NOT remove the error text message — the text description must remain visible. - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = 'bad-date'; - }); - await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - await expect(input).toHaveClass(/is-invalid/); - - // Blur the field — error state must be fully preserved - await input.focus(); - await input.blur(); - - await expect(input).toHaveClass(/is-invalid/); - await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); - } - ); + expect(isValid).toBe(false); + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); - regressionTest( - 'novalidate form: emptying the field after reportValidity() switches error message to required', - async ({ page, mount }) => { - // Scenario: invalid set → reportValidity() → value cleared via "Empty" button - // - // After clearing, the parse-error ("Date is not valid") must go away because - // the field is no longer holding a bad date. However, because the field is - // required and the user has already interacted (touched=true after - // reportValidity()), the required-missing error must now be shown instead — - // both as a red border AND as a text message ("This field is required"). - // - // Showing only a red border without text is not sufficient — - // errors must be described in text and color must not be the only indicator. - await mount(` -
- -
- `); - - const dateInput = page.locator('ix-date-input'); - const input = dateInput.getByRole('textbox'); - - // 1. Set an invalid value — novalidate suppresses visual error - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = 'bad-date'; - }); - await expect(input).not.toHaveClass(/is-invalid/); - - // 2. reportValidity() — parse error surfaces with text message - await dateInput.evaluate((el: HTMLIxDateInputElement) => - el.reportValidity() - ); - await expect(input).toHaveClass(/is-invalid/); - await expect( - dateInput - .locator('ix-field-wrapper') - .locator('ix-typography') - .filter({ hasText: 'Date is not valid' }) - ).toBeVisible(); - - // 3. Clear the value — parse error must be replaced by required-missing error - await dateInput.evaluate((el: HTMLIxDateInputElement) => { - el.value = ''; - }); - - // Red border is still shown (required + touched) - await expect(dateInput).toHaveClass(/ix-invalid--required/); - await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); - - // Error TEXT message now shows "This field is required" - await expect( - dateInput - .locator('ix-field-wrapper') - .locator('ix-typography') - .filter({ hasText: 'This field is required' }) - ).toBeVisible(); - } - ); + regressionTest( + 'reportValidity returns false and shows required error for empty required field', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(false); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'reportValidity returns true for a valid field', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + const isValid = await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + expect(isValid).toBe(true); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + + regressionTest( + 'form.reportValidity() with invalid date (parse error) shows field and title red', + async ({ page, mount }) => { + await mount(` +
+ +
+ `); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + // No interaction — clean + await expect(input).not.toHaveClass(/is-invalid/); + + await page.evaluate(() => { + (document.getElementById('form') as HTMLFormElement).reportValidity(); + }); + + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'after reportValidity() shows red, correcting value clears the error', + async ({ page, mount }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + await expect(input).toHaveClass(/is-invalid/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.value = '2024/05/05'; + }); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'after reportValidity() shows red, clear() resets to pristine', + async ({ page, mount }) => { + await mount( + `` + ); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => + el.reportValidity() + ); + + await expect(input).toHaveClass(/is-invalid/); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => el.clear()); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + }); }); From 5bead971408cb76bb021340a795c35b9c75f33e4 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Fri, 5 Jun 2026 14:35:08 +0530 Subject: [PATCH 08/39] feat: add 'i18nErrorRequired' prop to IxDateInput component --- packages/core/src/components.d.ts | 24 +-- .../react/src/components/components.server.ts | 199 +++++++++--------- packages/vue/src/components/ix-date-input.ts | 1 + 3 files changed, 113 insertions(+), 111 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index e9fa91af86a..a589ccb50fa 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -4127,17 +4127,17 @@ export namespace Components { /** * Interval for hour selection. * @since 3.2.0 - * @default 1 + * @default HOUR_INTERVAL_DEFAULT */ "hourInterval": number; /** * Text of the time confirm button. - * @default 'Confirm' + * @default CONFIRM_BUTTON_DEFAULT */ "i18nConfirmTime": string; /** * Text for the top header. - * @default 'Time' + * @default HEADER_DEFAULT */ "i18nHeader": string; /** @@ -4168,7 +4168,7 @@ export namespace Components { /** * Interval for millisecond selection. * @since 3.2.0 - * @default 100 + * @default MILLISECOND_INTERVAL_DEFAULT */ "millisecondInterval": number; /** @@ -4179,13 +4179,13 @@ export namespace Components { /** * Interval for minute selection. * @since 3.2.0 - * @default 1 + * @default MINUTE_INTERVAL_DEFAULT */ "minuteInterval": number; /** * Interval for second selection. * @since 3.2.0 - * @default 1 + * @default SECOND_INTERVAL_DEFAULT */ "secondInterval": number; /** @@ -10693,17 +10693,17 @@ declare namespace LocalJSX { /** * Interval for hour selection. * @since 3.2.0 - * @default 1 + * @default HOUR_INTERVAL_DEFAULT */ "hourInterval"?: number; /** * Text of the time confirm button. - * @default 'Confirm' + * @default CONFIRM_BUTTON_DEFAULT */ "i18nConfirmTime"?: string; /** * Text for the top header. - * @default 'Time' + * @default HEADER_DEFAULT */ "i18nHeader"?: string; /** @@ -10734,7 +10734,7 @@ declare namespace LocalJSX { /** * Interval for millisecond selection. * @since 3.2.0 - * @default 100 + * @default MILLISECOND_INTERVAL_DEFAULT */ "millisecondInterval"?: number; /** @@ -10745,7 +10745,7 @@ declare namespace LocalJSX { /** * Interval for minute selection. * @since 3.2.0 - * @default 1 + * @default MINUTE_INTERVAL_DEFAULT */ "minuteInterval"?: number; /** @@ -10759,7 +10759,7 @@ declare namespace LocalJSX { /** * Interval for second selection. * @since 3.2.0 - * @default 1 + * @default SECOND_INTERVAL_DEFAULT */ "secondInterval"?: number; /** diff --git a/packages/react/src/components/components.server.ts b/packages/react/src/components/components.server.ts index c4a7e03cd0f..01d0eeaa4b3 100644 --- a/packages/react/src/components/components.server.ts +++ b/packages/react/src/components/components.server.ts @@ -130,7 +130,7 @@ export const IxActionCard: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxActionCard as StencilReactComponent, serializeShadowRoot }); @@ -144,7 +144,7 @@ export const IxApplication: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxApplication as StencilReactComponent, serializeShadowRoot }); @@ -170,7 +170,7 @@ export const IxApplicationHeader: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxApplicationHeader as StencilReactComponent, serializeShadowRoot }); @@ -187,7 +187,7 @@ export const IxAvatar: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxAvatar as StencilReactComponent, serializeShadowRoot }); @@ -203,7 +203,7 @@ export const IxBlind: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxBlind as StencilReactComponent, serializeShadowRoot }); @@ -221,7 +221,7 @@ export const IxBreadcrumb: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxBreadcrumb as StencilReactComponent, serializeShadowRoot }); @@ -243,7 +243,7 @@ export const IxBreadcrumbItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxBreadcrumbItem as StencilReactComponent, serializeShadowRoot }); @@ -266,7 +266,7 @@ export const IxButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxButton as StencilReactComponent, serializeShadowRoot }); @@ -280,7 +280,7 @@ export const IxCard: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCard as StencilReactComponent, serializeShadowRoot }); @@ -294,7 +294,7 @@ export const IxCardAccordion: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCardAccordion as StencilReactComponent, serializeShadowRoot }); @@ -304,7 +304,7 @@ export type IxCardContentEvents = NonNullable; export const IxCardContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-card-content', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCardContent as StencilReactComponent, serializeShadowRoot }); @@ -330,7 +330,7 @@ export const IxCardList: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCardList as StencilReactComponent, serializeShadowRoot }); @@ -340,7 +340,7 @@ export type IxCardTitleEvents = NonNullable; export const IxCardTitle: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-card-title', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCardTitle as StencilReactComponent, serializeShadowRoot }); @@ -369,7 +369,7 @@ export const IxCategoryFilter: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCategoryFilter as StencilReactComponent, serializeShadowRoot }); @@ -391,7 +391,7 @@ export const IxCheckbox: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCheckbox as StencilReactComponent, serializeShadowRoot }); @@ -411,7 +411,7 @@ export const IxCheckboxGroup: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCheckboxGroup as StencilReactComponent, serializeShadowRoot }); @@ -433,7 +433,7 @@ export const IxChip: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxChip as StencilReactComponent, serializeShadowRoot }); @@ -448,7 +448,7 @@ export const IxCol: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCol as StencilReactComponent, serializeShadowRoot }); @@ -458,7 +458,7 @@ export type IxContentEvents = NonNullable; export const IxContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-content', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxContent as StencilReactComponent, serializeShadowRoot }); @@ -473,7 +473,7 @@ export const IxContentHeader: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxContentHeader as StencilReactComponent, serializeShadowRoot }); @@ -492,7 +492,7 @@ export const IxCustomField: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxCustomField as StencilReactComponent, serializeShadowRoot }); @@ -520,7 +520,7 @@ export const IxDateDropdown: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDateDropdown as StencilReactComponent, serializeShadowRoot }); @@ -553,6 +553,7 @@ export const IxDateInput: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDateInput as StencilReactComponent, serializeShadowRoot }); @@ -594,7 +595,7 @@ export const IxDatePicker: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDatePicker as StencilReactComponent, serializeShadowRoot }); @@ -641,7 +642,7 @@ export const IxDatetimeInput: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDatetimeInput as StencilReactComponent, serializeShadowRoot }); @@ -676,7 +677,7 @@ export const IxDatetimePicker: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDatetimePicker as StencilReactComponent, serializeShadowRoot }); @@ -686,7 +687,7 @@ export type IxDividerEvents = NonNullable; export const IxDivider: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-divider', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDivider as StencilReactComponent, serializeShadowRoot }); @@ -717,7 +718,7 @@ export const IxDropdown: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDropdown as StencilReactComponent, serializeShadowRoot }); @@ -741,7 +742,7 @@ export const IxDropdownButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDropdownButton as StencilReactComponent, serializeShadowRoot }); @@ -751,7 +752,7 @@ export type IxDropdownHeaderEvents = NonNullable; export const IxDropdownHeader: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-dropdown-header', properties: { label: 'label' }, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDropdownHeader as StencilReactComponent, serializeShadowRoot }); @@ -775,7 +776,7 @@ export const IxDropdownItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDropdownItem as StencilReactComponent, serializeShadowRoot }); @@ -785,7 +786,7 @@ export type IxDropdownQuickActionsEvents = NonNullable; export const IxDropdownQuickActions: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-dropdown-quick-actions', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxDropdownQuickActions as StencilReactComponent, serializeShadowRoot }); @@ -802,7 +803,7 @@ export const IxEmptyState: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxEmptyState as StencilReactComponent, serializeShadowRoot }); @@ -817,7 +818,7 @@ export const IxEventList: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxEventList as StencilReactComponent, serializeShadowRoot }); @@ -833,7 +834,7 @@ export const IxEventListItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxEventListItem as StencilReactComponent, serializeShadowRoot }); @@ -852,7 +853,7 @@ export const IxExpandingSearch: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxExpandingSearch as StencilReactComponent, serializeShadowRoot }); @@ -866,7 +867,7 @@ export const IxFieldLabel: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxFieldLabel as StencilReactComponent, serializeShadowRoot }); @@ -880,7 +881,7 @@ export const IxFilterChip: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxFilterChip as StencilReactComponent, serializeShadowRoot }); @@ -896,7 +897,7 @@ export const IxFlipTile: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxFlipTile as StencilReactComponent, serializeShadowRoot }); @@ -906,7 +907,7 @@ export type IxFlipTileContentEvents = NonNullable; export const IxFlipTileContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-flip-tile-content', properties: { contentVisible: 'content-visible' }, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxFlipTileContent as StencilReactComponent, serializeShadowRoot }); @@ -928,7 +929,7 @@ export const IxGroup: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxGroup as StencilReactComponent, serializeShadowRoot }); @@ -938,7 +939,7 @@ export type IxGroupContextMenuEvents = NonNullable; export const IxGroupContextMenu: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-group-context-menu', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxGroupContextMenu as StencilReactComponent, serializeShadowRoot }); @@ -958,7 +959,7 @@ export const IxGroupItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxGroupItem as StencilReactComponent, serializeShadowRoot }); @@ -975,7 +976,7 @@ export const IxHelperText: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxHelperText as StencilReactComponent, serializeShadowRoot }); @@ -994,7 +995,7 @@ export const IxIconButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxIconButton as StencilReactComponent, serializeShadowRoot }); @@ -1014,7 +1015,7 @@ export const IxIconToggleButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxIconToggleButton as StencilReactComponent, serializeShadowRoot }); @@ -1050,7 +1051,7 @@ export const IxInput: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxInput as StencilReactComponent, serializeShadowRoot }); @@ -1066,7 +1067,7 @@ export const IxKeyValue: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxKeyValue as StencilReactComponent, serializeShadowRoot }); @@ -1076,7 +1077,7 @@ export type IxKeyValueListEvents = NonNullable; export const IxKeyValueList: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-key-value-list', properties: { striped: 'striped' }, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxKeyValueList as StencilReactComponent, serializeShadowRoot }); @@ -1094,7 +1095,7 @@ export const IxKpi: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxKpi as StencilReactComponent, serializeShadowRoot }); @@ -1104,7 +1105,7 @@ export type IxLayoutAutoEvents = NonNullable; export const IxLayoutAuto: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-layout-auto', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxLayoutAuto as StencilReactComponent, serializeShadowRoot }); @@ -1118,7 +1119,7 @@ export const IxLayoutGrid: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxLayoutGrid as StencilReactComponent, serializeShadowRoot }); @@ -1132,7 +1133,7 @@ export const IxLinkButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxLinkButton as StencilReactComponent, serializeShadowRoot }); @@ -1162,7 +1163,7 @@ export const IxMenu: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenu as StencilReactComponent, serializeShadowRoot }); @@ -1181,7 +1182,7 @@ export const IxMenuAbout: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuAbout as StencilReactComponent, serializeShadowRoot }); @@ -1194,7 +1195,7 @@ export const IxMenuAboutItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuAboutItem as StencilReactComponent, serializeShadowRoot }); @@ -1214,7 +1215,7 @@ export const IxMenuAboutNews: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuAboutNews as StencilReactComponent, serializeShadowRoot }); @@ -1234,7 +1235,7 @@ export const IxMenuAvatar: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuAvatar as StencilReactComponent, serializeShadowRoot }); @@ -1247,7 +1248,7 @@ export const IxMenuAvatarItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuAvatarItem as StencilReactComponent, serializeShadowRoot }); @@ -1262,7 +1263,7 @@ export const IxMenuCategory: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuCategory as StencilReactComponent, serializeShadowRoot }); @@ -1285,7 +1286,7 @@ export const IxMenuItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuItem as StencilReactComponent, serializeShadowRoot }); @@ -1304,7 +1305,7 @@ export const IxMenuSettings: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuSettings as StencilReactComponent, serializeShadowRoot }); @@ -1317,7 +1318,7 @@ export const IxMenuSettingsItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMenuSettingsItem as StencilReactComponent, serializeShadowRoot }); @@ -1333,7 +1334,7 @@ export const IxMessageBar: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxMessageBar as StencilReactComponent, serializeShadowRoot }); @@ -1353,7 +1354,7 @@ export const IxModal: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxModal as StencilReactComponent, serializeShadowRoot }); @@ -1363,7 +1364,7 @@ export type IxModalContentEvents = NonNullable; export const IxModalContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-modal-content', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxModalContent as StencilReactComponent, serializeShadowRoot }); @@ -1373,7 +1374,7 @@ export type IxModalFooterEvents = NonNullable; export const IxModalFooter: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-modal-footer', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxModalFooter as StencilReactComponent, serializeShadowRoot }); @@ -1389,7 +1390,7 @@ export const IxModalHeader: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxModalHeader as StencilReactComponent, serializeShadowRoot }); @@ -1427,7 +1428,7 @@ export const IxNumberInput: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxNumberInput as StencilReactComponent, serializeShadowRoot }); @@ -1452,7 +1453,7 @@ export const IxPagination: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxPagination as StencilReactComponent, serializeShadowRoot }); @@ -1480,7 +1481,7 @@ export const IxPane: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxPane as StencilReactComponent, serializeShadowRoot }); @@ -1494,7 +1495,7 @@ export const IxPaneLayout: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxPaneLayout as StencilReactComponent, serializeShadowRoot }); @@ -1513,7 +1514,7 @@ export const IxPill: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxPill as StencilReactComponent, serializeShadowRoot }); @@ -1534,7 +1535,7 @@ export const IxProgressIndicator: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxProgressIndicator as StencilReactComponent, serializeShadowRoot }); @@ -1553,7 +1554,7 @@ export const IxPushCard: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxPushCard as StencilReactComponent, serializeShadowRoot }); @@ -1574,7 +1575,7 @@ export const IxRadio: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxRadio as StencilReactComponent, serializeShadowRoot }); @@ -1595,7 +1596,7 @@ export const IxRadioGroup: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxRadioGroup as StencilReactComponent, serializeShadowRoot }); @@ -1608,7 +1609,7 @@ export const IxRangeField: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxRangeField as StencilReactComponent, serializeShadowRoot }); @@ -1618,7 +1619,7 @@ export type IxRowEvents = NonNullable; export const IxRow: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-row', properties: {}, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxRow as StencilReactComponent, serializeShadowRoot }); @@ -1662,7 +1663,7 @@ export const IxSelect: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxSelect as StencilReactComponent, serializeShadowRoot }); @@ -1679,7 +1680,7 @@ export const IxSelectItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxSelectItem as StencilReactComponent, serializeShadowRoot }); @@ -1704,7 +1705,7 @@ export const IxSlider: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxSlider as StencilReactComponent, serializeShadowRoot }); @@ -1718,7 +1719,7 @@ export const IxSpinner: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxSpinner as StencilReactComponent, serializeShadowRoot }); @@ -1740,7 +1741,7 @@ export const IxSplitButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxSplitButton as StencilReactComponent, serializeShadowRoot }); @@ -1767,7 +1768,7 @@ export const IxTabItem: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTabItem as StencilReactComponent, serializeShadowRoot }); @@ -1788,7 +1789,7 @@ export const IxTabs: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTabs as StencilReactComponent, serializeShadowRoot }); @@ -1824,7 +1825,7 @@ export const IxTextarea: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTextarea as StencilReactComponent, serializeShadowRoot }); @@ -1834,7 +1835,7 @@ export type IxTileEvents = NonNullable; export const IxTile: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-tile', properties: { size: 'size' }, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTile as StencilReactComponent, serializeShadowRoot }); @@ -1881,7 +1882,7 @@ export const IxTimeInput: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTimeInput as StencilReactComponent, serializeShadowRoot }); @@ -1913,7 +1914,7 @@ export const IxTimePicker: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTimePicker as StencilReactComponent, serializeShadowRoot }); @@ -1932,7 +1933,7 @@ export const IxToast: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxToast as StencilReactComponent, serializeShadowRoot }); @@ -1942,7 +1943,7 @@ export type IxToastContainerEvents = NonNullable; export const IxToastContainer: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-toast-container', properties: { position: 'position' }, - hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxToastContainer as StencilReactComponent, serializeShadowRoot }); @@ -1966,7 +1967,7 @@ export const IxToggle: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxToggle as StencilReactComponent, serializeShadowRoot }); @@ -1983,7 +1984,7 @@ export const IxToggleButton: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxToggleButton as StencilReactComponent, serializeShadowRoot }); @@ -2001,7 +2002,7 @@ export const IxTooltip: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTooltip as StencilReactComponent, serializeShadowRoot }); @@ -2016,7 +2017,7 @@ export const IxTypography: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxTypography as StencilReactComponent, serializeShadowRoot }); @@ -2039,7 +2040,7 @@ export const IxUpload: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxUpload as StencilReactComponent, serializeShadowRoot }); @@ -2056,7 +2057,7 @@ export const IxWorkflowStep: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxWorkflowStep as StencilReactComponent, serializeShadowRoot }); @@ -2070,7 +2071,7 @@ export const IxWorkflowSteps: StencilReactComponent) : undefined, + hydrateModule: import('@siemens/ix/hydrate') as Promise, clientModule: clientComponents.IxWorkflowSteps as StencilReactComponent, serializeShadowRoot }); diff --git a/packages/vue/src/components/ix-date-input.ts b/packages/vue/src/components/ix-date-input.ts index 53eedcda202..d46e72efc44 100644 --- a/packages/vue/src/components/ix-date-input.ts +++ b/packages/vue/src/components/ix-date-input.ts @@ -26,6 +26,7 @@ export const IxDateInput: StencilVueComponent Date: Fri, 5 Jun 2026 15:36:27 +0530 Subject: [PATCH 09/39] Fixed sonarqube issues --- .../src/components/date-input/date-input.tsx | 16 ++++++---- .../date-input/tests/date-input.ct.ts | 31 ++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 3ec17b1a156..8ffcda27cad 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -397,7 +397,7 @@ export class DateInput // suppresses the bubble but does not affect the validity result returned by // form.reportValidity(). event.preventDefault(); - await reportFieldValidity(this, this._hasInvalidInput); + reportFieldValidity(this, this._hasInvalidInput); } /** @@ -811,11 +811,15 @@ export class DateInput // 2. Required empty — "This field is required" (or custom invalidText) // 3. Consumer-supplied invalidText only const isRequiredEmpty = !!this.required && !this.value && this.touched; - const invalidText = this.isInputInvalid - ? (this.invalidText ?? this.i18nErrorDateUnparsable) - : isRequiredEmpty - ? (this.invalidText ?? this.i18nErrorRequired) - : this.invalidText; + + let invalidText: string | undefined; + if (this.isInputInvalid) { + invalidText = this.invalidText ?? this.i18nErrorDateUnparsable; + } else if (isRequiredEmpty) { + invalidText = this.invalidText ?? this.i18nErrorRequired; + } else { + invalidText = this.invalidText; + } return ( { await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); }); - -regressionTest.describe('date-input validation scenarios', () => { - regressionTest.describe('required field behavior', () => { +regressionTest.describe( + 'date-input validation scenarios - required field behavior', + () => { regressionTest( 'Required input: Invalid input > Removing value with keyboard > Stays invalid', async ({ page, mount }) => { @@ -494,9 +494,12 @@ regressionTest.describe('date-input validation scenarios', () => { await expect(dateInput).not.toHaveClass(/ix-invalid--required/); } ); - }); + } +); - regressionTest.describe('optional field behavior', () => { +regressionTest.describe( + 'date-input validation scenarios - optional field behavior', + () => { regressionTest( 'Not required input: Invalid input > Removing value with keyboard > Valid', async ({ page, mount }) => { @@ -612,9 +615,12 @@ regressionTest.describe('date-input validation scenarios', () => { await expectNoVisualValidation(dateInput, input); } ); - }); + } +); - regressionTest.describe('novalidate form behavior', () => { +regressionTest.describe( + 'date-input validation scenarios - novalidate form behavior', + () => { regressionTest( 'novalidate form suppresses validation for required field', async ({ page, mount }) => { @@ -788,9 +794,12 @@ regressionTest.describe('date-input validation scenarios', () => { ).toBeVisible(); } ); - }); + } +); - regressionTest.describe('reportValidity behavior', () => { +regressionTest.describe( + 'date-input validation scenarios - reportValidity behavior', + () => { regressionTest( 'reportValidity returns false and shows error for invalid date without prior interaction', async ({ page, mount }) => { @@ -912,5 +921,5 @@ regressionTest.describe('date-input validation scenarios', () => { await expect(dateInput).not.toHaveClass(/ix-invalid/); } ); - }); -}); + } +); From 39f489187b81e129885847555ce9856347ffeaa2 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Fri, 5 Jun 2026 16:34:41 +0530 Subject: [PATCH 10/39] fixed - nested function more than 4 levels deep. --- .../date-input/tests/date-input.ct.ts | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index ea485ba62ec..cb71b076c63 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -41,6 +41,20 @@ const expectNoVisualValidation = async (dateInput: Locator, input: Locator) => { await expect(dateInput).not.toHaveClass(/ix-invalid--required/); }; +const waitForFormSubmit = (form: Locator) => + form.evaluate( + (formElement) => + new Promise((resolve) => { + const handleSubmit = (event: Event) => { + event.preventDefault(); + formElement.removeEventListener('submit', handleSubmit); + resolve(true); + }; + + formElement.addEventListener('submit', handleSubmit); + }) + ); + regressionTest('renders', async ({ mount, page }) => { await mount(``); const dateInputElement = page.locator('ix-date-input'); @@ -312,9 +326,9 @@ regressionTest.describe('keyboard navigation', () => { await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); }); -regressionTest.describe( - 'date-input validation scenarios - required field behavior', - () => { + +regressionTest.describe('date-input validation scenarios', () => { + regressionTest.describe('required field behavior', () => { regressionTest( 'Required input: Invalid input > Removing value with keyboard > Stays invalid', async ({ page, mount }) => { @@ -494,12 +508,9 @@ regressionTest.describe( await expect(dateInput).not.toHaveClass(/ix-invalid--required/); } ); - } -); + }); -regressionTest.describe( - 'date-input validation scenarios - optional field behavior', - () => { + regressionTest.describe('optional field behavior', () => { regressionTest( 'Not required input: Invalid input > Removing value with keyboard > Valid', async ({ page, mount }) => { @@ -615,12 +626,9 @@ regressionTest.describe( await expectNoVisualValidation(dateInput, input); } ); - } -); + }); -regressionTest.describe( - 'date-input validation scenarios - novalidate form behavior', - () => { + regressionTest.describe('novalidate form behavior', () => { regressionTest( 'novalidate form suppresses validation for required field', async ({ page, mount }) => { @@ -671,15 +679,7 @@ regressionTest.describe( `); - const submitPromise = page.locator('#form').evaluate( - (form) => - new Promise((resolve) => { - form.addEventListener('submit', (e) => { - e.preventDefault(); - resolve(true); - }); - }) - ); + const submitPromise = waitForFormSubmit(page.locator('#form')); await page.locator('button[type="submit"]').click(); const submitted = await submitPromise; @@ -794,12 +794,9 @@ regressionTest.describe( ).toBeVisible(); } ); - } -); + }); -regressionTest.describe( - 'date-input validation scenarios - reportValidity behavior', - () => { + regressionTest.describe('reportValidity behavior', () => { regressionTest( 'reportValidity returns false and shows error for invalid date without prior interaction', async ({ page, mount }) => { @@ -921,5 +918,5 @@ regressionTest.describe( await expect(dateInput).not.toHaveClass(/ix-invalid/); } ); - } -); + }); +}); From aae5c05237d5a0f5d5b6d1e83af3ba4486ff4093 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 8 Jun 2026 16:46:59 +0530 Subject: [PATCH 11/39] fix(date-input): update validation handling for empty date input and add accessibility tests --- .../src/components/date-input/date-input.tsx | 6 ++- .../date-input/tests/date-input.ct.ts | 41 +++++++++++++++++++ .../core/src/components/input/input.util.ts | 2 + 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 8ffcda27cad..c0861801761 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -553,7 +553,7 @@ export class DateInput this.acceptSuppressedValidationValue(value); } - private handleValidatedInput(value: string): void { + private async handleValidatedInput(value: string): Promise { const validation = this.getDateValidation(value); this._hasInvalidInput = !validation.isValid; @@ -565,6 +565,8 @@ export class DateInput this.from = undefined; } else { this.invalidReason = undefined; + + await syncRequiredValidationClass(this.hostElement, this); this.updateFormInternalValue(value); this.closeDropdown(); focusInputIfKeyboardMode(this.inputElementRef.current); @@ -599,7 +601,7 @@ export class DateInput return; } - this.handleValidatedInput(value); + await this.handleValidatedInput(value); } onCalenderClick(event: Event) { diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index cb71b076c63..13a02bfbfbb 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -13,6 +13,47 @@ import { regressionTest, } from '@utils/test'; +regressionTest.describe('accessibility', () => { + regressionTest('default state', async ({ mount, makeAxeBuilder }) => { + await mount(``); + + const accessibilityScanResults = await makeAxeBuilder().analyze(); + expect(accessibilityScanResults.violations).toEqual([]); + }); + + regressionTest( + 'invalid parse error state', + async ({ mount, page, makeAxeBuilder }) => { + await mount(``); + + await expect(page.locator('ix-date-input')).toHaveClass(/\bhydrated\b/); + + await page + .locator('ix-date-input') + .evaluate((el: HTMLIxDateInputElement) => el.reportValidity()); + + const accessibilityScanResults = await makeAxeBuilder().analyze(); + expect(accessibilityScanResults.violations).toEqual([]); + } + ); + + regressionTest( + 'required missing error state', + async ({ mount, page, makeAxeBuilder }) => { + await mount(``); + + await expect(page.locator('ix-date-input')).toHaveClass(/\bhydrated\b/); + + await page + .locator('ix-date-input') + .evaluate((el: HTMLIxDateInputElement) => el.reportValidity()); + + const accessibilityScanResults = await makeAxeBuilder().analyze(); + expect(accessibilityScanResults.violations).toEqual([]); + } + ); +}); + const createDateInputAccessor = async (dateInput: Locator) => { const dateDropdown = dateInput.getByTestId('date-dropdown'); diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 5b25a953779..106d37dc5bf 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -57,6 +57,7 @@ export async function checkInternalValidity( ) { const skipValidation = await shouldSuppressInternalValidation(comp); if (skipValidation) { + comp.hostElement.classList.remove('ix-invalid--validity-invalid'); return; } @@ -330,6 +331,7 @@ export async function syncRequiredValidationClass( ): Promise { const skipValidation = await shouldSuppressInternalValidation(comp); if (skipValidation) { + hostElement.classList.remove('ix-invalid--required'); return; } From ad34c1aed1a17bda7d35af4ba40ff670179431de Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 8 Jun 2026 16:56:48 +0530 Subject: [PATCH 12/39] fix(date-input): format mount calls for improved readability in accessibility tests --- .../core/src/components/date-input/tests/date-input.ct.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 13a02bfbfbb..15784ec1547 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -15,7 +15,9 @@ import { regressionTest.describe('accessibility', () => { regressionTest('default state', async ({ mount, makeAxeBuilder }) => { - await mount(``); + await mount( + `` + ); const accessibilityScanResults = await makeAxeBuilder().analyze(); expect(accessibilityScanResults.violations).toEqual([]); @@ -24,7 +26,9 @@ regressionTest.describe('accessibility', () => { regressionTest( 'invalid parse error state', async ({ mount, page, makeAxeBuilder }) => { - await mount(``); + await mount( + `` + ); await expect(page.locator('ix-date-input')).toHaveClass(/\bhydrated\b/); From 3f5cbe02ec4ac65f3098e05685c7d5a84881eeab Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 8 Jun 2026 20:54:15 +0530 Subject: [PATCH 13/39] Refactor code structure for improved readability and maintainability --- packages/core/src/components.d.ts | 24 +-- .../src/components/date-input/date-input.tsx | 2 +- .../date-input/tests/date-input.ct.ts | 87 ++++---- .../react/src/components/components.server.ts | 198 +++++++++--------- 4 files changed, 157 insertions(+), 154 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 4b0278a9583..32adf69c082 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -4133,17 +4133,17 @@ export namespace Components { /** * Interval for hour selection. * @since 3.2.0 - * @default HOUR_INTERVAL_DEFAULT + * @default 1 */ "hourInterval": number; /** * Text of the time confirm button. - * @default CONFIRM_BUTTON_DEFAULT + * @default 'Confirm' */ "i18nConfirmTime": string; /** * Text for the top header. - * @default HEADER_DEFAULT + * @default 'Time' */ "i18nHeader": string; /** @@ -4174,7 +4174,7 @@ export namespace Components { /** * Interval for millisecond selection. * @since 3.2.0 - * @default MILLISECOND_INTERVAL_DEFAULT + * @default 100 */ "millisecondInterval": number; /** @@ -4185,13 +4185,13 @@ export namespace Components { /** * Interval for minute selection. * @since 3.2.0 - * @default MINUTE_INTERVAL_DEFAULT + * @default 1 */ "minuteInterval": number; /** * Interval for second selection. * @since 3.2.0 - * @default SECOND_INTERVAL_DEFAULT + * @default 1 */ "secondInterval": number; /** @@ -10705,17 +10705,17 @@ declare namespace LocalJSX { /** * Interval for hour selection. * @since 3.2.0 - * @default HOUR_INTERVAL_DEFAULT + * @default 1 */ "hourInterval"?: number; /** * Text of the time confirm button. - * @default CONFIRM_BUTTON_DEFAULT + * @default 'Confirm' */ "i18nConfirmTime"?: string; /** * Text for the top header. - * @default HEADER_DEFAULT + * @default 'Time' */ "i18nHeader"?: string; /** @@ -10746,7 +10746,7 @@ declare namespace LocalJSX { /** * Interval for millisecond selection. * @since 3.2.0 - * @default MILLISECOND_INTERVAL_DEFAULT + * @default 100 */ "millisecondInterval"?: number; /** @@ -10757,7 +10757,7 @@ declare namespace LocalJSX { /** * Interval for minute selection. * @since 3.2.0 - * @default MINUTE_INTERVAL_DEFAULT + * @default 1 */ "minuteInterval"?: number; /** @@ -10771,7 +10771,7 @@ declare namespace LocalJSX { /** * Interval for second selection. * @since 3.2.0 - * @default SECOND_INTERVAL_DEFAULT + * @default 1 */ "secondInterval"?: number; /** diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index c0861801761..80d3b8c8f97 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -26,7 +26,6 @@ import { import { DateTime } from 'luxon'; import { SlotEnd, SlotStart } from '../input/input.fc'; import { - clearInputValue, DisposableChangesAndVisibilityObservers, PickerValidityStateTracker, addDisposableChangesAndVisibilityObservers, @@ -35,6 +34,7 @@ import { emitPickerValidityState, handleSubmitOnEnterKeydown, onInputBlurWithChange, + clearInputValue, syncRequiredValidationClass, } from '../input/input.util'; import { diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 15784ec1547..594923ac92f 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -13,6 +13,51 @@ import { regressionTest, } from '@utils/test'; +const createDateInputAccessor = async (dateInput: Locator) => { + const dateDropdown = dateInput.getByTestId('date-dropdown'); + + const handle = { + openByCalender: async () => { + const trigger = dateInput.getByTestId('open-calendar'); + await trigger.click(); + await expect(dateDropdown).toHaveClass(/show/); + }, + selectDay: async (day: number) => { + const dayButton = dateInput + .locator('ix-dropdown .calendar-item') + .filter({ hasText: new RegExp(`^${day}$`) }) + .first(); + + await expect(dayButton).toBeVisible(); + await dayButton.click(); + }, + }; + + return handle; +}; + +const expectNoVisualValidation = async (dateInput: Locator, input: Locator) => { + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); +}; + +const waitForFormSubmit = (form: Locator) => { + const submitPromise = form.evaluate( + (formElement) => + new Promise((resolve) => { + const handleSubmit = (event: Event) => { + event.preventDefault(); + formElement.removeEventListener('submit', handleSubmit); + resolve(true); + }; + + formElement.addEventListener('submit', handleSubmit); + }) + ); + + return submitPromise; +}; + regressionTest.describe('accessibility', () => { regressionTest('default state', async ({ mount, makeAxeBuilder }) => { await mount( @@ -58,48 +103,6 @@ regressionTest.describe('accessibility', () => { ); }); -const createDateInputAccessor = async (dateInput: Locator) => { - const dateDropdown = dateInput.getByTestId('date-dropdown'); - - const handle = { - openByCalender: async () => { - const trigger = dateInput.getByTestId('open-calendar'); - await trigger.click(); - await expect(dateDropdown).toHaveClass(/show/); - }, - selectDay: async (day: number) => { - const dayButton = dateInput - .locator('ix-dropdown .calendar-item') - .filter({ hasText: new RegExp(`^${day}$`) }) - .first(); - - await expect(dayButton).toBeVisible(); - await dayButton.click(); - }, - }; - - return handle; -}; - -const expectNoVisualValidation = async (dateInput: Locator, input: Locator) => { - await expect(input).not.toHaveClass(/is-invalid/); - await expect(dateInput).not.toHaveClass(/ix-invalid--required/); -}; - -const waitForFormSubmit = (form: Locator) => - form.evaluate( - (formElement) => - new Promise((resolve) => { - const handleSubmit = (event: Event) => { - event.preventDefault(); - formElement.removeEventListener('submit', handleSubmit); - resolve(true); - }; - - formElement.addEventListener('submit', handleSubmit); - }) - ); - regressionTest('renders', async ({ mount, page }) => { await mount(``); const dateInputElement = page.locator('ix-date-input'); diff --git a/packages/react/src/components/components.server.ts b/packages/react/src/components/components.server.ts index 1b72ef756e5..cf585ab859b 100644 --- a/packages/react/src/components/components.server.ts +++ b/packages/react/src/components/components.server.ts @@ -130,7 +130,7 @@ export const IxActionCard: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxActionCard as StencilReactComponent, serializeShadowRoot }); @@ -144,7 +144,7 @@ export const IxApplication: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxApplication as StencilReactComponent, serializeShadowRoot }); @@ -170,7 +170,7 @@ export const IxApplicationHeader: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxApplicationHeader as StencilReactComponent, serializeShadowRoot }); @@ -187,7 +187,7 @@ export const IxAvatar: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxAvatar as StencilReactComponent, serializeShadowRoot }); @@ -203,7 +203,7 @@ export const IxBlind: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxBlind as StencilReactComponent, serializeShadowRoot }); @@ -221,7 +221,7 @@ export const IxBreadcrumb: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxBreadcrumb as StencilReactComponent, serializeShadowRoot }); @@ -243,7 +243,7 @@ export const IxBreadcrumbItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxBreadcrumbItem as StencilReactComponent, serializeShadowRoot }); @@ -266,7 +266,7 @@ export const IxButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxButton as StencilReactComponent, serializeShadowRoot }); @@ -280,7 +280,7 @@ export const IxCard: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCard as StencilReactComponent, serializeShadowRoot }); @@ -294,7 +294,7 @@ export const IxCardAccordion: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCardAccordion as StencilReactComponent, serializeShadowRoot }); @@ -304,7 +304,7 @@ export type IxCardContentEvents = NonNullable; export const IxCardContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-card-content', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCardContent as StencilReactComponent, serializeShadowRoot }); @@ -330,7 +330,7 @@ export const IxCardList: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCardList as StencilReactComponent, serializeShadowRoot }); @@ -340,7 +340,7 @@ export type IxCardTitleEvents = NonNullable; export const IxCardTitle: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-card-title', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCardTitle as StencilReactComponent, serializeShadowRoot }); @@ -369,7 +369,7 @@ export const IxCategoryFilter: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCategoryFilter as StencilReactComponent, serializeShadowRoot }); @@ -391,7 +391,7 @@ export const IxCheckbox: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCheckbox as StencilReactComponent, serializeShadowRoot }); @@ -411,7 +411,7 @@ export const IxCheckboxGroup: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCheckboxGroup as StencilReactComponent, serializeShadowRoot }); @@ -433,7 +433,7 @@ export const IxChip: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxChip as StencilReactComponent, serializeShadowRoot }); @@ -448,7 +448,7 @@ export const IxCol: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCol as StencilReactComponent, serializeShadowRoot }); @@ -458,7 +458,7 @@ export type IxContentEvents = NonNullable; export const IxContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-content', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxContent as StencilReactComponent, serializeShadowRoot }); @@ -473,7 +473,7 @@ export const IxContentHeader: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxContentHeader as StencilReactComponent, serializeShadowRoot }); @@ -492,7 +492,7 @@ export const IxCustomField: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxCustomField as StencilReactComponent, serializeShadowRoot }); @@ -520,7 +520,7 @@ export const IxDateDropdown: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDateDropdown as StencilReactComponent, serializeShadowRoot }); @@ -562,7 +562,7 @@ export const IxDateInput: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDateInput as StencilReactComponent, serializeShadowRoot }); @@ -595,7 +595,7 @@ export const IxDatePicker: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDatePicker as StencilReactComponent, serializeShadowRoot }); @@ -642,7 +642,7 @@ export const IxDatetimeInput: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDatetimeInput as StencilReactComponent, serializeShadowRoot }); @@ -677,7 +677,7 @@ export const IxDatetimePicker: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDatetimePicker as StencilReactComponent, serializeShadowRoot }); @@ -687,7 +687,7 @@ export type IxDividerEvents = NonNullable; export const IxDivider: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-divider', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDivider as StencilReactComponent, serializeShadowRoot }); @@ -718,7 +718,7 @@ export const IxDropdown: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDropdown as StencilReactComponent, serializeShadowRoot }); @@ -742,7 +742,7 @@ export const IxDropdownButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDropdownButton as StencilReactComponent, serializeShadowRoot }); @@ -752,7 +752,7 @@ export type IxDropdownHeaderEvents = NonNullable; export const IxDropdownHeader: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-dropdown-header', properties: { label: 'label' }, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDropdownHeader as StencilReactComponent, serializeShadowRoot }); @@ -776,7 +776,7 @@ export const IxDropdownItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDropdownItem as StencilReactComponent, serializeShadowRoot }); @@ -786,7 +786,7 @@ export type IxDropdownQuickActionsEvents = NonNullable; export const IxDropdownQuickActions: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-dropdown-quick-actions', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxDropdownQuickActions as StencilReactComponent, serializeShadowRoot }); @@ -803,7 +803,7 @@ export const IxEmptyState: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxEmptyState as StencilReactComponent, serializeShadowRoot }); @@ -818,7 +818,7 @@ export const IxEventList: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxEventList as StencilReactComponent, serializeShadowRoot }); @@ -834,7 +834,7 @@ export const IxEventListItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxEventListItem as StencilReactComponent, serializeShadowRoot }); @@ -853,7 +853,7 @@ export const IxExpandingSearch: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxExpandingSearch as StencilReactComponent, serializeShadowRoot }); @@ -867,7 +867,7 @@ export const IxFieldLabel: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxFieldLabel as StencilReactComponent, serializeShadowRoot }); @@ -881,7 +881,7 @@ export const IxFilterChip: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxFilterChip as StencilReactComponent, serializeShadowRoot }); @@ -897,7 +897,7 @@ export const IxFlipTile: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxFlipTile as StencilReactComponent, serializeShadowRoot }); @@ -907,7 +907,7 @@ export type IxFlipTileContentEvents = NonNullable; export const IxFlipTileContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-flip-tile-content', properties: { contentVisible: 'content-visible' }, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxFlipTileContent as StencilReactComponent, serializeShadowRoot }); @@ -929,7 +929,7 @@ export const IxGroup: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxGroup as StencilReactComponent, serializeShadowRoot }); @@ -939,7 +939,7 @@ export type IxGroupContextMenuEvents = NonNullable; export const IxGroupContextMenu: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-group-context-menu', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxGroupContextMenu as StencilReactComponent, serializeShadowRoot }); @@ -959,7 +959,7 @@ export const IxGroupItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxGroupItem as StencilReactComponent, serializeShadowRoot }); @@ -976,7 +976,7 @@ export const IxHelperText: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxHelperText as StencilReactComponent, serializeShadowRoot }); @@ -995,7 +995,7 @@ export const IxIconButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxIconButton as StencilReactComponent, serializeShadowRoot }); @@ -1015,7 +1015,7 @@ export const IxIconToggleButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxIconToggleButton as StencilReactComponent, serializeShadowRoot }); @@ -1051,7 +1051,7 @@ export const IxInput: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxInput as StencilReactComponent, serializeShadowRoot }); @@ -1067,7 +1067,7 @@ export const IxKeyValue: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxKeyValue as StencilReactComponent, serializeShadowRoot }); @@ -1077,7 +1077,7 @@ export type IxKeyValueListEvents = NonNullable; export const IxKeyValueList: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-key-value-list', properties: { striped: 'striped' }, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxKeyValueList as StencilReactComponent, serializeShadowRoot }); @@ -1095,7 +1095,7 @@ export const IxKpi: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxKpi as StencilReactComponent, serializeShadowRoot }); @@ -1105,7 +1105,7 @@ export type IxLayoutAutoEvents = NonNullable; export const IxLayoutAuto: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-layout-auto', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxLayoutAuto as StencilReactComponent, serializeShadowRoot }); @@ -1119,7 +1119,7 @@ export const IxLayoutGrid: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxLayoutGrid as StencilReactComponent, serializeShadowRoot }); @@ -1133,7 +1133,7 @@ export const IxLinkButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxLinkButton as StencilReactComponent, serializeShadowRoot }); @@ -1163,7 +1163,7 @@ export const IxMenu: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenu as StencilReactComponent, serializeShadowRoot }); @@ -1182,7 +1182,7 @@ export const IxMenuAbout: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuAbout as StencilReactComponent, serializeShadowRoot }); @@ -1195,7 +1195,7 @@ export const IxMenuAboutItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuAboutItem as StencilReactComponent, serializeShadowRoot }); @@ -1215,7 +1215,7 @@ export const IxMenuAboutNews: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuAboutNews as StencilReactComponent, serializeShadowRoot }); @@ -1235,7 +1235,7 @@ export const IxMenuAvatar: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuAvatar as StencilReactComponent, serializeShadowRoot }); @@ -1248,7 +1248,7 @@ export const IxMenuAvatarItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuAvatarItem as StencilReactComponent, serializeShadowRoot }); @@ -1263,7 +1263,7 @@ export const IxMenuCategory: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuCategory as StencilReactComponent, serializeShadowRoot }); @@ -1286,7 +1286,7 @@ export const IxMenuItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuItem as StencilReactComponent, serializeShadowRoot }); @@ -1305,7 +1305,7 @@ export const IxMenuSettings: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuSettings as StencilReactComponent, serializeShadowRoot }); @@ -1318,7 +1318,7 @@ export const IxMenuSettingsItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMenuSettingsItem as StencilReactComponent, serializeShadowRoot }); @@ -1334,7 +1334,7 @@ export const IxMessageBar: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxMessageBar as StencilReactComponent, serializeShadowRoot }); @@ -1354,7 +1354,7 @@ export const IxModal: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxModal as StencilReactComponent, serializeShadowRoot }); @@ -1364,7 +1364,7 @@ export type IxModalContentEvents = NonNullable; export const IxModalContent: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-modal-content', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxModalContent as StencilReactComponent, serializeShadowRoot }); @@ -1374,7 +1374,7 @@ export type IxModalFooterEvents = NonNullable; export const IxModalFooter: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-modal-footer', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxModalFooter as StencilReactComponent, serializeShadowRoot }); @@ -1390,7 +1390,7 @@ export const IxModalHeader: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxModalHeader as StencilReactComponent, serializeShadowRoot }); @@ -1428,7 +1428,7 @@ export const IxNumberInput: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxNumberInput as StencilReactComponent, serializeShadowRoot }); @@ -1453,7 +1453,7 @@ export const IxPagination: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxPagination as StencilReactComponent, serializeShadowRoot }); @@ -1481,7 +1481,7 @@ export const IxPane: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxPane as StencilReactComponent, serializeShadowRoot }); @@ -1495,7 +1495,7 @@ export const IxPaneLayout: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxPaneLayout as StencilReactComponent, serializeShadowRoot }); @@ -1514,7 +1514,7 @@ export const IxPill: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxPill as StencilReactComponent, serializeShadowRoot }); @@ -1535,7 +1535,7 @@ export const IxProgressIndicator: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxProgressIndicator as StencilReactComponent, serializeShadowRoot }); @@ -1554,7 +1554,7 @@ export const IxPushCard: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxPushCard as StencilReactComponent, serializeShadowRoot }); @@ -1575,7 +1575,7 @@ export const IxRadio: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxRadio as StencilReactComponent, serializeShadowRoot }); @@ -1596,7 +1596,7 @@ export const IxRadioGroup: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxRadioGroup as StencilReactComponent, serializeShadowRoot }); @@ -1609,7 +1609,7 @@ export const IxRangeField: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxRangeField as StencilReactComponent, serializeShadowRoot }); @@ -1619,7 +1619,7 @@ export type IxRowEvents = NonNullable; export const IxRow: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-row', properties: {}, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxRow as StencilReactComponent, serializeShadowRoot }); @@ -1663,7 +1663,7 @@ export const IxSelect: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxSelect as StencilReactComponent, serializeShadowRoot }); @@ -1681,7 +1681,7 @@ export const IxSelectItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxSelectItem as StencilReactComponent, serializeShadowRoot }); @@ -1706,7 +1706,7 @@ export const IxSlider: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxSlider as StencilReactComponent, serializeShadowRoot }); @@ -1720,7 +1720,7 @@ export const IxSpinner: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxSpinner as StencilReactComponent, serializeShadowRoot }); @@ -1742,7 +1742,7 @@ export const IxSplitButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxSplitButton as StencilReactComponent, serializeShadowRoot }); @@ -1769,7 +1769,7 @@ export const IxTabItem: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTabItem as StencilReactComponent, serializeShadowRoot }); @@ -1790,7 +1790,7 @@ export const IxTabs: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTabs as StencilReactComponent, serializeShadowRoot }); @@ -1826,7 +1826,7 @@ export const IxTextarea: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTextarea as StencilReactComponent, serializeShadowRoot }); @@ -1836,7 +1836,7 @@ export type IxTileEvents = NonNullable; export const IxTile: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-tile', properties: { size: 'size' }, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTile as StencilReactComponent, serializeShadowRoot }); @@ -1883,7 +1883,7 @@ export const IxTimeInput: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTimeInput as StencilReactComponent, serializeShadowRoot }); @@ -1915,7 +1915,7 @@ export const IxTimePicker: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTimePicker as StencilReactComponent, serializeShadowRoot }); @@ -1934,7 +1934,7 @@ export const IxToast: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxToast as StencilReactComponent, serializeShadowRoot }); @@ -1944,7 +1944,7 @@ export type IxToastContainerEvents = NonNullable; export const IxToastContainer: StencilReactComponent = /*@__PURE__*/ createComponent({ tagName: 'ix-toast-container', properties: { position: 'position' }, - hydrateModule: import('@siemens/ix/hydrate') as Promise, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxToastContainer as StencilReactComponent, serializeShadowRoot }); @@ -1968,7 +1968,7 @@ export const IxToggle: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxToggle as StencilReactComponent, serializeShadowRoot }); @@ -1985,7 +1985,7 @@ export const IxToggleButton: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxToggleButton as StencilReactComponent, serializeShadowRoot }); @@ -2003,7 +2003,7 @@ export const IxTooltip: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTooltip as StencilReactComponent, serializeShadowRoot }); @@ -2018,7 +2018,7 @@ export const IxTypography: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxTypography as StencilReactComponent, serializeShadowRoot }); @@ -2041,7 +2041,7 @@ export const IxUpload: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxUpload as StencilReactComponent, serializeShadowRoot }); @@ -2058,7 +2058,7 @@ export const IxWorkflowStep: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxWorkflowStep as StencilReactComponent, serializeShadowRoot }); @@ -2072,7 +2072,7 @@ export const IxWorkflowSteps: StencilReactComponent, + hydrateModule: typeof window === 'undefined' ? (import('@siemens/ix/hydrate') as Promise) : undefined, clientModule: clientComponents.IxWorkflowSteps as StencilReactComponent, serializeShadowRoot }); From 322a6a04a5d419d84152ae519f01cddf8173e348 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 9 Jun 2026 17:44:10 +0530 Subject: [PATCH 14/39] fix(date-input): update error messages for empty date field and improve validation handling --- .../fix-input-validation-clear-novalidate.md | 2 +- packages/core/src/components.d.ts | 8 +++---- .../src/components/date-input/date-input.tsx | 22 +++---------------- .../date-input/tests/date-input.ct.ts | 2 +- .../core/src/components/input/input.util.ts | 1 - 5 files changed, 9 insertions(+), 26 deletions(-) diff --git a/.changeset/fix-input-validation-clear-novalidate.md b/.changeset/fix-input-validation-clear-novalidate.md index 7034224810a..c4b86dd9bb5 100644 --- a/.changeset/fix-input-validation-clear-novalidate.md +++ b/.changeset/fix-input-validation-clear-novalidate.md @@ -6,7 +6,7 @@ Added `clear()` method to `ix-date-input` to reset the value and all validation Added `reportValidity()` method to `ix-date-input` to programmatically trigger validation and show visual error state immediately — equivalent to calling `reportValidity()` on a native `` element. -Added `i18nErrorRequired` prop (`i18n-error-required`, default `"This field is required"`) to `ix-date-input`. When a required field is emptied after `reportValidity()` has surfaced an error, the error text now switches from "Date is not valid" to the required-missing message instead of disappearing — keeping both the red border and the text description visible. +Added `i18nErrorRequired` prop (`i18n-error-required`, default `"Date is required"`) to `ix-date-input`. When a required field is emptied after `reportValidity()` has surfaced an error, the error text now switches from "Date is not valid" to the required-missing message instead of disappearing — keeping both the red border and the text description visible. Fixed validation behavior for `ix-date-input`: diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 32adf69c082..5df51dc2566 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1012,9 +1012,9 @@ export namespace Components { */ "i18nErrorDateUnparsable": string; /** - * I18n string for the error message when a required field is empty. + * I18n string for the error message when the date field is empty. * @since 5.1.0 - * @default 'This field is required' + * @default 'Date is required' */ "i18nErrorRequired": string; /** @@ -7372,9 +7372,9 @@ declare namespace LocalJSX { */ "i18nErrorDateUnparsable"?: string; /** - * I18n string for the error message when a required field is empty. + * I18n string for the error message when the date field is empty. * @since 5.1.0 - * @default 'This field is required' + * @default 'Date is required' */ "i18nErrorRequired"?: string; /** diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 80d3b8c8f97..4e22e00df37 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -203,12 +203,12 @@ export class DateInput 'Date is not valid'; /** - * I18n string for the error message when a required field is empty. + * I18n string for the error message when the date field is empty. * * @since 5.1.0 */ @Prop({ attribute: 'i18n-error-required' }) i18nErrorRequired = - 'This field is required'; + 'Date is required'; /** * Shows week numbers displayed on the left side of the date picker. @@ -292,11 +292,6 @@ export class DateInput private classObserver?: ClassMutationObserver; - /** - * Tracks actual parse/format invalidity regardless of touched state. - * Used for internal validity queries (getValidityState, reportValidity). - * Visual feedback is gated on `touched`. - */ private _hasInvalidInput = false; /** @@ -307,13 +302,6 @@ export class DateInput */ private _blurHandledValidation = false; - /** - * Set to `true` only by an explicit `reportValidity()` call — NOT by blur. - * Used as the gate in novalidate forms to keep showing errors after an - * explicit validation request until the value is actually corrected. - * Regular blur in a novalidate form must never set this flag so that normal - * novalidate suppression is preserved. - */ private _reportValidityCalled = false; public initialValue?: string; @@ -392,10 +380,6 @@ export class DateInput @Listen('invalid') async onInvalid(event: Event) { - // Prevent the browser's native validation tooltip — the component provides - // its own styled error message via ix-field-wrapper. Calling preventDefault() - // suppresses the bubble but does not affect the validity result returned by - // form.reportValidity(). event.preventDefault(); reportFieldValidity(this, this._hasInvalidInput); } @@ -810,7 +794,7 @@ export class DateInput override render() { // Error text priority: // 1. Parse error — "Date is not valid" (or i18n override / custom invalidText) - // 2. Required empty — "This field is required" (or custom invalidText) + // 2. Required empty — "Date is required" (or custom invalidText) // 3. Consumer-supplied invalidText only const isRequiredEmpty = !!this.required && !this.value && this.touched; diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 594923ac92f..7ba5f17ffac 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -838,7 +838,7 @@ regressionTest.describe('date-input validation scenarios', () => { dateInput .locator('ix-field-wrapper') .locator('ix-typography') - .filter({ hasText: 'This field is required' }) + .filter({ hasText: 'Date is required' }) ).toBeVisible(); } ); diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 106d37dc5bf..4a871fc3e93 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -387,7 +387,6 @@ export async function clearInputValue( options?.additionalCleanup?.(); comp.updateFormInternalValue?.(emptyValue); - comp.value = emptyValue; if (options?.emitValueChange) { comp.valueChange?.emit(emptyValue); From bb12d7fc368dafd31634f3054ee73e4c69fd2530 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 9 Jun 2026 20:01:25 +0530 Subject: [PATCH 15/39] fix(date-input): improve validation for empty date input and enhance focus handling --- .../src/components/date-input/date-input.tsx | 100 ++++++++++-------- .../utils/input/picker-input.util.ts | 16 +-- 2 files changed, 65 insertions(+), 51 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 4e22e00df37..cb3baf7a05e 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -64,7 +64,7 @@ import { InputPickerMixin, InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; -import { forceTabIndex } from '../utils/a11y'; + /** * @form-ready @@ -421,14 +421,14 @@ export class DateInput invalidReason?: string; } { const date = DateTime.fromFormat(value, this.format); - const minDate = DateTime.fromFormat(this.minDate, this.format); - const maxDate = DateTime.fromFormat(this.maxDate, this.format); + const minDate = this.minDate ? DateTime.fromFormat(this.minDate, this.format) : null; + const maxDate = this.maxDate ? DateTime.fromFormat(this.maxDate, this.format) : null; return { isValid: date.isValid && - (!minDate.isValid || date >= minDate) && - (!maxDate.isValid || date <= maxDate), + (!minDate || !minDate.isValid || date >= minDate) && + (!maxDate || !maxDate.isValid || date <= maxDate), invalidReason: date.invalidReason ?? undefined, }; } @@ -653,19 +653,25 @@ export class DateInput this.ixFocus.emit(); }} onBlur={(e: FocusEvent) => - handlePickerInputBlur(e, this.show, this.hostElement, () => { - this.touched = true; - this.isInputInvalid = this._hasInvalidInput; - onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); - emitPickerValidityState(this); - // Signal to onFocusout (which fires right after) that validation - // has already been committed so it should not repeat it. - this._blurHandledValidation = true; - }) + handlePickerInputBlur( + e, + this.show, + this.hostElement, + () => { + this.touched = true; + this.isInputInvalid = this._hasInvalidInput; + onInputBlurWithChange( + this, + this.inputElementRef.current, + this.value + ); + emitPickerValidityState(this); + // Signal to onFocusout (which fires right after) that validation + // has already been committed so it should not repeat it. + this._blurHandledValidation = true; + }, + this.dropdownElementRef?.current + ) } onKeyDown={(event) => this.handleInputKeyDown(event)} style={{ @@ -678,7 +684,6 @@ export class DateInput > forceTabIndex(ref, -1)} aria-label={this.ariaLabelCalendarButton} data-testid="open-calendar" class={{ 'calendar-hidden': this.disabled || this.readonly }} @@ -814,34 +819,39 @@ export class DateInput readonly: this.readonly, }} onFocusout={(e: FocusEvent) => - handlePickerHostFocusout(e, this.hostElement, (hasRelatedTarget) => { - // Only close the dropdown when focus went to a known external - // element. When relatedTarget is null (focus to or a - // programmatic blur from tests) the dropdown's closeBehavior - // handles it, and closing here risks a flicker during rerenders. - if (hasRelatedTarget) { - this.closeDropdown(); - } + handlePickerHostFocusout( + e, + this.hostElement, + (hasRelatedTarget) => { + // Only close the dropdown when focus went to a known external + // element. When relatedTarget is null (focus to or a + // programmatic blur from tests) the dropdown's closeBehavior + // handles it, and closing here risks a flicker during rerenders. + if (hasRelatedTarget) { + this.closeDropdown(); + } - // When the input's onBlur was suppressed (picker was open, - // focus moved to an internal element) we must now run the full - // validation — the user has truly left the component. - // When onBlur already ran validation (direct tab-away), skip it - // here to avoid double-emitting ixBlur / ixChange. - if (this._blurHandledValidation) { - this._blurHandledValidation = false; - return; - } + // When the input's onBlur was suppressed (picker was open, + // focus moved to an internal element) we must now run the full + // validation — the user has truly left the component. + // When onBlur already ran validation (direct tab-away), skip it + // here to avoid double-emitting ixBlur / ixChange. + if (this._blurHandledValidation) { + this._blurHandledValidation = false; + return; + } - this.touched = true; - this.isInputInvalid = this._hasInvalidInput; - onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); - emitPickerValidityState(this); - }) + this.touched = true; + this.isInputInvalid = this._hasInvalidInput; + onInputBlurWithChange( + this, + this.inputElementRef.current, + this.value + ); + emitPickerValidityState(this); + }, + this.dropdownElementRef?.current + ) } > void + onBlur: () => void, + pickerElement?: HTMLElement | null ): void { const relatedTarget = e.relatedTarget as Node | null; - if (show && isInternalFocusTarget(hostElement, relatedTarget)) { + if (show && isInternalFocusTarget(hostElement, relatedTarget, pickerElement)) { return; } onBlur(); @@ -129,10 +132,11 @@ export function handlePickerInputBlur( export function handlePickerHostFocusout( e: FocusEvent, hostElement: HTMLElement, - onExternalFocusout: (hasRelatedTarget: boolean) => void + onExternalFocusout: (hasRelatedTarget: boolean) => void, + pickerElement?: HTMLElement | null ): void { const relatedTarget = e.relatedTarget as Node | null; - if (isInternalFocusTarget(hostElement, relatedTarget)) { + if (isInternalFocusTarget(hostElement, relatedTarget, pickerElement)) { return; } onExternalFocusout(relatedTarget !== null); From c84833f0a262bf336f3ab583662c6e2b4b91840d Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 9 Jun 2026 21:15:21 +0530 Subject: [PATCH 16/39] fix(date-input): enhance validation logic for empty date input and add keyboard navigation tests --- .../src/components/date-input/date-input.tsx | 13 ++++++----- .../date-input/tests/date-input.ct.ts | 22 +++++++++++++++++++ .../utils/input/picker-input.util.ts | 17 +++++++++++++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index cb3baf7a05e..66918e58b4b 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -65,7 +65,6 @@ import { InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; - /** * @form-ready * @@ -421,14 +420,18 @@ export class DateInput invalidReason?: string; } { const date = DateTime.fromFormat(value, this.format); - const minDate = this.minDate ? DateTime.fromFormat(this.minDate, this.format) : null; - const maxDate = this.maxDate ? DateTime.fromFormat(this.maxDate, this.format) : null; + const minDate = this.minDate + ? DateTime.fromFormat(this.minDate, this.format) + : null; + const maxDate = this.maxDate + ? DateTime.fromFormat(this.maxDate, this.format) + : null; return { isValid: date.isValid && - (!minDate || !minDate.isValid || date >= minDate) && - (!maxDate || !maxDate.isValid || date <= maxDate), + (!minDate?.isValid || date >= minDate) && + (!maxDate?.isValid || date <= maxDate), invalidReason: date.invalidReason ?? undefined, }; } diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 7ba5f17ffac..b91f6fb9ecd 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -373,6 +373,28 @@ regressionTest.describe('keyboard navigation', () => { await page.keyboard.press('Enter'); await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); + + regressionTest( + 'Keyboard navigation (PageUp/PageDown) should not trigger validation', + async ({ mount, page }) => { + await mount(``); + + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await input.focus(); + await page.keyboard.press('ArrowDown'); + + await page.keyboard.press('PageDown'); + + await page.keyboard.press('PageUp'); + + await page.keyboard.press('Escape'); + + await expectNoVisualValidation(dateInput, input); + await expect(input).toHaveValue(''); + } + ); }); regressionTest.describe('date-input validation scenarios', () => { diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 595f0b37a2d..3ddd8f410bc 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -123,7 +123,10 @@ export function handlePickerInputBlur( pickerElement?: HTMLElement | null ): void { const relatedTarget = e.relatedTarget as Node | null; - if (show && isInternalFocusTarget(hostElement, relatedTarget, pickerElement)) { + if ( + show && + isInternalFocusTarget(hostElement, relatedTarget, pickerElement) + ) { return; } onBlur(); @@ -136,9 +139,21 @@ export function handlePickerHostFocusout( pickerElement?: HTMLElement | null ): void { const relatedTarget = e.relatedTarget as Node | null; + if (isInternalFocusTarget(hostElement, relatedTarget, pickerElement)) { return; } + + const isPickerNavigating = + relatedTarget === null && + pickerElement && + 'show' in pickerElement && + pickerElement.show; + + if (isPickerNavigating) { + return; + } + onExternalFocusout(relatedTarget !== null); } From 800d4892e00137cbae33022bc1dd4f40d1bf5eb8 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 10 Jun 2026 10:14:00 +0530 Subject: [PATCH 17/39] fix(date-input): ensure form validity sync on required field change --- packages/core/src/components/date-input/date-input.tsx | 1 + packages/core/src/components/utils/input/picker-input.util.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 66918e58b4b..f793971b689 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -140,6 +140,7 @@ export class DateInput @Watch('required') async onRequiredChange() { await syncRequiredValidationClass(this.hostElement, this); + this.syncFormInternalsValidity(); } /** diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 3ddd8f410bc..6a1437ea361 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -148,7 +148,7 @@ export function handlePickerHostFocusout( relatedTarget === null && pickerElement && 'show' in pickerElement && - pickerElement.show; + (pickerElement as { show: boolean }).show; if (isPickerNavigating) { return; From d559e4e8d096cc2bc8067c0f6088e202bd5b160f Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 10 Jun 2026 13:13:14 +0530 Subject: [PATCH 18/39] fix(date-input): add keyboard navigation test for opening date picker on focus --- .../src/components/date-input/date-input.tsx | 4 ++++ .../components/date-input/tests/date-input.ct.ts | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index f793971b689..c9fb105bd20 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -64,6 +64,7 @@ import { InputPickerMixin, InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; +import { hasKeyboardMode } from '../utils/internal/mixins/setup.mixin'; /** * @form-ready @@ -655,6 +656,9 @@ export class DateInput }} onFocus={async () => { this.ixFocus.emit(); + if (hasKeyboardMode()) { + this.openDropdown(); + } }} onBlur={(e: FocusEvent) => handlePickerInputBlur( diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index b91f6fb9ecd..cc35f287c17 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -374,6 +374,21 @@ regressionTest.describe('keyboard navigation', () => { await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); + regressionTest( + 'keyboard focus opens picker (keyboard navigation)', + async ({ page, mount }) => { + await mount(``); + + const dateInputElement = page.locator('ix-date-input'); + const input = dateInputElement.locator('input'); + const dateDropdown = dateInputElement.getByTestId('date-dropdown'); + + await page.keyboard.press('Tab'); + await expect(input).toBeFocused(); + await expect(dateDropdown).toHaveClass(/show/); + } + ); + regressionTest( 'Keyboard navigation (PageUp/PageDown) should not trigger validation', async ({ mount, page }) => { @@ -934,7 +949,6 @@ regressionTest.describe('date-input validation scenarios', () => { const dateInput = page.locator('ix-date-input'); const input = dateInput.getByRole('textbox'); - // No interaction — clean await expect(input).not.toHaveClass(/is-invalid/); await page.evaluate(() => { From 001a7706de2a09d1bfbd5b7b355ff72ea0fdb0ff Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 10 Jun 2026 20:53:06 +0530 Subject: [PATCH 19/39] fix(date-input): enhance validation logic and add clear/reportValidity methods --- .changeset/feat-date-input-validation.md | 24 ++ .../fix-input-validation-clear-novalidate.md | 17 -- packages/core/src/components.d.ts | 4 +- .../src/components/date-input/date-input.tsx | 210 +++++++----------- .../core/src/components/input/input.util.ts | 1 + .../utils/input/picker-input.util.ts | 52 ++--- 6 files changed, 128 insertions(+), 180 deletions(-) create mode 100644 .changeset/feat-date-input-validation.md delete mode 100644 .changeset/fix-input-validation-clear-novalidate.md diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md new file mode 100644 index 00000000000..ec61d6072a6 --- /dev/null +++ b/.changeset/feat-date-input-validation.md @@ -0,0 +1,24 @@ +--- +'@siemens/ix': minor +--- + +Added `clear()` method to reset the value and all validation state to its initial state, removing visual error indicators even after the field has been touched. + +Added `reportValidity()` method to programmatically trigger validation and show visual error state immediately. + +Added `i18nErrorRequired` prop (`i18n-error-required`) to show required-missing error message when a required field is emptied after validation has been triggered. + +Validation behavior: Values are always validated internally regardless of source. Visual validation errors only appear after the field has been touched (after first blur). + +Fixed validation behavior for `ix-date-input`: + +- Non-required field now correctly validates as valid when the value is removed (both keyboard deletion and programmatic empty string) +- `novalidate` forms now fully suppress visual validation feedback while maintaining internal date parsing + +Additionally covered: + +- Dynamically toggling the `required` attribute now immediately reflects correct validation state +- Removed momentary red-border flash when clicking a calendar day +- Picker automatically opens when focusing the date-input via keyboard navigation (Tab key) +- Keyboard navigation within the calendar (e.g., `PageUp`/`PageDown` for months) does not trigger validation errors + diff --git a/.changeset/fix-input-validation-clear-novalidate.md b/.changeset/fix-input-validation-clear-novalidate.md deleted file mode 100644 index c4b86dd9bb5..00000000000 --- a/.changeset/fix-input-validation-clear-novalidate.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@siemens/ix': minor ---- - -Added `clear()` method to `ix-date-input` to reset the value and all validation state to its initial pristine condition. Unlike setting the `value` property directly, `clear()` removes visible error indicators even after the field has been touched. - -Added `reportValidity()` method to `ix-date-input` to programmatically trigger validation and show visual error state immediately — equivalent to calling `reportValidity()` on a native `` element. - -Added `i18nErrorRequired` prop (`i18n-error-required`, default `"Date is required"`) to `ix-date-input`. When a required field is emptied after `reportValidity()` has surfaced an error, the error text now switches from "Date is not valid" to the required-missing message instead of disappearing — keeping both the red border and the text description visible. - -Fixed validation behavior for `ix-date-input`: - -- Emptying a required field (via keyboard or programmatically) now correctly shows `ix-invalid--required` and a required-missing error message after the field has been touched -- After `reportValidity()` surfaces an error, setting a valid value programmatically now correctly clears all error state (red border and message) — previously the field stayed red despite holding a valid date -- Clicking a calendar day while the calendar is open no longer causes a momentary red-border flash on the input -- `novalidate` forms now fully suppress visual validation feedback (red border and error messages); date parsing continues internally to keep the calendar picker state consistent -- Dynamically toggling the `required` attribute now immediately reflects the correct validation state diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 5df51dc2566..f7f3cd0d2ec 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1062,8 +1062,8 @@ export namespace Components { */ "readonly": boolean; /** - * Triggers validation and shows visual error state immediately, regardless of whether the user has interacted with the field. Use this when submitting via AJAX (no HTML form) or when you need to programmatically surface validation errors — equivalent to calling `reportValidity()` on a native `` element. Unlike form submit, this explicit validation call is not suppressed by a surrounding `
` and will still surface errors. - * @returns `true` if the field is valid, `false` otherwise. + * Trigger validation and show visual error state immediately, independently of user interaction — for example, in AJAX submissions or manual validation. Not suppressed by `` — errors surface regardless. + * @returns `true` if valid, `false` otherwise. * @since 5.1.0 */ "reportValidity": () => Promise; diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index c9fb105bd20..6680bee4302 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -43,6 +43,7 @@ import { IxInputFieldComponent, ValidationResults, createClassMutationObserver, + getValidationText, reportFieldValidity, shouldSuppressInternalValidation, } from '../utils/input'; @@ -51,10 +52,9 @@ import { createValidityState, focusInputIfKeyboardMode, handleIconClick, - handlePickerHostFocusout, - handlePickerInputBlur, + handlePickerFocusoutWithValidation, + suppressInputBlurWhenFocusMovedToPicker, openDropdown as openDropdownUtil, - resetPickerValueIfInvalid, syncCustomInputValidity, } from '../utils/input/picker-input.util'; import { MakeRef, makeRef } from '../utils/make-ref'; @@ -65,6 +65,7 @@ import { InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; import { hasKeyboardMode } from '../utils/internal/mixins/setup.mixin'; +import { forceTabIndex } from '../utils/a11y'; /** * @form-ready @@ -294,15 +295,7 @@ export class DateInput private classObserver?: ClassMutationObserver; private _hasInvalidInput = false; - - /** - * Set to `true` by `onBlur` when a genuine (non-suppressed) external blur - * runs validation. Read and reset by `onFocusout` to avoid re-running the - * same validation path a second time (onBlur → onFocusout both fire for a - * direct tab-away). - */ - private _blurHandledValidation = false; - + private _shouldSkipFocusoutValidation = false; private _reportValidityCalled = false; public initialValue?: string; @@ -442,16 +435,12 @@ export class DateInput this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; - // Remove any stale parse-error host classes from a prior reportValidity() - // call. The value is now empty so those errors no longer apply. + this.hostElement.classList.remove( 'ix-invalid--validity-invalid', 'ix-invalid--validity-patternMismatch' ); - // In a regular form, show the required-missing error once the user has - // interacted (touched via blur or reportValidity). - // In a novalidate form, only show it when reportValidity() was explicitly - // called — a plain blur must not surface errors (novalidate intent). + const suppress = await shouldSuppressInternalValidation(this); const shouldShowRequired = suppress ? this._reportValidityCalled && !!this.required @@ -474,10 +463,7 @@ export class DateInput this.valueChange.emit(value); } - // Clears all error state and accepts the value as valid. - // Used for normal novalidate input AND when the user corrects a - // reportValidity() error. - private acceptSuppressedValidationValue(value: string): void { + private acceptValidAfterReportValidity(value: string): void { this._hasInvalidInput = false; this.isInputInvalid = false; this.invalidReason = undefined; @@ -489,64 +475,55 @@ export class DateInput ); this.updateFormInternalValue(value); - resetPickerValueIfInvalid( - value, - (currentValue) => this.getDateValidation(currentValue).isValid, - () => { - this.from = undefined; - } - ); + if (!this.getDateValidation(value).isValid) { + this.from = undefined; + } this.closeDropdown(); focusInputIfKeyboardMode(this.inputElementRef.current); this.emitSuppressedValidationChange(value); } - // Keeps the parse-error classes and message visible after reportValidity(). - private keepSuppressedValidationErrorVisible( + private keepReportValidityErrorsVisible( value: string, invalidReason?: string ): void { this.invalidReason = invalidReason; this.from = undefined; this.isInvalid = true; - this.hostElement.classList.remove('ix-invalid--required'); + this.hostElement.classList.remove( + 'ix-invalid--required', + 'ix-invalid--validity-patternMismatch' + ); this.hostElement.classList.add('ix-invalid--validity-invalid'); this.emitSuppressedValidationChange(value); } - private handleSuppressedValidationInput(value: string): void { - // When reportValidity() was called explicitly, keep validating in the - // novalidate form until the value is actually fixed (WCAG 1.4.1 / 3.3.1). - // NOTE: `touched` is intentionally NOT used — it is set by plain blur too, - // and blur in a novalidate form must never surface errors. - if (!this._reportValidityCalled) { - this.acceptSuppressedValidationValue(value); + private validateInReportValidityMode(value: string): void { + const reportValidityWasNotCalled = !this._reportValidityCalled; + if (reportValidityWasNotCalled) { + this.acceptValidAfterReportValidity(value); return; } const validation = this.getDateValidation(value); - this._hasInvalidInput = !validation.isValid; - this.isInputInvalid = this._hasInvalidInput; + const valueIsInvalid = !validation.isValid; + this._hasInvalidInput = valueIsInvalid; + this.isInputInvalid = valueIsInvalid; - if (this._hasInvalidInput) { - this.keepSuppressedValidationErrorVisible( - value, - validation.invalidReason - ); + if (valueIsInvalid) { + this.keepReportValidityErrorsVisible(value, validation.invalidReason); return; } - // Value is now valid — reset the flag so normal novalidate suppression resumes. this._reportValidityCalled = false; - this.acceptSuppressedValidationValue(value); + this.acceptValidAfterReportValidity(value); } private async handleValidatedInput(value: string): Promise { const validation = this.getDateValidation(value); this._hasInvalidInput = !validation.isValid; - // Only show visual invalid state when the user has interacted this.isInputInvalid = this._hasInvalidInput && this.touched; if (this._hasInvalidInput) { @@ -577,16 +554,12 @@ export class DateInput return; } - // Set _hasInvalidInput synchronously BEFORE the async suppression check so - // that if blur fires during the microtask gap, it always reads the correct - // state. handleValidatedInput and handleSuppressedValidationInput will - // overwrite it with the same (or reset) value afterwards. const validation = this.getDateValidation(value); this._hasInvalidInput = !validation.isValid; const suppressValidation = await shouldSuppressInternalValidation(this); if (suppressValidation) { - this.handleSuppressedValidationInput(value); + this.validateInReportValidityMode(value); return; } @@ -622,6 +595,22 @@ export class DateInput ); } + private handlePickerFocusoutCallback = (hasRelatedTarget: boolean) => { + if (hasRelatedTarget) { + this.closeDropdown(); + } + + if (this._shouldSkipFocusoutValidation) { + this._shouldSkipFocusoutValidation = false; + return; + } + + this.touched = true; + this.isInputInvalid = this._hasInvalidInput; + onInputBlurWithChange(this, this.inputElementRef.current, this.value); + emitPickerValidityState(this); + }; + private renderInput() { return (
@@ -661,11 +650,11 @@ export class DateInput } }} onBlur={(e: FocusEvent) => - handlePickerInputBlur( - e, - this.show, - this.hostElement, - () => { + suppressInputBlurWhenFocusMovedToPicker({ + event: e, + isDropdownOpen: this.show, + hostElement: this.hostElement, + onBlur: () => { this.touched = true; this.isInputInvalid = this._hasInvalidInput; onInputBlurWithChange( @@ -674,12 +663,10 @@ export class DateInput this.value ); emitPickerValidityState(this); - // Signal to onFocusout (which fires right after) that validation - // has already been committed so it should not repeat it. - this._blurHandledValidation = true; + this._shouldSkipFocusoutValidation = true; }, - this.dropdownElementRef?.current - ) + pickerElement: this.dropdownElementRef?.current, + }) } onKeyDown={(event) => this.handleInputKeyDown(event)} style={{ @@ -692,6 +679,7 @@ export class DateInput > forceTabIndex(ref, -1)} aria-label={this.ariaLabelCalendarButton} data-testid="open-calendar" class={{ 'calendar-hidden': this.disabled || this.readonly }} @@ -722,15 +710,9 @@ export class DateInput /** @internal */ @Method() getValidityState(): Promise { - // Gate patternMismatch on touched — same as isInputInvalid. - // HookValidationLifecycle reads this to set ix-invalid--validity-patternMismatch - // on the host, which drives visual feedback. We must not show errors before - // the user has interacted. - // The actual form validity for form.reportValidity() is handled separately - // by syncFormInternalsValidity() which always reflects the true parse state. return Promise.resolve( createValidityState( - this._hasInvalidInput && this.touched, + this.isInputInvalid, !!this.required && this.touched, this.value ) @@ -763,18 +745,12 @@ export class DateInput } /** - * Triggers validation and shows visual error state immediately, regardless - * of whether the user has interacted with the field. - * - * Use this when submitting via AJAX (no HTML form) or when you need to - * programmatically surface validation errors — equivalent to calling - * `reportValidity()` on a native `` element. + * Trigger validation and show visual error state immediately, independently + * of user interaction — for example, in AJAX submissions or manual validation. * - * Unlike form submit, this explicit validation call is not suppressed by a - * surrounding `` and will still surface errors. - * - * @returns `true` if the field is valid, `false` otherwise. + * Not suppressed by `` — errors surface regardless. * + * @returns `true` if valid, `false` otherwise. * @since 5.1.0 */ @Method() @@ -784,17 +760,7 @@ export class DateInput !!this.format && !this.getDateValidation(this.value).isValid; - // Sync _hasInvalidInput so that subsequent blur events preserve the error - // state surfaced by this explicit call. Without this, blur resets - // isInputInvalid back to _hasInvalidInput (which is false in novalidate - // forms due to suppression), causing the error message to disappear while - // the red border stays. this._hasInvalidInput = hasInvalidInput; - - // Mark that an explicit validation call was made. This allows - // handleSuppressedValidationInput and handleEmptyInput to keep showing - // errors in novalidate forms until the value is corrected — while normal - // blur events in novalidate forms remain suppressed. this._reportValidityCalled = true; return reportFieldValidity(this, hasInvalidInput); @@ -804,22 +770,29 @@ export class DateInput return this.dropdownElementRef; } - override render() { - // Error text priority: - // 1. Parse error — "Date is not valid" (or i18n override / custom invalidText) - // 2. Required empty — "Date is required" (or custom invalidText) - // 3. Consumer-supplied invalidText only + private getInvalidText(): string | undefined { const isRequiredEmpty = !!this.required && !this.value && this.touched; - let invalidText: string | undefined; - if (this.isInputInvalid) { - invalidText = this.invalidText ?? this.i18nErrorDateUnparsable; - } else if (isRequiredEmpty) { - invalidText = this.invalidText ?? this.i18nErrorRequired; - } else { - invalidText = this.invalidText; + let invalidText = getValidationText( + this.isInputInvalid, + this.invalidText, + this.i18nErrorDateUnparsable + ); + + if (!invalidText) { + invalidText = getValidationText( + isRequiredEmpty, + this.invalidText, + this.i18nErrorRequired + ); } + return invalidText; + } + + override render() { + const invalidText = this.getInvalidText(); + return ( - handlePickerHostFocusout( + handlePickerFocusoutWithValidation( e, this.hostElement, - (hasRelatedTarget) => { - // Only close the dropdown when focus went to a known external - // element. When relatedTarget is null (focus to or a - // programmatic blur from tests) the dropdown's closeBehavior - // handles it, and closing here risks a flicker during rerenders. - if (hasRelatedTarget) { - this.closeDropdown(); - } - - // When the input's onBlur was suppressed (picker was open, - // focus moved to an internal element) we must now run the full - // validation — the user has truly left the component. - // When onBlur already ran validation (direct tab-away), skip it - // here to avoid double-emitting ixBlur / ixChange. - if (this._blurHandledValidation) { - this._blurHandledValidation = false; - return; - } - - this.touched = true; - this.isInputInvalid = this._hasInvalidInput; - onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); - emitPickerValidityState(this); - }, + this.handlePickerFocusoutCallback, this.dropdownElementRef?.current ) } diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 4a871fc3e93..106d37dc5bf 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -387,6 +387,7 @@ export async function clearInputValue( options?.additionalCleanup?.(); comp.updateFormInternalValue?.(emptyValue); + comp.value = emptyValue; if (options?.emitValueChange) { comp.valueChange?.emit(emptyValue); diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 6a1437ea361..26be9b9d757 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -10,6 +10,14 @@ import { dropdownController } from '../../dropdown/dropdown-controller'; import { hasKeyboardMode } from '../internal/mixins/setup.mixin'; +export interface SuppressInputBlurOptions { + event: FocusEvent; + isDropdownOpen: boolean; + hostElement: HTMLElement; + onBlur: () => void; + pickerElement?: HTMLElement | null; +} + export function focusInputIfKeyboardMode( inputElement: HTMLInputElement | null | undefined ): void { @@ -18,20 +26,6 @@ export function focusInputIfKeyboardMode( } } -export function resetPickerValueIfInvalid( - value: string, - isValid: (value: string) => boolean, - resetPickerValue: () => void -): boolean { - const valid = isValid(value); - - if (!valid) { - resetPickerValue(); - } - - return valid; -} - export async function openDropdown(dropdownElementRef: any) { const dropdownElement = await dropdownElementRef.waitForCurrent(); const id = dropdownElement.getAttribute('data-ix-dropdown'); @@ -100,7 +94,7 @@ export function createValidityState( }; } -function isInternalFocusTarget( +function isFocusWithinPickerBoundary( hostElement: HTMLElement, relatedTarget: Node | null, pickerElement?: HTMLElement | null @@ -115,32 +109,32 @@ function isInternalFocusTarget( ); } -export function handlePickerInputBlur( - e: FocusEvent, - show: boolean, - hostElement: HTMLElement, - onBlur: () => void, - pickerElement?: HTMLElement | null +export function suppressInputBlurWhenFocusMovedToPicker( + options: SuppressInputBlurOptions ): void { - const relatedTarget = e.relatedTarget as Node | null; + const relatedTarget = options.event.relatedTarget as Node | null; if ( - show && - isInternalFocusTarget(hostElement, relatedTarget, pickerElement) + options.isDropdownOpen && + isFocusWithinPickerBoundary( + options.hostElement, + relatedTarget, + options.pickerElement + ) ) { return; } - onBlur(); + options.onBlur(); } -export function handlePickerHostFocusout( +export function handlePickerFocusoutWithValidation( e: FocusEvent, hostElement: HTMLElement, - onExternalFocusout: (hasRelatedTarget: boolean) => void, + onValidateAndBlur: (hasRelatedTarget: boolean) => void, pickerElement?: HTMLElement | null ): void { const relatedTarget = e.relatedTarget as Node | null; - if (isInternalFocusTarget(hostElement, relatedTarget, pickerElement)) { + if (isFocusWithinPickerBoundary(hostElement, relatedTarget, pickerElement)) { return; } @@ -154,7 +148,7 @@ export function handlePickerHostFocusout( return; } - onExternalFocusout(relatedTarget !== null); + onValidateAndBlur(relatedTarget !== null); } export function syncCustomInputValidity( From e1a2a926a67c1ee34d40dcac9b95fbed26f9c8ff Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 10 Jun 2026 21:01:58 +0530 Subject: [PATCH 20/39] fix(date-input): refactor handlePickerFocusoutCallback to use standard method syntax --- packages/core/src/components/date-input/date-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 6680bee4302..cd4b71088f2 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -595,7 +595,7 @@ export class DateInput ); } - private handlePickerFocusoutCallback = (hasRelatedTarget: boolean) => { + private handlePickerFocusoutCallback(hasRelatedTarget: boolean) { if (hasRelatedTarget) { this.closeDropdown(); } @@ -609,7 +609,7 @@ export class DateInput this.isInputInvalid = this._hasInvalidInput; onInputBlurWithChange(this, this.inputElementRef.current, this.value); emitPickerValidityState(this); - }; + } private renderInput() { return ( From 6d83c6c3e461e59f3d56cd8769d4fae70329f06e Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 13:07:15 +0530 Subject: [PATCH 21/39] fix(date-input): Updated changeset file content --- .changeset/feat-date-input-validation.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md index ec61d6072a6..992bb05b64c 100644 --- a/.changeset/feat-date-input-validation.md +++ b/.changeset/feat-date-input-validation.md @@ -2,23 +2,20 @@ '@siemens/ix': minor --- -Added `clear()` method to reset the value and all validation state to its initial state, removing visual error indicators even after the field has been touched. +Added `clear()` method to reset value and validation state, removing all visual error indicators even after the field has been touched. Added `reportValidity()` method to programmatically trigger validation and show visual error state immediately. -Added `i18nErrorRequired` prop (`i18n-error-required`) to show required-missing error message when a required field is emptied after validation has been triggered. - -Validation behavior: Values are always validated internally regardless of source. Visual validation errors only appear after the field has been touched (after first blur). +Added `i18nErrorRequired` prop (`i18n-error-required`) to customize the required-field error message. Fixed validation behavior for `ix-date-input`: -- Non-required field now correctly validates as valid when the value is removed (both keyboard deletion and programmatic empty string) -- `novalidate` forms now fully suppress visual validation feedback while maintaining internal date parsing - -Additionally covered: - -- Dynamically toggling the `required` attribute now immediately reflects correct validation state -- Removed momentary red-border flash when clicking a calendar day -- Picker automatically opens when focusing the date-input via keyboard navigation (Tab key) -- Keyboard navigation within the calendar (e.g., `PageUp`/`PageDown` for months) does not trigger validation errors +- Non-required field is now valid when the value is removed (keyboard deletion or programmatic empty string). +- Required field shows required-missing error only when empty and touched, or after `reportValidity()`. +- Visual validation errors only appear after first blur; programmatic value changes are validated internally without visual feedback until interaction. +- `novalidate` forms suppress all visual validation while `reportValidity()` overrides this suppression. +- Dynamically toggling the `required` attribute immediately reflects correct validation state. +- Picker auto-opens when navigating to the date-input via keyboard (Tab key). +- Calendar keyboard navigation (PageUp/PageDown) does not trigger validation errors. +- Removed momentary red-border flash when clicking a calendar day. From e626b83462d1766696923b1a1734ffc4b74b7b60 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 16:50:01 +0530 Subject: [PATCH 22/39] fix(date-input): update validation logic to handle empty date values correctly --- .../src/components/date-input/date-input.tsx | 43 +++++++++++++------ .../core/src/components/input/input.util.ts | 8 ++-- .../utils/input/picker-input.util.ts | 14 +++--- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index cd4b71088f2..031e3e2f64d 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -391,6 +391,7 @@ export class DateInput this._hasInvalidInput = false; this._reportValidityCalled = false; await clearInputValue(this, { + defaultValue: '', additionalCleanup: () => { this.from = undefined; }, @@ -463,10 +464,14 @@ export class DateInput this.valueChange.emit(value); } - private acceptValidAfterReportValidity(value: string): void { - this._hasInvalidInput = false; + private acceptValidAfterReportValidity( + value: string, + hasInvalidInput: boolean, + invalidReason?: string + ): void { + this._hasInvalidInput = hasInvalidInput; this.isInputInvalid = false; - this.invalidReason = undefined; + this.invalidReason = invalidReason; this.isInvalid = false; this.hostElement.classList.remove( 'ix-invalid--required', @@ -475,7 +480,7 @@ export class DateInput ); this.updateFormInternalValue(value); - if (!this.getDateValidation(value).isValid) { + if (hasInvalidInput) { this.from = undefined; } @@ -500,24 +505,28 @@ export class DateInput } private validateInReportValidityMode(value: string): void { + const validation = this.getDateValidation(value); + const valueIsInvalid = !validation.isValid; + this._hasInvalidInput = valueIsInvalid; + const reportValidityWasNotCalled = !this._reportValidityCalled; if (reportValidityWasNotCalled) { - this.acceptValidAfterReportValidity(value); + this.acceptValidAfterReportValidity( + value, + valueIsInvalid, + valueIsInvalid ? validation.invalidReason : undefined + ); return; } - const validation = this.getDateValidation(value); - const valueIsInvalid = !validation.isValid; - this._hasInvalidInput = valueIsInvalid; this.isInputInvalid = valueIsInvalid; - if (valueIsInvalid) { this.keepReportValidityErrorsVisible(value, validation.invalidReason); return; } this._reportValidityCalled = false; - this.acceptValidAfterReportValidity(value); + this.acceptValidAfterReportValidity(value, false, undefined); } private async handleValidatedInput(value: string): Promise { @@ -595,7 +604,7 @@ export class DateInput ); } - private handlePickerFocusoutCallback(hasRelatedTarget: boolean) { + private async handlePickerFocusoutCallback(hasRelatedTarget: boolean) { if (hasRelatedTarget) { this.closeDropdown(); } @@ -606,7 +615,10 @@ export class DateInput } this.touched = true; - this.isInputInvalid = this._hasInvalidInput; + const suppress = await shouldSuppressInternalValidation(this); + if (!suppress) { + this.isInputInvalid = this._hasInvalidInput; + } onInputBlurWithChange(this, this.inputElementRef.current, this.value); emitPickerValidityState(this); } @@ -654,9 +666,12 @@ export class DateInput event: e, isDropdownOpen: this.show, hostElement: this.hostElement, - onBlur: () => { + onBlur: async () => { this.touched = true; - this.isInputInvalid = this._hasInvalidInput; + const suppress = await shouldSuppressInternalValidation(this); + if (!suppress) { + this.isInputInvalid = this._hasInvalidInput; + } onInputBlurWithChange( this, this.inputElementRef.current, diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 106d37dc5bf..97327498e64 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -335,7 +335,7 @@ export async function syncRequiredValidationClass( return; } - const hasValue = !!comp.value; + const hasValue = comp.value != null && comp.value !== ''; if (comp.required) { hostElement.classList.toggle( 'ix-invalid--required', @@ -358,13 +358,13 @@ export interface ClearableInputComponent { export async function clearInputValue( comp: ClearableInputComponent, - options?: { - defaultValue?: T; + options: { + defaultValue: T; additionalCleanup?: () => void; emitValueChange?: boolean; } ): Promise { - const emptyValue = options?.defaultValue ?? ('' as T); + const emptyValue = options.defaultValue; if ('touched' in comp) { comp.touched = false; diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 26be9b9d757..2a479303f1e 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -15,7 +15,7 @@ export interface SuppressInputBlurOptions { isDropdownOpen: boolean; hostElement: HTMLElement; onBlur: () => void; - pickerElement?: HTMLElement | null; + pickerElement?: (HTMLElement & { show?: boolean }) | null; } export function focusInputIfKeyboardMode( @@ -97,7 +97,7 @@ export function createValidityState( function isFocusWithinPickerBoundary( hostElement: HTMLElement, relatedTarget: Node | null, - pickerElement?: HTMLElement | null + pickerElement?: (HTMLElement & { show?: boolean }) | null ): boolean { if (!relatedTarget) { return false; @@ -130,7 +130,7 @@ export function handlePickerFocusoutWithValidation( e: FocusEvent, hostElement: HTMLElement, onValidateAndBlur: (hasRelatedTarget: boolean) => void, - pickerElement?: HTMLElement | null + pickerElement?: (HTMLElement & { show?: boolean }) | null ): void { const relatedTarget = e.relatedTarget as Node | null; @@ -138,11 +138,7 @@ export function handlePickerFocusoutWithValidation( return; } - const isPickerNavigating = - relatedTarget === null && - pickerElement && - 'show' in pickerElement && - (pickerElement as { show: boolean }).show; + const isPickerNavigating = relatedTarget === null && pickerElement?.show; if (isPickerNavigating) { return; @@ -157,7 +153,7 @@ export function syncCustomInputValidity( required: boolean | undefined, value: string | undefined, invalidMessage: string, - requiredMessage: string = 'Please fill out this field.' + requiredMessage: string ): void { if (hasInvalidInput) { formInternals.setValidity({ patternMismatch: true }, invalidMessage); From 5888100d44f924b8bf5e7ddc33d8389becb982c0 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 16:57:46 +0530 Subject: [PATCH 23/39] fix(date-input): remove unnecessary parameter from acceptValidAfterReportValidity call --- packages/core/src/components/date-input/date-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 031e3e2f64d..f18c9f39352 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -526,7 +526,7 @@ export class DateInput } this._reportValidityCalled = false; - this.acceptValidAfterReportValidity(value, false, undefined); + this.acceptValidAfterReportValidity(value, false); } private async handleValidatedInput(value: string): Promise { From 387eead6a426d2848f151a941e88f35a3f84a325 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 17:06:13 +0530 Subject: [PATCH 24/39] fix(date-input): refactor handlePickerFocusoutCallback to use arrow function syntax --- packages/core/src/components/date-input/date-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index f18c9f39352..642d798eb94 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -604,7 +604,7 @@ export class DateInput ); } - private async handlePickerFocusoutCallback(hasRelatedTarget: boolean) { + private handlePickerFocusoutCallback = async (hasRelatedTarget: boolean) => { if (hasRelatedTarget) { this.closeDropdown(); } @@ -621,7 +621,7 @@ export class DateInput } onInputBlurWithChange(this, this.inputElementRef.current, this.value); emitPickerValidityState(this); - } + }; private renderInput() { return ( From ce00c93de93e5cec33ee9ff7daadf88e71a2f547 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 17:10:05 +0530 Subject: [PATCH 25/39] fix(date-input): mark handlePickerFocusoutCallback as readonly --- packages/core/src/components/date-input/date-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 642d798eb94..9e8541cfb53 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -604,7 +604,7 @@ export class DateInput ); } - private handlePickerFocusoutCallback = async (hasRelatedTarget: boolean) => { + private readonly handlePickerFocusoutCallback = async (hasRelatedTarget: boolean) => { if (hasRelatedTarget) { this.closeDropdown(); } From 09ad34a8bee691c9abf5a3307d3056514cd3c6a4 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Thu, 11 Jun 2026 17:19:33 +0530 Subject: [PATCH 26/39] fix(date-input): format handlePickerFocusoutCallback for improved readability --- packages/core/src/components/date-input/date-input.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 9e8541cfb53..ada9e5eea04 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -604,7 +604,9 @@ export class DateInput ); } - private readonly handlePickerFocusoutCallback = async (hasRelatedTarget: boolean) => { + private readonly handlePickerFocusoutCallback = async ( + hasRelatedTarget: boolean + ) => { if (hasRelatedTarget) { this.closeDropdown(); } From 1f263e70816dee700707c69e4545f1929d8a2e0f Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 22 Jun 2026 20:27:17 +0530 Subject: [PATCH 27/39] fix(date-input): remove unnecessary validation on calendar interactions and improve focusout handling --- .changeset/feat-date-input-validation.md | 1 - .../src/components/date-input/date-input.tsx | 42 +++++-------------- .../date-input/tests/date-input.ct.ts | 24 ++++++----- .../utils/input/picker-input.util.ts | 21 ---------- 4 files changed, 23 insertions(+), 65 deletions(-) diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md index 992bb05b64c..c50a725740a 100644 --- a/.changeset/feat-date-input-validation.md +++ b/.changeset/feat-date-input-validation.md @@ -16,6 +16,5 @@ Fixed validation behavior for `ix-date-input`: - `novalidate` forms suppress all visual validation while `reportValidity()` overrides this suppression. - Dynamically toggling the `required` attribute immediately reflects correct validation state. - Picker auto-opens when navigating to the date-input via keyboard (Tab key). -- Calendar keyboard navigation (PageUp/PageDown) does not trigger validation errors. - Removed momentary red-border flash when clicking a calendar day. diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index ada9e5eea04..8ceb753d3a3 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -52,7 +52,6 @@ import { createValidityState, focusInputIfKeyboardMode, handleIconClick, - handlePickerFocusoutWithValidation, suppressInputBlurWhenFocusMovedToPicker, openDropdown as openDropdownUtil, syncCustomInputValidity, @@ -295,7 +294,6 @@ export class DateInput private classObserver?: ClassMutationObserver; private _hasInvalidInput = false; - private _shouldSkipFocusoutValidation = false; private _reportValidityCalled = false; public initialValue?: string; @@ -604,27 +602,6 @@ export class DateInput ); } - private readonly handlePickerFocusoutCallback = async ( - hasRelatedTarget: boolean - ) => { - if (hasRelatedTarget) { - this.closeDropdown(); - } - - if (this._shouldSkipFocusoutValidation) { - this._shouldSkipFocusoutValidation = false; - return; - } - - this.touched = true; - const suppress = await shouldSuppressInternalValidation(this); - if (!suppress) { - this.isInputInvalid = this._hasInvalidInput; - } - onInputBlurWithChange(this, this.inputElementRef.current, this.value); - emitPickerValidityState(this); - }; - private renderInput() { return (
@@ -680,7 +657,6 @@ export class DateInput this.value ); emitPickerValidityState(this); - this._shouldSkipFocusoutValidation = true; }, pickerElement: this.dropdownElementRef?.current, }) @@ -816,14 +792,16 @@ export class DateInput disabled: this.disabled, readonly: this.readonly, }} - onFocusout={(e: FocusEvent) => - handlePickerFocusoutWithValidation( - e, - this.hostElement, - this.handlePickerFocusoutCallback, - this.dropdownElementRef?.current - ) - } + onFocusout={(e: FocusEvent) => { + const relatedTarget = e.relatedTarget as Node; + + // Related target might be null during rerenders, which would cause the dropdown to close unexpectedly + if (!relatedTarget) { + return; + } + + this.closeDropdown(); + }} > { await expect(dateDropdown).toHaveClass(/show/); } ); +}); +regressionTest.describe('calendar interaction with validation', () => { regressionTest( - 'Keyboard navigation (PageUp/PageDown) should not trigger validation', + 'click and hold on calendar date does not trigger validation for required empty field', async ({ mount, page }) => { - await mount(``); + await mount(``); const dateInput = page.locator('ix-date-input'); const input = dateInput.getByRole('textbox'); - await input.focus(); - await page.keyboard.press('ArrowDown'); - - await page.keyboard.press('PageDown'); - - await page.keyboard.press('PageUp'); + await dateInput.getByTestId('open-calendar').click(); + await expect(dateInput.getByTestId('date-dropdown')).toHaveClass(/show/); - await page.keyboard.press('Escape'); + const calendarDate = page + .locator('ix-dropdown .calendar-item') + .filter({ hasText: /^\d{1,2}$/ }) + .first(); - await expectNoVisualValidation(dateInput, input); - await expect(input).toHaveValue(''); + await calendarDate.dispatchEvent('mousedown'); + await expect(input).not.toHaveClass(/is-invalid/); + await expect(dateInput).not.toHaveClass(/ix-invalid--required/); } ); }); diff --git a/packages/core/src/components/utils/input/picker-input.util.ts b/packages/core/src/components/utils/input/picker-input.util.ts index 2a479303f1e..82360221b68 100644 --- a/packages/core/src/components/utils/input/picker-input.util.ts +++ b/packages/core/src/components/utils/input/picker-input.util.ts @@ -126,27 +126,6 @@ export function suppressInputBlurWhenFocusMovedToPicker( options.onBlur(); } -export function handlePickerFocusoutWithValidation( - e: FocusEvent, - hostElement: HTMLElement, - onValidateAndBlur: (hasRelatedTarget: boolean) => void, - pickerElement?: (HTMLElement & { show?: boolean }) | null -): void { - const relatedTarget = e.relatedTarget as Node | null; - - if (isFocusWithinPickerBoundary(hostElement, relatedTarget, pickerElement)) { - return; - } - - const isPickerNavigating = relatedTarget === null && pickerElement?.show; - - if (isPickerNavigating) { - return; - } - - onValidateAndBlur(relatedTarget !== null); -} - export function syncCustomInputValidity( formInternals: ElementInternals, hasInvalidInput: boolean, From 086f9f5adcb282f906441040210e608627252047 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 22 Jun 2026 20:28:30 +0530 Subject: [PATCH 28/39] fix(date-input): update validation behavior for calendar date selection to prevent error flash on required empty fields --- .changeset/feat-date-input-validation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md index c50a725740a..ded49babe1f 100644 --- a/.changeset/feat-date-input-validation.md +++ b/.changeset/feat-date-input-validation.md @@ -16,5 +16,5 @@ Fixed validation behavior for `ix-date-input`: - `novalidate` forms suppress all visual validation while `reportValidity()` overrides this suppression. - Dynamically toggling the `required` attribute immediately reflects correct validation state. - Picker auto-opens when navigating to the date-input via keyboard (Tab key). -- Removed momentary red-border flash when clicking a calendar day. +- Clicking and holding calendar dates does not trigger validation errors on required empty fields; removed momentary red-border flash when selecting a calendar date From 52c8dbc56c89d2993743c7bb8a0bd279c3ec1c4c Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 22 Jun 2026 21:03:50 +0530 Subject: [PATCH 29/39] fix(number-input): update assertions to use toBeCloseTo for floating point precision --- packages/core/src/components/input/tests/form-ready.ct.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/input/tests/form-ready.ct.ts b/packages/core/src/components/input/tests/form-ready.ct.ts index 53be4ec4792..b27d86972ee 100644 --- a/packages/core/src/components/input/tests/form-ready.ct.ts +++ b/packages/core/src/components/input/tests/form-ready.ct.ts @@ -207,7 +207,7 @@ regressionTest( const emittedValue = await page.evaluate( () => globalThis.__lastEmittedValue ); - expect(emittedValue).toBe(0.3); + expect(emittedValue).toBeCloseTo(0.3); const decrementButton = numberInput.locator('.step-minus'); await decrementButton.click(); @@ -216,7 +216,7 @@ regressionTest( const decrementedValue = await page.evaluate( () => globalThis.__lastEmittedValue ); - expect(decrementedValue).toBe(0.1); + expect(decrementedValue).toBeCloseTo(0.1); } ); From 3b80918364e231a8d185524ebbfdd2b295bdbba2 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Mon, 22 Jun 2026 23:33:18 +0530 Subject: [PATCH 30/39] fix(date-input): prevent form submission when required field is empty or invalid - test cases added --- .changeset/feat-date-input-validation.md | 3 +- .../date-input/tests/date-input.ct.ts | 135 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md index ded49babe1f..5011fa00ac3 100644 --- a/.changeset/feat-date-input-validation.md +++ b/.changeset/feat-date-input-validation.md @@ -16,5 +16,6 @@ Fixed validation behavior for `ix-date-input`: - `novalidate` forms suppress all visual validation while `reportValidity()` overrides this suppression. - Dynamically toggling the `required` attribute immediately reflects correct validation state. - Picker auto-opens when navigating to the date-input via keyboard (Tab key). -- Clicking and holding calendar dates does not trigger validation errors on required empty fields; removed momentary red-border flash when selecting a calendar date +- Clicking and holding calendar dates does not trigger validation errors on required empty fields; removed momentary red-border flash when selecting a calendar date. +- Form submission is now prevented when the field is invalid or required but empty; submission proceeds only when the field is valid (form-associated component with ElementInternals validation). diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index feab218e9e6..958ce2fa95c 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -1005,4 +1005,139 @@ regressionTest.describe('date-input validation scenarios', () => { } ); }); + + regressionTest.describe('validated form submission', () => { + regressionTest( + 'validated form: submit is prevented when required field is empty', + async ({ page, mount }) => { + await mount(` + + + + + `); + + await page.evaluate(() => { + globalThis.__formSubmitted = false; + (document.getElementById('form') as HTMLFormElement).addEventListener( + 'submit', + () => { + globalThis.__formSubmitted = true; + } + ); + }); + + const dateInput = page.locator('ix-date-input'); + await page.locator('button[type="submit"]').click(); + + const wasSubmitted = await page.evaluate( + () => globalThis.__formSubmitted + ); + expect(wasSubmitted).toBe(false); + + await expect(dateInput).toHaveClass(/ix-invalid--required/); + } + ); + + regressionTest( + 'validated form: submit is prevented when field contains invalid date', + async ({ page, mount }) => { + await mount(` +
+ + +
+ `); + + await page.evaluate(() => { + globalThis.__formSubmitted = false; + (document.getElementById('form') as HTMLFormElement).addEventListener( + 'submit', + () => { + globalThis.__formSubmitted = true; + } + ); + }); + + const dateInput = page.locator('ix-date-input'); + + await page.locator('button[type="submit"]').click(); + + const wasSubmitted = await page.evaluate( + () => globalThis.__formSubmitted + ); + expect(wasSubmitted).toBe(false); + + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + } + ); + + regressionTest( + 'validated form: submit is allowed when field is valid', + async ({ page, mount }) => { + await mount(` +
+ + +
+ `); + + await page.evaluate(() => { + globalThis.__formSubmitted = false; + (document.getElementById('form') as HTMLFormElement).addEventListener( + 'submit', + (event: Event) => { + event.preventDefault(); + globalThis.__formSubmitted = true; + } + ); + }); + + const dateInput = page.locator('ix-date-input'); + + await page.locator('button[type="submit"]').click(); + + const wasSubmitted = await page.evaluate( + () => globalThis.__formSubmitted + ); + expect(wasSubmitted).toBe(true); + + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + + regressionTest( + 'novalidate form: submit is allowed when required field is empty', + async ({ page, mount }) => { + await mount(` +
+ + +
+ `); + + await page.evaluate(() => { + globalThis.__formSubmitted = false; + (document.getElementById('form') as HTMLFormElement).addEventListener( + 'submit', + (event: Event) => { + event.preventDefault(); + globalThis.__formSubmitted = true; + } + ); + }); + + const dateInput = page.locator('ix-date-input'); + + await page.locator('button[type="submit"]').click(); + + const wasSubmitted = await page.evaluate( + () => globalThis.__formSubmitted + ); + expect(wasSubmitted).toBe(true); + + await expect(dateInput).not.toHaveClass(/ix-invalid/); + } + ); + }); }); From e531f5d5f7cad6bf6f0ec0938190947b2c104480 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 23 Jun 2026 10:15:04 +0530 Subject: [PATCH 31/39] fix(date-input): improve validation handling for empty date values and streamline form submission tests --- .../src/components/date-input/date-input.tsx | 3 +- .../date-input/tests/date-input.ct.ts | 79 ++++--------------- 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 8ceb753d3a3..9dc9c38dff4 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -426,7 +426,8 @@ export class DateInput date.isValid && (!minDate?.isValid || date >= minDate) && (!maxDate?.isValid || date <= maxDate), - invalidReason: date.invalidReason ?? undefined, + invalidReason: + date.invalidReason == null ? undefined : date.invalidReason, }; } diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 958ce2fa95c..620577e46f0 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -58,6 +58,20 @@ const waitForFormSubmit = (form: Locator) => { return submitPromise; }; +const setupFormSubmitTracking = (page: any, shouldPreventDefault: boolean) => { + return page.evaluate((prevent: boolean) => { + globalThis.__formSubmitted = false; + const form = document.getElementById('form') as HTMLFormElement; + const handleSubmit = (event: Event) => { + if (prevent) { + event.preventDefault(); + } + globalThis.__formSubmitted = true; + }; + form.addEventListener('submit', handleSubmit); + }, shouldPreventDefault); +}; + regressionTest.describe('accessibility', () => { regressionTest('default state', async ({ mount, makeAxeBuilder }) => { await mount( @@ -1017,15 +1031,7 @@ regressionTest.describe('date-input validation scenarios', () => { `); - await page.evaluate(() => { - globalThis.__formSubmitted = false; - (document.getElementById('form') as HTMLFormElement).addEventListener( - 'submit', - () => { - globalThis.__formSubmitted = true; - } - ); - }); + await setupFormSubmitTracking(page, false); const dateInput = page.locator('ix-date-input'); await page.locator('button[type="submit"]').click(); @@ -1049,15 +1055,7 @@ regressionTest.describe('date-input validation scenarios', () => { `); - await page.evaluate(() => { - globalThis.__formSubmitted = false; - (document.getElementById('form') as HTMLFormElement).addEventListener( - 'submit', - () => { - globalThis.__formSubmitted = true; - } - ); - }); + await setupFormSubmitTracking(page, false); const dateInput = page.locator('ix-date-input'); @@ -1082,50 +1080,7 @@ regressionTest.describe('date-input validation scenarios', () => { `); - await page.evaluate(() => { - globalThis.__formSubmitted = false; - (document.getElementById('form') as HTMLFormElement).addEventListener( - 'submit', - (event: Event) => { - event.preventDefault(); - globalThis.__formSubmitted = true; - } - ); - }); - - const dateInput = page.locator('ix-date-input'); - - await page.locator('button[type="submit"]').click(); - - const wasSubmitted = await page.evaluate( - () => globalThis.__formSubmitted - ); - expect(wasSubmitted).toBe(true); - - await expect(dateInput).not.toHaveClass(/ix-invalid/); - } - ); - - regressionTest( - 'novalidate form: submit is allowed when required field is empty', - async ({ page, mount }) => { - await mount(` -
- - -
- `); - - await page.evaluate(() => { - globalThis.__formSubmitted = false; - (document.getElementById('form') as HTMLFormElement).addEventListener( - 'submit', - (event: Event) => { - event.preventDefault(); - globalThis.__formSubmitted = true; - } - ); - }); + await setupFormSubmitTracking(page, true); const dateInput = page.locator('ix-date-input'); From 27c4cc8df71082ffe882a467ec9be4f66a928c2f Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 23 Jun 2026 10:21:56 +0530 Subject: [PATCH 32/39] fix(date-input): simplify invalidReason assignment for date validation - sonarqube --- packages/core/src/components/date-input/date-input.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 9dc9c38dff4..8ceb753d3a3 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -426,8 +426,7 @@ export class DateInput date.isValid && (!minDate?.isValid || date >= minDate) && (!maxDate?.isValid || date <= maxDate), - invalidReason: - date.invalidReason == null ? undefined : date.invalidReason, + invalidReason: date.invalidReason ?? undefined, }; } From b4f5174826eaa6f7cd652f529ca3a49094f099df Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 24 Jun 2026 11:15:54 +0530 Subject: [PATCH 33/39] fix(date-input): remove unused keyboard focus test and clean up code --- .../core/src/components/date-input/date-input.tsx | 4 ---- .../components/date-input/tests/date-input.ct.ts | 15 --------------- 2 files changed, 19 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 8ceb753d3a3..b4aaeb18527 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -63,7 +63,6 @@ import { InputPickerMixin, InputPickerMixinContract, } from '../utils/internal/mixins/input/input-picker.mixin'; -import { hasKeyboardMode } from '../utils/internal/mixins/setup.mixin'; import { forceTabIndex } from '../utils/a11y'; /** @@ -636,9 +635,6 @@ export class DateInput }} onFocus={async () => { this.ixFocus.emit(); - if (hasKeyboardMode()) { - this.openDropdown(); - } }} onBlur={(e: FocusEvent) => suppressInputBlurWhenFocusMovedToPicker({ diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 620577e46f0..2d7cb30bd93 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -387,21 +387,6 @@ regressionTest.describe('keyboard navigation', () => { await page.keyboard.press('Enter'); await expect(dateInputElement).toHaveAttribute('value', '2024/09/05'); }); - - regressionTest( - 'keyboard focus opens picker (keyboard navigation)', - async ({ page, mount }) => { - await mount(``); - - const dateInputElement = page.locator('ix-date-input'); - const input = dateInputElement.locator('input'); - const dateDropdown = dateInputElement.getByTestId('date-dropdown'); - - await page.keyboard.press('Tab'); - await expect(input).toBeFocused(); - await expect(dateDropdown).toHaveClass(/show/); - } - ); }); regressionTest.describe('calendar interaction with validation', () => { From f5902bc025842aa9019da66431d8d06324728751 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 24 Jun 2026 17:42:06 +0530 Subject: [PATCH 34/39] fix(date-input): remove picker auto-open behavior when navigating via keyboard --- .changeset/feat-date-input-validation.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/feat-date-input-validation.md b/.changeset/feat-date-input-validation.md index 5011fa00ac3..431f24263ec 100644 --- a/.changeset/feat-date-input-validation.md +++ b/.changeset/feat-date-input-validation.md @@ -15,7 +15,6 @@ Fixed validation behavior for `ix-date-input`: - Visual validation errors only appear after first blur; programmatic value changes are validated internally without visual feedback until interaction. - `novalidate` forms suppress all visual validation while `reportValidity()` overrides this suppression. - Dynamically toggling the `required` attribute immediately reflects correct validation state. -- Picker auto-opens when navigating to the date-input via keyboard (Tab key). - Clicking and holding calendar dates does not trigger validation errors on required empty fields; removed momentary red-border flash when selecting a calendar date. - Form submission is now prevented when the field is invalid or required but empty; submission proceeds only when the field is valid (form-associated component with ElementInternals validation). From 4d03404356519e46ee6da3b6f60a875171fc2c7d Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Tue, 30 Jun 2026 23:00:39 +0530 Subject: [PATCH 35/39] Added fix for novalidate + invalid + reportValidaity + blur behavior. Updated since tag --- packages/core/src/components.d.ts | 8 ++-- .../src/components/date-input/date-input.tsx | 41 +++++++++++-------- .../date-input/tests/date-input.ct.ts | 40 ++++++++++++++++++ .../core/src/components/input/input.util.ts | 8 ++-- 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index f5f35fa167b..76c89711c79 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -972,7 +972,7 @@ export namespace Components { "ariaLabelPreviousMonthButton"?: string; /** * Clears the input value and resets the touched state. Unlike clearing the value directly, this method restores the initial, non-invalid state and removes visible validation errors. - * @since 5.1.0 + * @since 5.2.0 */ "clear": () => Promise; /** @@ -1013,7 +1013,7 @@ export namespace Components { "i18nErrorDateUnparsable": string; /** * I18n string for the error message when the date field is empty. - * @since 5.1.0 + * @since 5.2.0 * @default 'Date is required' */ "i18nErrorRequired": string; @@ -1064,7 +1064,7 @@ export namespace Components { /** * Trigger validation and show visual error state immediately, independently of user interaction — for example, in AJAX submissions or manual validation. Not suppressed by `
` — errors surface regardless. * @returns `true` if valid, `false` otherwise. - * @since 5.1.0 + * @since 5.2.0 */ "reportValidity": () => Promise; /** @@ -7379,7 +7379,7 @@ declare namespace LocalJSX { "i18nErrorDateUnparsable"?: string; /** * I18n string for the error message when the date field is empty. - * @since 5.1.0 + * @since 5.2.0 * @default 'Date is required' */ "i18nErrorRequired"?: string; diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index b4aaeb18527..2849b209513 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -205,7 +205,7 @@ export class DateInput /** * I18n string for the error message when the date field is empty. * - * @since 5.1.0 + * @since 5.2.0 */ @Prop({ attribute: 'i18n-error-required' }) i18nErrorRequired = 'Date is required'; @@ -381,7 +381,7 @@ export class DateInput * Unlike clearing the value directly, this method restores the initial, * non-invalid state and removes visible validation errors. * - * @since 5.1.0 + * @since 5.2.0 */ @Method() async clear(): Promise { @@ -590,7 +590,8 @@ export class DateInput } private checkClassList() { - this.isInvalid = this.hostElement.classList.contains('ix-invalid'); + this.isInvalid = + this.hostElement.classList.contains('ix-invalid') || this.isInputInvalid; } private handleInputKeyDown(event: KeyboardEvent) { @@ -601,6 +602,24 @@ export class DateInput ); } + private async handleInputBlur(): Promise { + this.touched = true; + const suppress = await shouldSuppressInternalValidation(this); + if (!suppress) { + this.isInputInvalid = this._hasInvalidInput; + } + await onInputBlurWithChange( + this, + this.inputElementRef.current, + this.value + ); + emitPickerValidityState(this); + + if (suppress && this._reportValidityCalled && this._hasInvalidInput) { + this.hostElement.classList.add('ix-invalid--validity-invalid'); + } + } + private renderInput() { return (
@@ -641,19 +660,7 @@ export class DateInput event: e, isDropdownOpen: this.show, hostElement: this.hostElement, - onBlur: async () => { - this.touched = true; - const suppress = await shouldSuppressInternalValidation(this); - if (!suppress) { - this.isInputInvalid = this._hasInvalidInput; - } - onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); - emitPickerValidityState(this); - }, + onBlur: () => this.handleInputBlur(), pickerElement: this.dropdownElementRef?.current, }) } @@ -740,7 +747,7 @@ export class DateInput * Not suppressed by `` — errors surface regardless. * * @returns `true` if valid, `false` otherwise. - * @since 5.1.0 + * @since 5.2.0 */ @Method() async reportValidity(): Promise { diff --git a/packages/core/src/components/date-input/tests/date-input.ct.ts b/packages/core/src/components/date-input/tests/date-input.ct.ts index 2d7cb30bd93..7640d214d77 100644 --- a/packages/core/src/components/date-input/tests/date-input.ct.ts +++ b/packages/core/src/components/date-input/tests/date-input.ct.ts @@ -803,6 +803,46 @@ regressionTest.describe('date-input validation scenarios', () => { } ); + regressionTest( + 'novalidate form: reportValidity() error persists after manual blur with input border red', + async ({ page, mount }) => { + await mount(` + + + + `); + const dateInput = page.locator('ix-date-input'); + const input = dateInput.getByRole('textbox'); + + await input.fill('invalid-date'); + + await dateInput.evaluate((el: HTMLIxDateInputElement) => { + el.reportValidity(); + }); + + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'Date is not valid' }) + ).toBeVisible(); + + await input.focus(); + await input.blur(); + + await expect(input).toHaveClass(/is-invalid/); + await expect(dateInput).toHaveClass(/ix-invalid--validity-invalid/); + await expect( + dateInput + .locator('ix-field-wrapper') + .locator('ix-typography') + .filter({ hasText: 'Date is not valid' }) + ).toBeVisible(); + } + ); + regressionTest( 'novalidate form: reportValidity() error persists when value remains invalid, clears when fixed', async ({ page, mount }) => { diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 97327498e64..0785f701fbd 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -87,7 +87,7 @@ export async function checkInternalValidity( comp.hostElement.classList.toggle('ix-invalid--validity-invalid', !valid); } -export function onInputBlur( +export async function onInputBlur( comp: IxFormComponent, input?: HTMLInputElement | HTMLTextAreaElement | null ) { @@ -98,7 +98,7 @@ export function onInputBlur( } input.setAttribute('data-ix-touched', 'true'); - checkInternalValidity(comp, input); + await checkInternalValidity(comp, input); } export function applyPaddingEnd( @@ -229,7 +229,7 @@ export function onInputFocus(comp: { initialValue?: T }, currentValue: T) { comp.initialValue = currentValue; } -export function onInputBlurWithChange( +export async function onInputBlurWithChange( comp: IxFormComponent & { initialValue?: T; ixChange: { emit: (value: T) => void }; @@ -237,7 +237,7 @@ export function onInputBlurWithChange( input?: HTMLInputElement | HTMLTextAreaElement | null, currentValue?: T ) { - onInputBlur(comp, input); + await onInputBlur(comp, input); if (comp.initialValue !== currentValue) { comp.ixChange.emit(currentValue!); From d24c4636a3b533178db6a44f72890bade7199050 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 1 Jul 2026 17:05:20 +0530 Subject: [PATCH 36/39] fix(date-input): streamline input blur handling for improved validity state --- packages/core/src/components/date-input/date-input.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index 2849b209513..bf5a926003a 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -608,11 +608,7 @@ export class DateInput if (!suppress) { this.isInputInvalid = this._hasInvalidInput; } - await onInputBlurWithChange( - this, - this.inputElementRef.current, - this.value - ); + await onInputBlurWithChange(this, this.inputElementRef.current, this.value); emitPickerValidityState(this); if (suppress && this._reportValidityCalled && this._hasInvalidInput) { From a8c94fcf3b18c480b0b290cf6651a8ff103c6cb1 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Wed, 1 Jul 2026 21:45:32 +0530 Subject: [PATCH 37/39] Move additionalCleanup call after updating form value --- packages/core/src/components/input/input.util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/input/input.util.ts b/packages/core/src/components/input/input.util.ts index 0785f701fbd..ebe16e84a1b 100644 --- a/packages/core/src/components/input/input.util.ts +++ b/packages/core/src/components/input/input.util.ts @@ -384,11 +384,11 @@ export async function clearInputValue( 'ix-invalid--validity-patternMismatch' ); - options?.additionalCleanup?.(); - comp.updateFormInternalValue?.(emptyValue); comp.value = emptyValue; + options?.additionalCleanup?.(); + if (options?.emitValueChange) { comp.valueChange?.emit(emptyValue); } From 66bc24e0cd37772f36800c4e5f14a8012867b8fe Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Fri, 3 Jul 2026 20:57:00 +0530 Subject: [PATCH 38/39] fix(date-input): reduced code lines and used existing method --- .../core/src/components/date-input/date-input.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index bf5a926003a..ed81a4d9e6e 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -450,12 +450,10 @@ export class DateInput this.hostElement.classList.remove('ix-invalid--required'); } this.updateFormInternalValue(value); - this.syncFormInternalsValidity(); - emitPickerValidityState(this); - this.valueChange.emit(value); + this.emitSuppressedValidationChange(value); } - private emitSuppressedValidationChange(value: string): void { + private emitSuppressedValidationChange(value: string | undefined): void { this.syncFormInternalsValidity(); emitPickerValidityState(this); this.valueChange.emit(value); @@ -544,9 +542,7 @@ export class DateInput focusInputIfKeyboardMode(this.inputElementRef.current); } - this.syncFormInternalsValidity(); - emitPickerValidityState(this); - this.valueChange.emit(value); + this.emitSuppressedValidationChange(value); } async onInput(value: string | undefined) { @@ -651,9 +647,9 @@ export class DateInput onFocus={async () => { this.ixFocus.emit(); }} - onBlur={(e: FocusEvent) => + onBlur={(event: FocusEvent) => suppressInputBlurWhenFocusMovedToPicker({ - event: e, + event: event, isDropdownOpen: this.show, hostElement: this.hostElement, onBlur: () => this.handleInputBlur(), From 1d16592baa646ccfda86f1838ec55f67f09515f2 Mon Sep 17 00:00:00 2001 From: Ram Mondal Date: Fri, 3 Jul 2026 21:17:44 +0530 Subject: [PATCH 39/39] Simplify event parameter in onBlur handler --- packages/core/src/components/date-input/date-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/date-input/date-input.tsx b/packages/core/src/components/date-input/date-input.tsx index ed81a4d9e6e..e4570a73589 100644 --- a/packages/core/src/components/date-input/date-input.tsx +++ b/packages/core/src/components/date-input/date-input.tsx @@ -649,7 +649,7 @@ export class DateInput }} onBlur={(event: FocusEvent) => suppressInputBlurWhenFocusMovedToPicker({ - event: event, + event, isDropdownOpen: this.show, hostElement: this.hostElement, onBlur: () => this.handleInputBlur(),