Skip to content

Commit

Permalink
feat(radio): add validityState
Browse files Browse the repository at this point in the history
  • Loading branch information
aesteves60 authored and dpellier committed Nov 28, 2024
1 parent 0c3ff26 commit f11ed4e
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 16 deletions.
4 changes: 2 additions & 2 deletions packages/ods/src/components/radio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"start": "stencil build --dev --watch --serve",
"test:e2e": "stencil test --e2e --config stencil.config.ts --max-workers=2",
"test:e2e:ci": "tsc --noEmit && stencil test --e2e --ci --runInBand --config stencil.config.ts",
"test:spec": "stencil test --spec --config stencil.config.ts --coverage",
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci"
"test:spec": "stencil test --spec --config stencil.config.ts --coverage --passWithNoTests",
"test:spec:ci": "tsc --noEmit && stencil test --config stencil.config.ts --spec --ci --passWithNoTests"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@

&__radio {
@include radio.ods-radio();

&--error {
border-color: var(--ods-color-form-element-border-critical);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AttachInternals, Component, Element, Event, type EventEmitter, type FunctionalComponent, Host, Method, Prop, h } from '@stencil/core';
import { Component, Element, Event, type EventEmitter, type FunctionalComponent, Host, Method, Prop, State, h } from '@stencil/core';
import { submitFormOnEnter } from '../../../../../utils/dom';
import { type OdsRadioChangeEventDetail } from '../../interfaces/events';

Expand All @@ -13,7 +13,7 @@ export class OdsRadio {

@Element() el!: HTMLElement;

@AttachInternals() private internals!: ElementInternals;
@State() private isInvalid: boolean = false;

@Prop({ reflect: true }) public ariaLabel: HTMLElement['ariaLabel'] = null;
@Prop({ reflect: true }) public ariaLabelledby?: string;
Expand All @@ -30,6 +30,12 @@ export class OdsRadio {
@Event() odsFocus!: EventEmitter<void>;
@Event() odsReset!: EventEmitter<void>;

@Method()
public async checkValidity(): Promise<boolean | undefined> {
this.isInvalid = !this.inputEl?.validity.valid;
return this.inputEl?.checkValidity();
}

@Method()
public async clear(): Promise<void> {
if (this.inputEl) {
Expand All @@ -45,6 +51,11 @@ export class OdsRadio {
this.inputEl?.focus();
}

@Method()
public async getValidationMessage(): Promise<string | undefined> {
return this.inputEl?.validationMessage;
}

@Method()
public async getValidity(): Promise<ValidityState | undefined> {
return this.inputEl?.validity;
Expand Down Expand Up @@ -72,11 +83,22 @@ export class OdsRadio {
});
}

@Method()
public async reportValidity(): Promise<boolean | undefined> {
this.isInvalid = !this.inputEl?.validity.valid;
return this.inputEl?.reportValidity();
}

@Method()
public async select(): Promise<void> {
this.inputEl?.click();
}

@Method()
public async willValidate(): Promise<boolean | undefined> {
return this.inputEl?.willValidate;
}

async formResetCallback(): Promise<void> {
await this.reset();
}
Expand All @@ -94,6 +116,11 @@ export class OdsRadio {
return document.querySelectorAll(`ods-radio[name="${this.name}"]`);
}

private onBlur(): void {
this.isInvalid = !this.inputEl?.validity.valid;
this.odsBlur.emit();
}

private onInput(event: Event): void {
this.emitChange({
checked: (event.target as HTMLInputElement)?.checked,
Expand All @@ -103,21 +130,35 @@ export class OdsRadio {
});
}

private onInvalidEvent(event: Event): void {
// Remove the native validation message popup
event.preventDefault();
event.stopPropagation();

// Enforce the state here as we may still be in pristine state (if the form is submitted before any changes occurs)
this.isInvalid = true;
}

render(): FunctionalComponent {
return (
<Host class="ods-radio">
<Host class="ods-radio"
disabled={ this.isDisabled }>
<input
aria-label={ this.ariaLabel }
aria-labelledby={ this.ariaLabelledby }
class="ods-radio__radio"
class={{
'ods-radio__radio': true,
'ods-radio__radio--error': this.isInvalid,
}}
checked={ this.isChecked }
disabled={ this.isDisabled }
id={ this.inputId }
name={ this.name }
onBlur={ (): CustomEvent<void> => this.odsBlur.emit() }
onBlur={ (): void => this.onBlur() }
onFocus={ (): CustomEvent<void> => this.odsFocus.emit() }
onInput={ (event: InputEvent): void => this.onInput(event) }
onKeyUp={ (event: KeyboardEvent): void => submitFormOnEnter(event, this.internals.form) }
onInvalid={ (e): void => this.onInvalidEvent(e) }
onKeyUp={ (event: KeyboardEvent): void => this.inputEl && submitFormOnEnter(event, this.inputEl.form) }
ref={ (el): HTMLInputElement => this.inputEl = el as HTMLInputElement }
required={ this.isRequired }
type="radio"
Expand Down
41 changes: 41 additions & 0 deletions packages/ods/src/components/radio/tests/rendering/ods-radio.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { E2EElement, E2EPage } from '@stencil/core/testing';
import { newE2EPage } from '@stencil/core/testing';

describe('ods-radio rendering', () => {
let el: E2EElement;
let page: E2EPage;
let inputRadio: E2EElement;

Expand All @@ -15,6 +16,7 @@ describe('ods-radio rendering', () => {
await page.addStyleTag({ content: customStyle });
}

el = await page.find('ods-radio');
inputRadio = await page.find('ods-radio > input[type="radio"]');
}

Expand All @@ -25,4 +27,43 @@ describe('ods-radio rendering', () => {
expect(inputRadioStyle.getPropertyValue('background-color')).toBe('rgb(255, 0, 0)');
});
});

describe('error state', () => {
it('should render in error on form submit, before any changes, if invalid', async() => {
await setup('<form method="get" onsubmit="return false"><ods-radio name="error-state" is-required></ods-radio></form>');

await page.evaluate(() => {
document.querySelector<HTMLFormElement>('form')?.requestSubmit();
});
await page.waitForChanges();

const hasErrorClass = await page.evaluate(() => {
return document.querySelector('ods-radio')?.querySelector('input')?.classList.contains('ods-radio__radio--error');
});
expect(hasErrorClass).toBe(true);
});

it('should toggle the error state on value change', async() => {
await setup('<form method="get" onsubmit="return false"><ods-radio name="error-state" is-required></ods-radio></form>');

await el.click();
await page.waitForChanges();

const hasErrorClass = await page.evaluate(() => {
return document.querySelector('ods-radio')?.querySelector('input')?.classList.contains('ods-radio__radio--error');
});
expect(hasErrorClass).toBe(false);

await el.callMethod('clear');
await page.click('body', { offset: { x: 200, y: 200 } }); // Blur
await page.waitForChanges();

const hasErrorClass2 = await page.evaluate(() => {
return document.querySelector('ods-radio')?.querySelector('input')?.classList.contains('ods-radio__radio--error');
});
await page.waitForChanges();

expect(hasErrorClass2).toBe(true);
});
});
});
Loading

0 comments on commit f11ed4e

Please sign in to comment.