diff --git a/packages/examples/react-webpack/src/app/App.tsx b/packages/examples/react-webpack/src/app/App.tsx index 0185d535a1..fcb87c49f9 100644 --- a/packages/examples/react-webpack/src/app/App.tsx +++ b/packages/examples/react-webpack/src/app/App.tsx @@ -1,15 +1,15 @@ import React, { type ReactElement } from 'react'; -import { Gallery } from './components/gallery/Gallery'; +// import { Gallery } from './components/gallery/Gallery'; // import { FormFormik } from './components/formFormik/FormFormik'; -// import { FormNative } from './components/formNative/FormNative'; import styles from './app.scss'; +import { FormNative } from './components/formNative/FormNative'; function App(): ReactElement { return (
- + {/* */} {/**/} - {/**/} +
); } diff --git a/packages/examples/react-webpack/src/app/components/formNative/FormNative.tsx b/packages/examples/react-webpack/src/app/components/formNative/FormNative.tsx index 5a448dd9bb..48e02011d2 100644 --- a/packages/examples/react-webpack/src/app/components/formNative/FormNative.tsx +++ b/packages/examples/react-webpack/src/app/components/formNative/FormNative.tsx @@ -4,7 +4,7 @@ import React, { type ReactElement, useRef, useState } from 'react'; import styles from './formNative.scss'; function FormNative(): ReactElement { - // const checkboxRef = useRef(null); + const checkboxRef = useRef(null); const datepickerRef = useRef(null); const formRef = useRef(null); const inputNumberRef = useRef(null); @@ -12,7 +12,7 @@ function FormNative(): ReactElement { const passwordRef = useRef(null); const phoneNumberRef = useRef(null); const quantityRef = useRef(null); - // const radioRef = useRef(null); + const radioRef = useRef(null); const selectRef = useRef(null); const switchRef = useRef(null); const textareaRef = useRef(null); @@ -48,21 +48,21 @@ function FormNative(): ReactElement { // await validateField(selectRef.current); // await validateField(switchRef.current); // await validateField(textareaRef.current); - await validateField(timepickerRef.current); + // await validateField(timepickerRef.current); const formData = new FormData(formRef.current!); - console.log(formData) + console.log(formData); return false; } - async function validateField(element: any) { + async function validateField(element: any): Promise { if (!element) { return; } const validity = await element.getValidity(); -// console.log(validity) + // console.log(validity) if (validity !== undefined) { setError({ ...error, @@ -71,6 +71,18 @@ function FormNative(): ReactElement { } } + function onChange(event: CustomEvent) { + console.log('onChange', event); + } + + function onReset(event: CustomEvent): void { + console.log('onReset', event); + } + + function onClear(event: CustomEvent): void { + console.log('onClear', event); + } + return (
@@ -113,21 +126,20 @@ function FormNative(): ReactElement { defaultValue={ 23 } hasError={ error.inputNumber } isClearable={ true } - isRequired={ true } + // isRequired={ true } name="inputNumber" - onOdsChange={ () => validateField(inputNumberRef.current) } ref={ inputNumberRef } type={ ODS_INPUT_TYPE.number } /> {/* OK */} validateField(inputTextRef.current) } ref={ inputTextRef } type={ ODS_INPUT_TYPE.text } /> @@ -139,30 +151,27 @@ function FormNative(): ReactElement { isClearable={ true } isRequired={ true } name="password" - onOdsChange={ () => validateField(passwordRef.current) } ref={ passwordRef } /> {/* KO default value not in formData on immediate submit */} {/* KO style width different */} validateField(phoneNumberRef.current) } ref={ phoneNumberRef } /> {/* KO reset does not reset formData if no default value */} validateField(quantityRef.current) } ref={ quantityRef } /> @@ -172,7 +181,10 @@ function FormNative(): ReactElement { // isRequired={ true } inputId="radio1" name="radio" + isChecked value="radio-1" + ref={ radioRef } + /> @@ -192,6 +204,9 @@ function FormNative(): ReactElement { hasError={ error.select } isRequired={ true } name="select" + onOdsChange={ (event) => onChange(event) } + onOdsReset={ (event) => onReset(event) } + onOdsClear={ (event) => onClear(event) } ref={ selectRef } > @@ -202,14 +217,18 @@ function FormNative(): ReactElement { + + {/* KO isRequired => no way to get validity */} {/* KO should have an hasError status */} + {/* Two event change on reset & clear */} @@ -229,18 +248,16 @@ function FormNative(): ReactElement { hasError={ error.textarea } isRequired={ true } name="textarea" - onOdsChange={ () => validateField(textareaRef.current) } ref={ textareaRef } /> {/* KO value not in formData when no default */} {/* KO value in formData not updated when default */} validateField(timepickerRef.current) } ref={ timepickerRef } /> diff --git a/packages/ods/src/components/checkbox/src/components/ods-checkbox/ods-checkbox.tsx b/packages/ods/src/components/checkbox/src/components/ods-checkbox/ods-checkbox.tsx index fb6ca4c880..105a66d4d4 100644 --- a/packages/ods/src/components/checkbox/src/components/ods-checkbox/ods-checkbox.tsx +++ b/packages/ods/src/components/checkbox/src/components/ods-checkbox/ods-checkbox.tsx @@ -34,6 +34,7 @@ export class OdsCheckbox { } this.odsClear.emit(); hasChange && this.onInput(); + this.inputEl?.focus(); } @Method() diff --git a/packages/ods/src/components/input/src/components/ods-input/ods-input.tsx b/packages/ods/src/components/input/src/components/ods-input/ods-input.tsx index dbde72b44a..7bb36c4c10 100644 --- a/packages/ods/src/components/input/src/components/ods-input/ods-input.tsx +++ b/packages/ods/src/components/input/src/components/ods-input/ods-input.tsx @@ -51,9 +51,9 @@ export class OdsInput { @Method() async clear(): Promise { + this.odsClear.emit(); this.value = null; this.inputEl?.focus(); - this.odsClear.emit(); } @Method() @@ -69,8 +69,8 @@ export class OdsInput { @Method() async reset(): Promise { - this.value = this.defaultValue ?? null; this.odsReset.emit(); + this.value = this.defaultValue ?? null; } @Watch('isMasked') diff --git a/packages/ods/src/components/quantity/src/components/ods-quantity/ods-quantity.tsx b/packages/ods/src/components/quantity/src/components/ods-quantity/ods-quantity.tsx index 1ae588f848..b5a8da5217 100644 --- a/packages/ods/src/components/quantity/src/components/ods-quantity/ods-quantity.tsx +++ b/packages/ods/src/components/quantity/src/components/ods-quantity/ods-quantity.tsx @@ -81,9 +81,9 @@ export class OdsQuantity { private onOdsChange(event: OdsInputValueChangeEvent): void { if (event.detail.value === null) { this.value = null; - return; + } else { + this.value = Number(event.detail.value) ?? null; } - this.value = Number(event.detail.value) ?? null; setFormValue(this.internals, this.value); } diff --git a/packages/ods/src/components/quantity/src/interfaces/events.ts b/packages/ods/src/components/quantity/src/interfaces/events.ts index 9728881238..acea2d3f17 100644 --- a/packages/ods/src/components/quantity/src/interfaces/events.ts +++ b/packages/ods/src/components/quantity/src/interfaces/events.ts @@ -2,7 +2,7 @@ interface OdsQuantityEventValueChangeDetail { name: string; previousValue?: number; validity?: ValidityState; - value: number; + value: number | null; } type OdsQuantityEventValueChange = CustomEvent; diff --git a/packages/ods/src/components/quantity/tests/behaviour/ods-quantity.e2e.ts b/packages/ods/src/components/quantity/tests/behaviour/ods-quantity.e2e.ts index 40c2139123..6bfea06c6a 100644 --- a/packages/ods/src/components/quantity/tests/behaviour/ods-quantity.e2e.ts +++ b/packages/ods/src/components/quantity/tests/behaviour/ods-quantity.e2e.ts @@ -222,7 +222,7 @@ describe('ods-quantity behaviour', () => { await submitButton.click(); await page.waitForNetworkIdle(); const url = new URL(page.url()); - expect(url.searchParams.get('ods-quantity')).toBe('0'); + expect(url.searchParams.get('ods-quantity')).toBe(''); }); }); }); diff --git a/packages/ods/src/components/radio/src/components/ods-radio/ods-radio.tsx b/packages/ods/src/components/radio/src/components/ods-radio/ods-radio.tsx index 40dfe3f15b..7b93f0e045 100644 --- a/packages/ods/src/components/radio/src/components/ods-radio/ods-radio.tsx +++ b/packages/ods/src/components/radio/src/components/ods-radio/ods-radio.tsx @@ -33,6 +33,13 @@ export class OdsRadio { this.inputEl.checked = false; } this.odsClear.emit(); + this.emitChange({ + checked: false, + name: this.name, + validity: this.inputEl?.validity, + value: this.value ?? null, + }); + this.inputEl?.focus(); } @Method() @@ -47,13 +54,19 @@ export class OdsRadio { if (!inputRadio) { return; } - if (radio.getAttribute('is-checked') === '') { + if (radio.getAttribute('is-checked') !== null) { inputRadio.checked = true; } else { inputRadio.checked = false; } }); this.odsReset.emit(); + this.emitChange({ + checked: this.isChecked, + name: this.name, + validity: this.inputEl?.validity, + value: this.value ?? null, + }); } @Method() @@ -65,8 +78,8 @@ export class OdsRadio { return document.querySelectorAll(`ods-radio[name="${this.name}"]`); } - private onInput(event: InputEvent): void { - this.odsChange.emit({ + private onInput(event: Event): void { + this.emitChange({ checked: (event.target as HTMLInputElement)?.checked, name: this.name, validity: this.inputEl?.validity, @@ -74,6 +87,15 @@ export class OdsRadio { }); } + private emitChange(detail: OdsRadioValueChangeEventDetail): void { + this.odsChange.emit({ + checked: detail.checked, + name: detail.name, + validity: detail.validity, + value: detail.value, + }); + } + render(): FunctionalComponent { return ( diff --git a/packages/ods/src/components/radio/tests/behaviour/ods-radio.e2e.ts b/packages/ods/src/components/radio/tests/behaviour/ods-radio.e2e.ts index d938cf2a60..c5e58cdddd 100644 --- a/packages/ods/src/components/radio/tests/behaviour/ods-radio.e2e.ts +++ b/packages/ods/src/components/radio/tests/behaviour/ods-radio.e2e.ts @@ -26,27 +26,43 @@ describe('ods-radio behaviour', () => { describe('Methods', () => { describe('method:clear', () => { it('should receive odsClear event', async() => { - await setup(''); + await setup(''); const odsClearSpy = await page.spyOnEvent('odsClear'); + const odsChangeSpy = await page.spyOnEvent('odsChange'); expect(await isInputRadioChecked(el)).toBe(true); await el.callMethod('clear'); await page.waitForChanges(); expect(await isInputRadioChecked(el)).toBe(false); expect(odsClearSpy).toHaveReceivedEventTimes(1); + expect(odsChangeSpy).toHaveReceivedEventTimes(1); + expect(odsChangeSpy).toHaveReceivedEventDetail({ + checked: false, + name: 'ods-radio', + validity: {}, + value: 'value', + }); }); }); describe('method:reset', () => { it('should receive odsReset event', async() => { - await setup(''); + await setup(''); const odsResetSpy = await page.spyOnEvent('odsReset'); + const odsChangeSpy = await page.spyOnEvent('odsChange'); expect(await isInputRadioChecked(el)).toBe(true); await el.callMethod('reset'); await page.waitForChanges(); expect(await isInputRadioChecked(el)).toBe(true); expect(odsResetSpy).toHaveReceivedEventTimes(1); + expect(odsChangeSpy).toHaveReceivedEventTimes(1); + expect(odsChangeSpy).toHaveReceivedEventDetail({ + checked: true, + name: 'ods-radio', + validity: {}, + value: 'value', + }); }); it('should checked the radio with is-checked after reset', async() => { @@ -55,6 +71,8 @@ describe('ods-radio behaviour', () => { `); const radios = await page.findAll('ods-radio'); const odsResetSpy = await page.spyOnEvent('odsReset'); + const odsChangeSpy = await page.spyOnEvent('odsChange'); + expect(await isInputRadioChecked(radios[0])).toBe(true); await radios[2].click(); @@ -64,6 +82,13 @@ describe('ods-radio behaviour', () => { expect(await isInputRadioChecked(radios[0])).toBe(true); expect(odsResetSpy).toHaveReceivedEventTimes(1); + expect(odsChangeSpy).toHaveReceivedEventTimes(2); + expect(odsChangeSpy).toHaveReceivedEventDetail({ + checked: false, + name: 'radio-group', + validity: {}, + value: 'value3', + }); }); }); diff --git a/packages/ods/src/components/select/src/components/ods-select/ods-select.tsx b/packages/ods/src/components/select/src/components/ods-select/ods-select.tsx index 3699c9bde4..6885c32931 100644 --- a/packages/ods/src/components/select/src/components/ods-select/ods-select.tsx +++ b/packages/ods/src/components/select/src/components/ods-select/ods-select.tsx @@ -18,6 +18,7 @@ TomSelect.define('placeholder', placeholderPlugin); export class OdsSelect { private hasMovedNodes: boolean = false; private isSelectSync: boolean = false; + private isValueSync: boolean = false; private observer!: MutationObserver; private select?: TomSelect; private selectElement?: HTMLSelectElement; @@ -52,8 +53,12 @@ export class OdsSelect { @Method() async clear(): Promise { - this.value = null; this.odsClear.emit(); + if (this.value !== null) { + this.isValueSync = true; + } + this.value = null; + this.select?.focus(); } @Method() @@ -73,8 +78,11 @@ export class OdsSelect { @Method() async reset(): Promise { - this.updateValue(this.defaultValue ?? null); this.odsReset.emit(); + if (this.value !== (this.defaultValue ?? null)) { + this.isValueSync = true; + } + this.updateValue(this.defaultValue ?? null); } @Watch('isDisabled') @@ -104,7 +112,7 @@ export class OdsSelect { } @Watch('value') - onValueChange(value: string | string[], previousValue?: string | string[]): void { + onValueChange(value: string | string[] | null, previousValue?: string | string[]): void { // Value change can be triggered from either value attribute change or select change // For the latter, we don't want to trigger a new change (as it may causes loop) if (!this.isSelectSync) { @@ -116,7 +124,7 @@ export class OdsSelect { this.odsChange.emit({ name: this.name, - previousValue: inlineValue(previousValue), + previousValue: inlineValue(previousValue) ?? undefined, validity: this.selectElement?.validity, value: inlineValue(value), }); @@ -221,8 +229,11 @@ export class OdsSelect { this.odsBlur.emit(); }, onChange: (value: string | string[]): void => { - this.isSelectSync = true; - this.updateValue(value); + if (!this.isValueSync) { + this.isSelectSync = true; + this.updateValue(value); + } + this.isValueSync = false; }, onDropdownClose: (dropdown: HTMLDivElement): void => { dropdown.classList.remove('ods-select__dropdown--bottom', 'ods-select__dropdown--top'); diff --git a/packages/ods/src/components/select/src/controller/ods-select.ts b/packages/ods/src/components/select/src/controller/ods-select.ts index cc69f31263..40197ada91 100644 --- a/packages/ods/src/components/select/src/controller/ods-select.ts +++ b/packages/ods/src/components/select/src/controller/ods-select.ts @@ -23,11 +23,11 @@ function getSelectConfig(allowMultiple: boolean, multipleSelectionLabel: string, return { plugin, template }; } -function inlineValue(value: string | string[] | null | undefined): string { +function inlineValue(value: string | string[] | null | undefined): string | null { if (Array.isArray(value)) { return value.join(','); } - return value || ''; + return value ?? null; } function moveSlottedElements(targetElement: HTMLSelectElement, slottedElements: Element[]): void { diff --git a/packages/ods/src/components/select/src/interfaces/events.ts b/packages/ods/src/components/select/src/interfaces/events.ts index cd0f529f11..885522bfc9 100644 --- a/packages/ods/src/components/select/src/interfaces/events.ts +++ b/packages/ods/src/components/select/src/interfaces/events.ts @@ -2,7 +2,7 @@ interface OdsSelectEventChangeDetail { name: string; previousValue?: string; validity?: ValidityState; - value: string; + value: string | null; } type OdsSelectEventChange = CustomEvent; diff --git a/packages/ods/src/components/switch/src/components/ods-switch/ods-switch.tsx b/packages/ods/src/components/switch/src/components/ods-switch/ods-switch.tsx index bed30cbc96..edcb881778 100644 --- a/packages/ods/src/components/switch/src/components/ods-switch/ods-switch.tsx +++ b/packages/ods/src/components/switch/src/components/ods-switch/ods-switch.tsx @@ -25,14 +25,15 @@ export class OdsSwitch { @Method() async clear(): Promise { - await clearItems(Array.from(this.el.children)); this.odsClear.emit(); + await clearItems(Array.from(this.el.children)); + this.el.focus(); } @Method() async reset(): Promise { - await resetItems(Array.from(this.el.children)); this.odsReset.emit(); + await resetItems(Array.from(this.el.children)); } @Listen('odsSwitchItemFocus') diff --git a/packages/ods/src/components/textarea/src/components/ods-textarea/ods-textarea.tsx b/packages/ods/src/components/textarea/src/components/ods-textarea/ods-textarea.tsx index ee12dda81a..c2500057b8 100644 --- a/packages/ods/src/components/textarea/src/components/ods-textarea/ods-textarea.tsx +++ b/packages/ods/src/components/textarea/src/components/ods-textarea/ods-textarea.tsx @@ -36,8 +36,9 @@ export class OdsTextarea { @Method() async clear(): Promise { - this.value = null; this.odsClear.emit(); + this.value = null; + this.textareaElement?.focus(); } @Method() @@ -47,8 +48,8 @@ export class OdsTextarea { @Method() async reset(): Promise { - this.value = this.defaultValue ?? null; this.odsReset.emit(); + this.value = this.defaultValue ?? null; } @Watch('value') @@ -59,7 +60,7 @@ export class OdsTextarea { name: this.name, previousValue, validity: this.textareaElement?.validity, - value: value ?? '', + value: value ?? null, }); } diff --git a/packages/ods/src/components/textarea/src/interfaces/events.ts b/packages/ods/src/components/textarea/src/interfaces/events.ts index 93fb2ddded..8d128e8a62 100644 --- a/packages/ods/src/components/textarea/src/interfaces/events.ts +++ b/packages/ods/src/components/textarea/src/interfaces/events.ts @@ -2,7 +2,7 @@ interface OdsTextareaEventChangeDetail { name: string; previousValue?: string; validity?: ValidityState; - value: string; + value: string | null; } type OdsTextareaEventChange = CustomEvent; diff --git a/packages/ods/src/components/timepicker/src/components/ods-timepicker/ods-timepicker.tsx b/packages/ods/src/components/timepicker/src/components/ods-timepicker/ods-timepicker.tsx index 057fcb5b64..4a03711c65 100644 --- a/packages/ods/src/components/timepicker/src/components/ods-timepicker/ods-timepicker.tsx +++ b/packages/ods/src/components/timepicker/src/components/ods-timepicker/ods-timepicker.tsx @@ -119,7 +119,7 @@ export class OdsTimepicker { name: this.name, previousValue: this.previousValue ?? undefined, validity: await this.odsInput?.getValidity(), - value: this.value ?? '', + value: this.value ?? null, }); } diff --git a/packages/ods/src/components/timepicker/src/interfaces/event.ts b/packages/ods/src/components/timepicker/src/interfaces/event.ts index 97003e6140..71baf3fbfd 100644 --- a/packages/ods/src/components/timepicker/src/interfaces/event.ts +++ b/packages/ods/src/components/timepicker/src/interfaces/event.ts @@ -5,7 +5,7 @@ interface OdsTimepickerValueChangeEventDetail { name: string; previousValue?: string; validity?: ValidityState; - value: string; + value: string | null; } type OdsTimepickerValueChangeEvent = CustomEvent; diff --git a/packages/ods/src/style/_checkbox.scss b/packages/ods/src/style/_checkbox.scss index 6fa0e1642a..c74fe540b5 100644 --- a/packages/ods/src/style/_checkbox.scss +++ b/packages/ods/src/style/_checkbox.scss @@ -25,7 +25,7 @@ $ods-checkbox-size: 16px; font-size: 0.5rem; } - &:focus-visible { + &:focus-visible, &:focus { @include focus.ods-focus(); } diff --git a/packages/ods/src/style/_radio.scss b/packages/ods/src/style/_radio.scss index 601905127e..073164cc90 100644 --- a/packages/ods/src/style/_radio.scss +++ b/packages/ods/src/style/_radio.scss @@ -21,7 +21,7 @@ content: ""; } - &:focus-visible { + &:focus-visible, &:focus { @include focus.ods-focus(); }