Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 2 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ export declare interface IxCategoryFilter extends Components.IxCategoryFilter {


@ProxyCmp({
inputs: ['checked', 'disabled', 'indeterminate', 'label', 'name', 'required', 'value']
inputs: ['checked', 'disabled', 'indeterminate', 'label', 'name', 'required', 'value'],
methods: ['clear']
})
@Component({
selector: 'ix-checkbox',
Expand Down Expand Up @@ -443,7 +444,8 @@ export declare interface IxCheckbox extends Components.IxCheckbox {


@ProxyCmp({
inputs: ['direction', 'helperText', 'infoText', 'invalidText', 'label', 'showTextAsTooltip', 'validText', 'warningText']
inputs: ['direction', 'helperText', 'infoText', 'invalidText', 'label', 'showTextAsTooltip', 'validText', 'warningText'],
methods: ['clear']
})
@Component({
selector: 'ix-checkbox-group',
Expand Down
6 changes: 4 additions & 2 deletions packages/angular/standalone/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@

@ProxyCmp({
defineCustomElementFn: defineIxCheckbox,
inputs: ['checked', 'disabled', 'indeterminate', 'label', 'name', 'required', 'value']
inputs: ['checked', 'disabled', 'indeterminate', 'label', 'name', 'required', 'value'],
methods: ['clear']
})
@Component({
selector: 'ix-checkbox',
Expand Down Expand Up @@ -546,7 +547,8 @@

@ProxyCmp({
defineCustomElementFn: defineIxCheckboxGroup,
inputs: ['direction', 'helperText', 'infoText', 'invalidText', 'label', 'showTextAsTooltip', 'validText', 'warningText']
inputs: ['direction', 'helperText', 'infoText', 'invalidText', 'label', 'showTextAsTooltip', 'validText', 'warningText'],
methods: ['clear']
})
@Component({
selector: 'ix-checkbox-group',
Expand Down Expand Up @@ -961,7 +963,7 @@
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['ariaLabelCloseButton', 'closeOnClickOutside', 'fullHeight', 'maxWidth', 'minWidth', 'show', 'width'],
outputs: ['open', 'drawerClose'],

Check failure on line 966 in packages/angular/standalone/src/components.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `@Output` rather than the `outputs` metadata property

See more on https://sonarcloud.io/project/issues?id=siemens_ix&issues=AZ5LkRB8iEJNhEcQ-qoe&open=AZ5LkRB8iEJNhEcQ-qoe&pullRequest=2550
})
export class IxDrawer {
protected el: HTMLIxDrawerElement;
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ export namespace Components {
* @default false
*/
"checked": boolean;
/**
* Resets the checkbox to its initial unchecked state and clears validation.
*/
"clear": () => Promise<void>;
/**
* Disabled state of the checkbox component
* @default false
Expand Down Expand Up @@ -686,6 +690,7 @@ export namespace Components {
* @form-ready
*/
interface IxCheckboxGroup {
"clear": () => Promise<void>;
/**
* Alignment of the checkboxes in the group
* @default 'column'
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/components/checkbox-group/checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../utils/input';
import { IxComponentInterface } from '../utils/internal';
import { makeRef } from '../utils/make-ref';
import { clearCheckboxGroupValidationState } from '../checkbox/checkbox-validation';

/**
* @form-ready
Expand Down Expand Up @@ -144,6 +145,19 @@ export class CheckboxGroup
);
}

@Method()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The new clear method is missing JSDoc documentation and the @SInCE tag required by the repository style guide.

  /**
   * Resets the checkbox group to its initial state and clears validation.
   *
   * @since 2.2.0
   */
  @Method()
References
  1. Ensure that each new component, property, method, or event has a JSDocs which contains a @SInCE tag with the version number of the release in which it was added. (link)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer please confirm if we need to add '@SInCE version' for @method() ?

async clear(): Promise<void> {
this.touched = false;
this.checkboxElements.forEach((checkbox) => {
checkbox.clear();
});
this.clearValidationState();
}

private clearValidationState() {
clearCheckboxGroupValidationState(this.hostElement, this.checkboxElements);
}

render() {
return (
<Host ref={this.groupRef} onIxBlur={() => (this.touched = true)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ regressionTest('renders', async ({ mount, page }) => {
await expect(radioOption3).toHaveClass(/hydrated/);
});

regressionTest(
'should apply invalid class to checkbox-group when required checkbox is touched',
async ({ mount, page }) => {
await mount(`
<form>
<ix-checkbox-group>
<ix-checkbox label="Option 1" value="option1" required></ix-checkbox>
<ix-checkbox label="Option 2" value="option2"></ix-checkbox>
</ix-checkbox-group>
<button type="button">Other</button>
</form>
`);

const checkboxGroup = page.locator('ix-checkbox-group');
const checkbox1 = page.locator('ix-checkbox').nth(0);
await expect(checkbox1).toHaveClass(/\bhydrated\b/);

await checkbox1.focus();
await page.locator('button[type="button"]').focus();

await expect(checkboxGroup).toHaveClass(/\bix-invalid--required\b/);
await expect(checkboxGroup).toHaveClass(/\bix-invalid\b/);
}
);

regressionTest('required', async ({ mount, page }) => {
await mount(
`
Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/components/checkbox/checkbox-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
๏ปฟ/*
* SPDX-FileCopyrightText: 2024 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

interface CheckboxElement extends HTMLElement {
required: boolean;
checked: boolean;
}

export function isFormNoValidate(element: HTMLElement): boolean {
const form = element.closest('form');
if (!form) return false;
return (
form.hasAttribute('novalidate') ||
form.dataset.novalidate !== undefined ||
form.hasAttribute('ngnovalidate')
);
}

export function setupFormSubmitListener(
element: HTMLElement,
callback: () => void
): (() => void) | undefined {
const form = element.closest('form');
if (!form) return undefined;

const handler = () => callback();
form.addEventListener('submit', handler);

return () => {
form.removeEventListener('submit', handler);
};
}

export function applyCheckboxValidation(
checkboxes: NodeListOf<CheckboxElement> | CheckboxElement[],
group: Element | null,
touched: boolean,
formSubmissionAttempted: boolean
) {
const checkboxArr = Array.from(checkboxes);
const requiredCheckboxes = checkboxArr.filter((el) => el.required);
const targets =
requiredCheckboxes.length > 0 ? requiredCheckboxes : checkboxArr;

const anyChecked = targets.some((el) => el.checked);
const shouldShowError = !anyChecked && (touched || formSubmissionAttempted);

checkboxArr.forEach((el) => {
const isTarget = targets.includes(el);
el.classList.toggle('ix-invalid--required', isTarget && shouldShowError);
el.classList.toggle('ix-invalid', isTarget && shouldShowError);
});

if (group) {
group.classList.toggle('ix-invalid--required', shouldShowError);
group.classList.toggle('ix-invalid', shouldShowError);
}
}

export function clearCheckboxGroupValidationState(
group: HTMLElement,
checkboxes: CheckboxElement[] | NodeListOf<CheckboxElement>
) {
const checkboxArr = Array.from(checkboxes);
group.classList.remove('ix-invalid--required', 'ix-invalid');
checkboxArr.forEach((el) => {
el.classList.remove('ix-invalid', 'ix-invalid--required');
});
}
77 changes: 75 additions & 2 deletions packages/core/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ import {
import { a11yBoolean } from '../utils/a11y';
import { HookValidationLifecycle, IxFormComponent } from '../utils/input';
import { makeRef } from '../utils/make-ref';
import {
isFormNoValidate,
applyCheckboxValidation,
setupFormSubmitListener,
} from './checkbox-validation';

/**
* @form-ready
*/
@Component({
tag: 'ix-checkbox',
styleUrl: 'checkbox.scss',
shadow: true,
shadow: { delegatesFocus: true },
formAssociated: true,
})
export class Checkbox implements IxFormComponent<string> {
Expand Down Expand Up @@ -91,6 +96,8 @@ export class Checkbox implements IxFormComponent<string> {
@Event() ixBlur!: EventEmitter<void>;

private touched = false;
private formSubmissionAttempted = false;
private cleanupFormListener?: () => void;

private readonly inputRef = makeRef<HTMLInputElement>((checkboxRef) => {
checkboxRef.checked = this.checked;
Expand All @@ -105,11 +112,61 @@ export class Checkbox implements IxFormComponent<string> {
onCheckedChange() {
this.touched = true;
this.updateFormInternalValue();
this.syncValidationClasses();
}

private syncValidationClasses() {
if (isFormNoValidate(this.hostElement)) {
this.hostElement.classList.remove('ix-invalid--required', 'ix-invalid');
return;
}

const checkboxGroup = this.hostElement.closest('ix-checkbox-group');
let checkboxes: HTMLIxCheckboxElement[];
let group: Element | null;

if (checkboxGroup) {
checkboxes = Array.from(
checkboxGroup.querySelectorAll<HTMLIxCheckboxElement>('ix-checkbox')
);
group = checkboxGroup;
} else {
checkboxes = [this.hostElement];
group = null;
}

const hasRequired = checkboxes.some((c) => c.required);
if (!hasRequired) {
this.hostElement.classList.remove('ix-invalid--required', 'ix-invalid');
return;
}

applyCheckboxValidation(
checkboxes,
group,
this.touched,
this.formSubmissionAttempted
);
}

@Watch('value')
onValueChange() {
this.valueChange.emit(this.value);
this.syncValidationClasses();
}

connectedCallback(): void {
this.syncValidationClasses();
this.cleanupFormListener = setupFormSubmitListener(this.hostElement, () => {
this.formSubmissionAttempted = true;
this.syncValidationClasses();
});
}

disconnectedCallback(): void {
if (this.cleanupFormListener) {
this.cleanupFormListener();
}
}

componentWillLoad() {
Expand Down Expand Up @@ -147,6 +204,18 @@ export class Checkbox implements IxFormComponent<string> {
/** This function is intentionally empty */
}

/**
* Resets the checkbox to its initial unchecked state and clears validation.
*/
Comment on lines +207 to +209

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSDoc for the new clear method is missing the @SInCE tag required by the repository style guide.

  /**
   * Resets the checkbox to its initial unchecked state and clears validation.
   *
   * @since 2.2.0
   */
References
  1. Ensure that each new component, property, method, or event has a JSDocs which contains a @SInCE tag with the version number of the release in which it was added. (link)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer please confirm if we need to add '@SInCE version' for @method() ?

@Method()
async clear(): Promise<void> {
this.checked = false;
this.touched = false;
this.formSubmissionAttempted = false;
this.updateFormInternalValue();
this.syncValidationClasses();
}

private renderCheckmark() {
return (
<svg
Expand Down Expand Up @@ -192,7 +261,11 @@ export class Checkbox implements IxFormComponent<string> {
indeterminate: this.indeterminate,
}}
onFocus={() => (this.touched = true)}
onBlur={() => this.ixBlur.emit()}
onBlur={() => {
this.ixBlur.emit();
this.touched = true;
this.syncValidationClasses();
}}
>
<label>
<input
Expand Down
Loading
Loading