Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
bab3d39
feat(time-input): enhance validation and accessibility features
RamVinayMandal Jun 22, 2026
ef0116f
fix(time-input): improve validation handling and accessibility features
RamVinayMandal Jun 23, 2026
caacf3f
Implement code changes to enhance functionality and improve performance
RamVinayMandal Jun 25, 2026
dae9b77
feat: test cases finalized. removed unnecessary method call from timeโ€ฆ
RamVinayMandal Jun 29, 2026
7999216
Merge branch 'main' into fix-ix-3322-time-input-validation-improvements
RamVinayMandal Jun 29, 2026
54e06bf
addressed gemini review comments and added fix for novalidate form blโ€ฆ
RamVinayMandal Jun 29, 2026
522b5c2
Merge branch 'fix-ix-3322-time-input-validation-improvements' of githโ€ฆ
RamVinayMandal Jun 29, 2026
b42b428
chore: update version annotations for time input validation methods tโ€ฆ
RamVinayMandal Jul 1, 2026
145cdb5
fix: improve time input validation messages and cleanup code
RamVinayMandal Jul 1, 2026
522c06c
Merge branch 'main' into fix-ix-3322-time-input-validation-improvements
RamVinayMandal Jul 1, 2026
f3946eb
refactor: rename syncValidityToFormInternals to syncFormInternalsValiโ€ฆ
RamVinayMandal Jul 2, 2026
9858e7a
Merge branch 'fix-ix-3322-time-input-validation-improvements' of githโ€ฆ
RamVinayMandal Jul 2, 2026
89dace5
Merge branch 'main' into fix-ix-3322-time-input-validation-improvements
RamVinayMandal Jul 3, 2026
1bcf90b
refactor(time-input): remove unused focusInputIfKeyboardMode functionโ€ฆ
RamVinayMandal Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/feat-time-input-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@siemens/ix': minor
---

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 customize the required-field error message.

Fixed validation behavior for `ix-time-input`:

- 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.
- Clicking and holding time values does not trigger validation errors on required empty fields; removed momentary red-border flash when selecting a time.
- 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).
6 changes: 3 additions & 3 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2793,15 +2793,15 @@ export declare interface IxTile extends Components.IxTile {}


@ProxyCmp({
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorRequired', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['clear', 'reportValidity', 'getNativeInputElement', 'focusInput']
})
@Component({
selector: 'ix-time-input',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorRequired', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
outputs: ['valueChange', 'validityStateChange', 'ixChange'],
standalone: false
})
Expand Down
6 changes: 3 additions & 3 deletions packages/angular/standalone/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2898,15 +2898,15 @@ export declare interface IxTile extends Components.IxTile {}

@ProxyCmp({
defineCustomElementFn: defineIxTimeInput,
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorRequired', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['clear', 'reportValidity', 'getNativeInputElement', 'focusInput']
})
@Component({
selector: 'ix-time-input',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
inputs: ['ariaLabelTimeToggleButton', 'disabled', 'enableTopLayer', 'format', 'helperText', 'hideHeader', 'hourInterval', 'i18nErrorRequired', 'i18nErrorTimeUnparsable', 'i18nHourColumnHeader', 'i18nMillisecondColumnHeader', 'i18nMinuteColumnHeader', 'i18nSecondColumnHeader', 'i18nSelectTime', 'i18nTime', 'infoText', 'invalidText', 'label', 'maxTime', 'millisecondInterval', 'minTime', 'minuteInterval', 'name', 'placeholder', 'readonly', 'required', 'secondInterval', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
outputs: ['valueChange', 'validityStateChange', 'ixChange'],
})
export class IxTimeInput {
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4045,6 +4045,11 @@ export namespace Components {
* @default 'Toggle time picker'
*/
"ariaLabelTimeToggleButton"?: 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.2.0
*/
"clear": () => Promise<void>;
/**
* Disabled attribute.
* @default false
Expand Down Expand Up @@ -4087,6 +4092,12 @@ export namespace Components {
* @default 1
*/
"hourInterval": number;
/**
* I18n string for the error message when the time field is empty and required.
* @since 5.2.0
* @default 'Time is required'
*/
"i18nErrorRequired": string;
/**
* I18n string for the error message when the time is not parsable.
* @default 'Time is not valid'
Expand Down Expand Up @@ -4172,6 +4183,12 @@ export namespace Components {
* @default false
*/
"readonly": boolean;
/**
* Trigger validation and show visual error state immediately, independently of user interaction โ€” for example, in AJAX submissions or manual validation. Not suppressed by `<form novalidate>` โ€” errors surface regardless.
* @returns `true` if valid, `false` otherwise.
* @since 5.2.0
*/
"reportValidity": () => Promise<boolean>;
/**
* Required attribute.
*/
Expand Down Expand Up @@ -10874,6 +10891,12 @@ declare namespace LocalJSX {
* @default 1
*/
"hourInterval"?: number;
/**
* I18n string for the error message when the time field is empty and required.
* @since 5.2.0
* @default 'Time is required'
*/
"i18nErrorRequired"?: string;
/**
* I18n string for the error message when the time is not parsable.
* @default 'Time is not valid'
Expand Down Expand Up @@ -12435,6 +12458,7 @@ declare namespace LocalJSX {
"validText": string;
"showTextAsTooltip": boolean;
"i18nErrorTimeUnparsable": string;
"i18nErrorRequired": string;
"hourInterval": number;
"minuteInterval": number;
"secondInterval": number;
Expand Down
88 changes: 79 additions & 9 deletions packages/core/src/components/input/input.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export async function checkInternalValidity<T>(
comp: IxFormComponent<T>,
input: HTMLInputElement | HTMLTextAreaElement
) {
const skipValidation = await shouldSuppressInternalValidation(comp);
if (skipValidation) {
comp.hostElement.classList.remove('ix-invalid--validity-invalid');
return;
}

const validityState = input.validity;
const currentValidityState = !comp.hostElement.classList.contains(
'ix-invalid--validity-invalid'
Expand All @@ -77,16 +83,11 @@ export async function checkInternalValidity<T>(
return;
}

const skipValidation = await shouldSuppressInternalValidation(comp);
if (skipValidation) {
return;
}

const { valid } = validityState;
comp.hostElement.classList.toggle('ix-invalid--validity-invalid', !valid);
}

export function onInputBlur<T>(
export async function onInputBlur<T>(
comp: IxFormComponent<T>,
input?: HTMLInputElement | HTMLTextAreaElement | null
) {
Expand All @@ -97,7 +98,7 @@ export function onInputBlur<T>(
}

input.setAttribute('data-ix-touched', 'true');
checkInternalValidity(comp, input);
await checkInternalValidity(comp, input);
}

export function applyPaddingEnd(
Expand Down Expand Up @@ -228,15 +229,15 @@ export function onInputFocus<T>(comp: { initialValue?: T }, currentValue: T) {
comp.initialValue = currentValue;
}

export function onInputBlurWithChange<T>(
export async function onInputBlurWithChange<T>(
comp: IxFormComponent<T> & {
initialValue?: T;
ixChange: { emit: (value: T) => void };
},
input?: HTMLInputElement | HTMLTextAreaElement | null,
currentValue?: T
) {
onInputBlur(comp, input);
await onInputBlur(comp, input);

if (comp.initialValue !== currentValue) {
comp.ixChange.emit(currentValue!);
Expand Down Expand Up @@ -323,3 +324,72 @@ export async function emitPickerValidityStateChangeIfChanged(
invalidReason: context.invalidReason,
});
}

export async function syncRequiredValidationClass<T>(
hostElement: HTMLElement,
comp: IxFormComponent<T> & { required?: boolean; touched: boolean }
): Promise<void> {
const skipValidation = await shouldSuppressInternalValidation(comp);
if (skipValidation) {
hostElement.classList.remove('ix-invalid--required');
return;
}

const hasValue = comp.value != null && comp.value !== '';
if (comp.required) {
hostElement.classList.toggle(
'ix-invalid--required',
!hasValue && comp.touched
);
} else {
hostElement.classList.remove('ix-invalid--required');
}
}

export interface ClearableInputComponent<T> {
value?: T;
hostElement: HTMLElement;
touched?: boolean;
isInputInvalid?: boolean;
invalidReason?: string;
updateFormInternalValue?: (value: T) => void;
valueChange?: { emit: (value: T) => void };
}

export async function clearInputValue<T>(
comp: ClearableInputComponent<T>,
options: {
defaultValue: T;
additionalCleanup?: () => void;
emitValueChange?: boolean;
}
): Promise<void> {
const emptyValue = options.defaultValue;

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'
);

comp.updateFormInternalValue?.(emptyValue);
comp.value = emptyValue;

options?.additionalCleanup?.();

if (options?.emitValueChange) {
comp.valueChange?.emit(emptyValue);
}
}
Loading
Loading