Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
98ab732
Pushed changes for input, textarea, number input and select
1307-Dev Nov 19, 2025
08923ca
Checkbox - form validations handled.
khathija-ahamadi Nov 19, 2025
728fe99
Merge branch '(feature/core)validation-input-components' of https://gโ€ฆ
khathija-ahamadi Nov 19, 2025
85f2720
Checkbox unit tests added for noValidate scenraios.
khathija-ahamadi Nov 20, 2025
df34372
removed reset for select component.
1307-Dev Nov 20, 2025
0e93b01
Merge branch '(feature/core)validation-input-components' of https://gโ€ฆ
1307-Dev Nov 20, 2025
947c8c8
Lint and prettier fixes - Checkbox component.
khathija-ahamadi Nov 20, 2025
f69720c
Merge branch '(feature/core)validation-input-components' of https://gโ€ฆ
khathija-ahamadi Nov 20, 2025
2114e6b
Added lint fixes
1307-Dev Nov 20, 2025
fa156fc
Checkbox component refactored - congnitive complexity fixed (sonar)
khathija-ahamadi Nov 20, 2025
1a804f4
Merge branch '(feature/core)validation-input-components' of https://gโ€ฆ
khathija-ahamadi Nov 20, 2025
384f62b
Radio form validations for noValidate cases implemented.
khathija-ahamadi Nov 20, 2025
620e354
Checkbox and radio component refactored.
khathija-ahamadi Nov 20, 2025
d1f1b32
Radio button component refactored.
khathija-ahamadi Nov 20, 2025
78d44d8
Duplicate issue of sonar qube for checkbox component fixed.
khathija-ahamadi Nov 20, 2025
42ba146
Sonar fixes
khathija-ahamadi Nov 21, 2025
78c5ea2
Sonar fixes.
khathija-ahamadi Nov 21, 2025
2e25c8d
Checkbox and radio sonar fixes.
khathija-ahamadi Nov 21, 2025
7eb75b0
Checkbox and radio component refactored.
khathija-ahamadi Nov 21, 2025
aa3cccb
Radio and checkbox sonar fixes.
khathija-ahamadi Nov 21, 2025
8b4c75d
Merge branch 'main' into (feature/core)validation-input-components
1307-Dev Nov 21, 2025
0177782
Radio group linting issue fixed.
khathija-ahamadi Nov 21, 2025
2017b10
Merge branch '(feature/core)validation-input-components' of https://gโ€ฆ
khathija-ahamadi Nov 21, 2025
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
6 changes: 3 additions & 3 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1326,7 +1326,7 @@ export declare interface IxIconToggleButton extends Components.IxIconToggleButto

@ProxyCmp({
inputs: ['allowedCharactersPattern', 'disabled', 'helperText', 'infoText', 'invalidText', 'label', 'maxLength', 'minLength', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'type', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'getValidityState', 'focusInput']
methods: ['getNativeInputElement', 'getValidityState', 'focusInput', 'reset']
})
@Component({
selector: 'ix-input',
Expand Down Expand Up @@ -2003,7 +2003,7 @@ Can be prevented, in which case only the event is triggered, and the modal remai

@ProxyCmp({
inputs: ['allowEmptyValueChange', 'allowedCharactersPattern', 'disabled', 'helperText', 'infoText', 'invalidText', 'label', 'max', 'min', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'showStepperButtons', 'showTextAsTooltip', 'step', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
methods: ['getNativeInputElement', 'focusInput', 'reset']
})
@Component({
selector: 'ix-number-input',
Expand Down Expand Up @@ -2526,7 +2526,7 @@ export declare interface IxTabs extends Components.IxTabs {

@ProxyCmp({
inputs: ['disabled', 'helperText', 'infoText', 'invalidText', 'label', 'maxLength', 'minLength', 'name', 'placeholder', 'readonly', 'required', 'resizeBehavior', 'showTextAsTooltip', 'textareaCols', 'textareaHeight', 'textareaRows', 'textareaWidth', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
methods: ['getNativeInputElement', 'getValidityState', 'focusInput', 'reset']
})
@Component({
selector: 'ix-textarea',
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 @@ -1427,7 +1427,7 @@ export declare interface IxIconToggleButton extends Components.IxIconToggleButto
@ProxyCmp({
defineCustomElementFn: defineIxInput,
inputs: ['allowedCharactersPattern', 'disabled', 'helperText', 'infoText', 'invalidText', 'label', 'maxLength', 'minLength', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'showTextAsTooltip', 'suppressSubmitOnEnter', 'textAlignment', 'type', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'getValidityState', 'focusInput']
methods: ['getNativeInputElement', 'getValidityState', 'focusInput', 'reset']
})
@Component({
selector: 'ix-input',
Expand Down Expand Up @@ -2104,7 +2104,7 @@ Can be prevented, in which case only the event is triggered, and the modal remai
@ProxyCmp({
defineCustomElementFn: defineIxNumberInput,
inputs: ['allowEmptyValueChange', 'allowedCharactersPattern', 'disabled', 'helperText', 'infoText', 'invalidText', 'label', 'max', 'min', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'showStepperButtons', 'showTextAsTooltip', 'step', 'suppressSubmitOnEnter', 'textAlignment', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
methods: ['getNativeInputElement', 'focusInput', 'reset']
})
@Component({
selector: 'ix-number-input',
Expand Down Expand Up @@ -2627,7 +2627,7 @@ export declare interface IxTabs extends Components.IxTabs {
@ProxyCmp({
defineCustomElementFn: defineIxTextarea,
inputs: ['disabled', 'helperText', 'infoText', 'invalidText', 'label', 'maxLength', 'minLength', 'name', 'placeholder', 'readonly', 'required', 'resizeBehavior', 'showTextAsTooltip', 'textareaCols', 'textareaHeight', 'textareaRows', 'textareaWidth', 'validText', 'value', 'warningText'],
methods: ['getNativeInputElement', 'focusInput']
methods: ['getNativeInputElement', 'getValidityState', 'focusInput', 'reset']
})
@Component({
selector: 'ix-textarea',
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,10 @@ export namespace Components {
* @default false
*/
"required": boolean;
/**
* Resets the input field validation state by removing the touched state and clearing validation states while preserving the current value.
*/
"reset": () => Promise<void>;
/**
* Specifies whether to show the text as a tooltip.
*/
Expand Down Expand Up @@ -2580,6 +2584,10 @@ export namespace Components {
* @default false
*/
"required": boolean;
/**
* Resets the input field validation state by removing the touched state and clearing validation states while preserving the current value.
*/
"reset": () => Promise<void>;
/**
* Indicates if the stepper buttons should be shown
*/
Expand Down Expand Up @@ -3357,6 +3365,10 @@ export namespace Components {
* Get the native textarea element.
*/
"getNativeInputElement": () => Promise<HTMLTextAreaElement>;
/**
* Returns the validity state of the textarea field.
*/
"getValidityState": () => Promise<ValidityState>;
"hasValidValue": () => Promise<boolean>;
/**
* The helper text for the textarea field.
Expand Down Expand Up @@ -3404,6 +3416,10 @@ export namespace Components {
* @default false
*/
"required": boolean;
/**
* Resets the input field validation state by removing the touched state and clearing validation states while preserving the current value.
*/
"reset": () => Promise<void>;
/**
* Determines the resize behavior of the textarea field. Resizing can be enabled in one direction, both directions or completely disabled.
* @default 'both'
Expand Down
172 changes: 152 additions & 20 deletions packages/core/src/components/checkbox-group/checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import {
ValidationResults,
} from '../utils/input';
import { IxComponent } from '../utils/internal';
import { useFieldGroupValidation } from '../utils/field-group-utils';
import { makeRef } from '../utils/make-ref';
import {
isFormNoValidate,
setupFormSubmitListener,
updateCheckboxValidationClasses,
} from '../checkbox/checkbox-validation';

/**
* @form-ready
Expand Down Expand Up @@ -77,36 +83,39 @@ export class CheckboxGroup
@State() isWarning = false;

private touched = false;
private formSubmissionAttempt = false;
private cleanFormListener?: () => void;
private readonly groupRef = makeRef<HTMLElement>();

get checkboxElements(): HTMLIxCheckboxElement[] {
return Array.from(this.hostElement.querySelectorAll('ix-checkbox'));
}

private readonly observer = new MutationObserver(() => {
this.checkForRequiredCheckbox();
});
private readonly validation = useFieldGroupValidation<HTMLIxCheckboxElement>(
this.hostElement,
{
selector: 'ix-checkbox',
isChecked: (el) => el.checked,
isRequired: (el) => el.required,
updateValidationClasses: updateCheckboxValidationClasses,
clearValidationState: this.clearValidationState.bind(this),
}
);

private checkForRequiredCheckbox() {
this.required = this.checkboxElements.some((checkbox) => checkbox.required);
get checkboxElements(): HTMLIxCheckboxElement[] {
return this.validation.getElements();
}

connectedCallback(): void {
this.observer.observe(this.hostElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['checked', 'required'],
private setupFormListener() {
this.cleanFormListener = setupFormSubmitListener(this.hostElement, () => {
this.formSubmissionAttempt = true;
this.syncValidationClasses();
});
}

componentWillLoad(): void | Promise<void> {
this.checkForRequiredCheckbox();
connectedCallback(): void {
this.setupFormListener();
}

disconnectedCallback(): void {
if (this.observer) {
this.observer.disconnect();
if (this.cleanFormListener) {
this.cleanFormListener();
}
}

Expand Down Expand Up @@ -144,9 +153,132 @@ export class CheckboxGroup
);
}

private hasAnyChecked(): boolean {
return this.validation.hasAnyChecked();
}

private clearValidationState() {
this.hostElement.classList.remove('ix-invalid--required', 'ix-invalid');
if (this.invalidText) {
this.invalidText = '';
}
this.checkboxElements.forEach((el: any) => {
el.classList.remove('ix-invalid', 'ix-invalid--required');
});
}

private handleRequiredValidationShared(params: {
elements: HTMLElement[];
hasAnyChecked: boolean;
touched: boolean;
formSubmissionAttempt: boolean;
invalidText: string | undefined;
hostElement: HTMLElement;
clearValidationState: () => void;
updateValidationClasses: (
elements: HTMLElement[],
isChecked: boolean,
touched: boolean,
formSubmissionAttempt: boolean
) => void;
}) {
const {
elements,
hasAnyChecked,
touched,
formSubmissionAttempt,
invalidText,
hostElement,
clearValidationState,
updateValidationClasses,
} = params;

if (isFormNoValidate(hostElement)) {
clearValidationState();
return;
}
const requiredElements = elements.filter(
(el) => (el as HTMLIxCheckboxElement).required
);
const isChecked = hasAnyChecked;
const anyTouched = requiredElements.some(
(el) =>
(
el as HTMLIxCheckboxElement & {
touched?: boolean;
formSubmissionAttempted?: boolean;
}
).touched ||
(
el as HTMLIxCheckboxElement & {
touched?: boolean;
formSubmissionAttempted?: boolean;
}
).formSubmissionAttempted
);
const isRequiredInvalid =
!isChecked && (touched || formSubmissionAttempt || anyTouched);
hostElement.classList.toggle('ix-invalid--required', isRequiredInvalid);
if (isRequiredInvalid) {
hostElement.classList.add('ix-invalid');
this.invalidText =
invalidText && invalidText.trim().length > 0
? invalidText
: 'Please select the required field.';
} else {
hostElement.classList.remove('ix-invalid', 'ix-invalid--required');
if (invalidText === 'Please select the required field.') {
this.invalidText = '';
}
}
if (!isFormNoValidate(hostElement)) {
updateValidationClasses(
elements,
isChecked,
touched,
formSubmissionAttempt
);
}
if (isChecked) {
hostElement.classList.remove('ix-invalid', 'ix-invalid--required');
}
}

private handleRequiredValidation() {
this.handleRequiredValidationShared({
elements: this.checkboxElements,
hasAnyChecked: this.hasAnyChecked(),
touched: this.touched,
formSubmissionAttempt: this.formSubmissionAttempt,
invalidText: this.invalidText,
hostElement: this.hostElement,
clearValidationState: this.clearValidationState.bind(this),
updateValidationClasses: updateCheckboxValidationClasses,
});
}

async syncValidationClasses() {
if (isFormNoValidate(this.hostElement)) {
this.clearValidationState();
return;
}
if (this.required) {
this.handleRequiredValidation();
} else {
this.clearValidationState();
}
}

render() {
return (
<Host ref={this.groupRef} onIxBlur={() => (this.touched = true)}>
<Host
onIxBlur={() => {
if (!this.touched) {
this.touched = true;
this.syncValidationClasses();
}
}}
>
<ix-field-wrapper
label={this.label}
helperText={this.helperText}
Expand Down
Loading