diff --git a/.changeset/dbcustomselect-form-reset.md b/.changeset/dbcustomselect-form-reset.md new file mode 100644 index 000000000000..ba3e49f9b045 --- /dev/null +++ b/.changeset/dbcustomselect-form-reset.md @@ -0,0 +1,14 @@ +--- +"@db-ux/core-components": patch +"@db-ux/ngx-core-components": patch +"@db-ux/react-core-components": patch +"@db-ux/v-core-components": patch +"@db-ux/wc-core-components": patch +--- + +fix(DBCustomSelect): automatically handle form reset events + +An event listener is now added for every form component (input, custom-select, etc.) when a `form` property is passed. +This listener detects form resets and updates the component's internal value/checked state accordingly. + +> **Note**: This does not work for `ngModel` in Angular. diff --git a/__snapshots__/radio/patternhub/radio-properties-should-match-screenshot.png b/__snapshots__/radio/patternhub/radio-properties-should-match-screenshot.png index 8f371cba5f41..b393686fe217 100644 Binary files a/__snapshots__/radio/patternhub/radio-properties-should-match-screenshot.png and b/__snapshots__/radio/patternhub/radio-properties-should-match-screenshot.png differ diff --git a/packages/components/scripts/post-build/components.ts b/packages/components/scripts/post-build/components.ts index 7962afbe98b1..9177fff99b1d 100644 --- a/packages/components/scripts/post-build/components.ts +++ b/packages/components/scripts/post-build/components.ts @@ -178,6 +178,12 @@ export const getComponents = (): Component[] => [ to: '{{value()}}' } ], + vue: [ + { + from: '', + to: '{{value}}' + } + ], react: [{ from: /HTMLAttributes/g, to: 'TextareaHTMLAttributes' }], stencil: [{ from: 'HTMLElement', to: 'HTMLTextAreaElement' }] } @@ -214,9 +220,7 @@ export const getComponents = (): Component[] => [ { name: 'select', overwrites: { - angular: [ - { from: '', to: '' } - ], + angular: [{ from: '', to: '' }], react: [ // React not allowing selected for options { from: 'selected={option.selected}', to: '' }, diff --git a/packages/components/src/components/checkbox/checkbox.lite.tsx b/packages/components/src/components/checkbox/checkbox.lite.tsx index 96a9a26b130f..8ae89a8d0c6f 100644 --- a/packages/components/src/components/checkbox/checkbox.lite.tsx +++ b/packages/components/src/components/checkbox/checkbox.lite.tsx @@ -1,5 +1,6 @@ import { onMount, + onUnMount, onUpdate, Show, useDefaultProps, @@ -26,6 +27,7 @@ import { uuid } from '../../utils'; import { + addCheckedResetEventListener, handleFrameworkEventAngular, handleFrameworkEventVue } from '../../utils/form-components'; @@ -55,6 +57,7 @@ export default function DBCheckbox(props: DBCheckboxProps) { _invalidMessage: undefined, _descByIds: undefined, _voiceOverFallback: '', + abortController: undefined, hasValidState: () => { return !!(props.validMessage ?? props.validation === 'valid'); }, @@ -87,10 +90,25 @@ export default function DBCheckbox(props: DBCheckboxProps) { state._descByIds = undefined; } }, - handleChange: (event: ChangeEvent) => { - if (props.onChange) { - props.onChange(event); - } + handleChange: ( + event: ChangeEvent, + reset?: boolean + ) => { + useTarget({ + angular: () => { + if (props.onChange) { + // We need to split the if statements for generation + if (reset) { + props.onChange(event); + } + } + }, + default: () => { + if (props.onChange) { + props.onChange(event); + } + } + }); useTarget({ angular: () => @@ -177,6 +195,34 @@ export default function DBCheckbox(props: DBCheckboxProps) { state.initialized = false; } }, [state.initialized, _ref, props.checked]); + + onUpdate(() => { + if (_ref) { + const defaultChecked = useTarget({ + react: (props as any).defaultChecked, + default: undefined + }); + + let controller = state.abortController; + if (!controller) { + controller = new AbortController(); + state.abortController = controller; + } + + addCheckedResetEventListener( + _ref, + { checked: props.checked, defaultChecked }, + (event) => { + state.handleChange(event, true); + }, + controller.signal + ); + } + }, [_ref]); + + onUnMount(() => { + state.abortController?.abort(); + }); // jscpd:ignore-end return ( diff --git a/packages/components/src/components/checkbox/docs/HTML.md b/packages/components/src/components/checkbox/docs/HTML.md index 424be16ca8ac..214a750c39d9 100644 --- a/packages/components/src/components/checkbox/docs/HTML.md +++ b/packages/components/src/components/checkbox/docs/HTML.md @@ -32,6 +32,8 @@ To add a descriptive text with HTML formatting (e.g., bold or italic text) to a /> Label -

Example Text

+

+ Example Text +

``` diff --git a/packages/components/src/components/checkbox/docs/React.md b/packages/components/src/components/checkbox/docs/React.md index 2e149ca64eff..6aa70f8053a6 100644 --- a/packages/components/src/components/checkbox/docs/React.md +++ b/packages/components/src/components/checkbox/docs/React.md @@ -47,7 +47,7 @@ const App = () => { {/* The DBInfotext component holds the formatted message. */} - Example Text + Example Text ); diff --git a/packages/components/src/components/checkbox/docs/Vue.md b/packages/components/src/components/checkbox/docs/Vue.md index 32605da4365f..625ed140ceee 100644 --- a/packages/components/src/components/checkbox/docs/Vue.md +++ b/packages/components/src/components/checkbox/docs/Vue.md @@ -33,6 +33,8 @@ import { DBCheckbox, DBInfotext } from "@db-ux/v-core-components"; Example Checkbox - Example Text + + Example Text + ``` diff --git a/packages/components/src/components/custom-select/custom-select.lite.tsx b/packages/components/src/components/custom-select/custom-select.lite.tsx index f287129c888f..f9f1ce254cbc 100644 --- a/packages/components/src/components/custom-select/custom-select.lite.tsx +++ b/packages/components/src/components/custom-select/custom-select.lite.tsx @@ -2,6 +2,7 @@ import { For, onMount, + onUnMount, onUpdate, Show, useDefaultProps, @@ -48,6 +49,7 @@ import { DocumentClickListener } from '../../utils/document-click-listener'; import { DocumentScrollListener } from '../../utils/document-scroll-listener'; import { handleFixedDropdown } from '../../utils/floating-components'; import { + addResetEventListener, handleFrameworkEventAngular, handleFrameworkEventVue } from '../../utils/form-components'; @@ -103,6 +105,7 @@ export default function DBCustomSelect(props: DBCustomSelectProps) { _infoTextId: undefined, _validity: 'no-validation', _userInteraction: false, + abortController: undefined, // Workaround for Vue output: TS for Vue would think that it could be a function, and by this we clarify that it's a string _descByIds: undefined, _selectedLabels: '', @@ -750,6 +753,31 @@ export default function DBCustomSelect(props: DBCustomSelectProps) { } }, [state._values, selectRef]); + onUpdate(() => { + if (selectRef) { + let controller = state.abortController; + if (!controller) { + controller = new AbortController(); + state.abortController = controller; + } + + const initialValues = props.values; + addResetEventListener( + selectRef, + () => { + const resetValue = initialValues + ? initialValues + : selectRef.value + ? [selectRef.value] + : []; + state.handleOptionSelected(resetValue); + state.handleValidation(); + }, + controller.signal + ); + } + }, [selectRef]); + onUpdate(() => { state._validity = props.validation; }, [props.validation]); @@ -853,6 +881,10 @@ export default function DBCustomSelect(props: DBCustomSelectProps) { DEFAULT_INVALID_MESSAGE; }, [selectRef, props.invalidMessage]); + onUnMount(() => { + state.abortController?.abort(); + }); + function satisfyReact(event: any) { // This is a function to satisfy React event.stopPropagation(); diff --git a/packages/components/src/components/input/input.lite.tsx b/packages/components/src/components/input/input.lite.tsx index f01eaf1ffbdd..0d780c6f7780 100644 --- a/packages/components/src/components/input/input.lite.tsx +++ b/packages/components/src/components/input/input.lite.tsx @@ -1,6 +1,7 @@ import { For, onMount, + onUnMount, onUpdate, Show, useDefaultProps, @@ -41,6 +42,7 @@ import { uuid } from '../../utils'; import { + addValueResetEventListener, handleFrameworkEventAngular, handleFrameworkEventVue } from '../../utils/form-components'; @@ -71,6 +73,7 @@ export default function DBInput(props: DBInputProps) { _descByIds: undefined, _value: undefined, _voiceOverFallback: '', + abortController: undefined, hasValidState: () => { return !!(props.validMessage ?? props.validation === 'valid'); }, @@ -106,8 +109,15 @@ export default function DBInput(props: DBInputProps) { state._descByIds = undefined; } }, - handleInput: (event: InputEvent) => { + handleInput: (event: InputEvent, reset?: boolean) => { useTarget({ + angular: () => { + if (props.onInput) { + if (reset) { + props.onInput(event); + } + } + }, vue: () => { if (props.input) { props.input(event); @@ -129,10 +139,25 @@ export default function DBInput(props: DBInputProps) { }); state.handleValidation(); }, - handleChange: (event: ChangeEvent) => { - if (props.onChange) { - props.onChange(event); - } + handleChange: ( + event: ChangeEvent, + reset?: boolean + ) => { + useTarget({ + angular: () => { + if (props.onChange) { + // We need to split the if statements for generation + if (reset) { + props.onChange(event); + } + } + }, + default: () => { + if (props.onChange) { + props.onChange(event); + } + } + }); useTarget({ angular: () => handleFrameworkEventAngular(state, event), @@ -202,6 +227,35 @@ export default function DBInput(props: DBInputProps) { state._value = props.value; }, [props.value]); + onUpdate(() => { + if (_ref) { + const defaultValue = useTarget({ + react: (props as any).defaultValue, + default: undefined + }); + + let controller = state.abortController; + if (!controller) { + controller = new AbortController(); + state.abortController = controller; + } + + addValueResetEventListener( + _ref, + { value: props.value, defaultValue }, + (event) => { + state.handleChange(event, true); + state.handleInput(event, true); + }, + controller.signal + ); + } + }, [_ref]); + + onUnMount(() => { + state.abortController?.abort(); + }); + return (
{ expect(test).toEqual('test'); }); - test('should have enterkeyhint attribute when provided', async ({ mount }) => { + test('should have enterkeyhint attribute when provided', async ({ + mount + }) => { const component = await mount( ); @@ -66,12 +68,10 @@ const testAction = () => { await expect(input).toHaveAttribute('inputmode', 'numeric'); }); - - - test('should not have enterkeyhint or inputmode when not provided', async ({ mount }) => { - const component = await mount( - - ); + test('should not have enterkeyhint or inputmode when not provided', async ({ + mount + }) => { + const component = await mount(); const input = component.getByRole('textbox'); await expect(input).not.toHaveAttribute('enterkeyhint'); await expect(input).not.toHaveAttribute('inputmode'); diff --git a/packages/components/src/components/radio/model.ts b/packages/components/src/components/radio/model.ts index 8f6fd478a711..c133cf52d6ee 100644 --- a/packages/components/src/components/radio/model.ts +++ b/packages/components/src/components/radio/model.ts @@ -9,6 +9,8 @@ import { GlobalProps, GlobalState, InitializedState, + InputEventProps, + InputEventState, SizeProps } from '../../shared/model'; @@ -16,6 +18,7 @@ export type DBRadioDefaultProps = {}; export type DBRadioProps = DBRadioDefaultProps & GlobalProps & + InputEventProps & ChangeEventProps & FocusEventProps & FormProps & @@ -26,6 +29,7 @@ export type DBRadioDefaultState = {}; export type DBRadioState = DBRadioDefaultState & GlobalState & + InputEventState & ChangeEventState & FocusEventState & FormState & diff --git a/packages/components/src/components/radio/radio.lite.tsx b/packages/components/src/components/radio/radio.lite.tsx index 82256364ad73..af77762a6a4a 100644 --- a/packages/components/src/components/radio/radio.lite.tsx +++ b/packages/components/src/components/radio/radio.lite.tsx @@ -1,5 +1,6 @@ import { onMount, + onUnMount, onUpdate, Show, useDefaultProps, @@ -9,8 +10,9 @@ import { useTarget } from '@builder.io/mitosis'; import { ChangeEvent, InteractionEvent } from '../../shared/model'; -import { cls, getBoolean, getHideProp, uuid } from '../../utils'; +import { cls, delay, getBoolean, getHideProp, uuid } from '../../utils'; import { + addResetEventListener, handleFrameworkEventAngular, handleFrameworkEventVue } from '../../utils/form-components'; @@ -32,10 +34,58 @@ export default function DBRadio(props: DBRadioProps) { const state = useStore({ initialized: false, _id: undefined, - handleChange: (event: ChangeEvent | any) => { - if (props.onChange) { - props.onChange(event); - } + abortController: undefined, + handleInput: ( + event: ChangeEvent | any, + reset?: boolean + ) => { + useTarget({ + angular: () => { + if (props.onInput) { + if (reset) { + props.onInput(event); + } + } + }, + vue: () => { + if (props.input) { + props.input(event); + } + if (props.onInput) { + props.onInput(event); + } + }, + default: () => { + if (props.onInput) { + props.onInput(event); + } + } + }); + + useTarget({ + angular: () => handleFrameworkEventAngular(state, event), + vue: () => handleFrameworkEventVue(() => {}, event) + }); + }, + handleChange: ( + event: ChangeEvent | any, + reset?: boolean + ) => { + useTarget({ + angular: () => { + if (props.onChange) { + // We need to split the if statements for generation + if (reset) { + props.onChange(event); + } + } + }, + default: () => { + if (props.onChange) { + props.onChange(event); + } + } + }); useTarget({ angular: () => handleFrameworkEventAngular(state, event), @@ -58,7 +108,6 @@ export default function DBRadio(props: DBRadioProps) { state.initialized = true; state._id = props.id ?? `radio-${uuid()}`; }); - // jscpd:ignore-end onUpdate(() => { if (props.checked && state.initialized && _ref) { @@ -66,6 +115,50 @@ export default function DBRadio(props: DBRadioProps) { } }, [state.initialized, _ref, props.checked]); + onUpdate(() => { + if (_ref) { + const defaultChecked = useTarget({ + react: (props as any).defaultChecked, + default: undefined + }); + + let controller = state.abortController; + if (!controller) { + controller = new AbortController(); + state.abortController = controller; + } + + addResetEventListener( + _ref, + (event: Event) => { + void delay(() => { + const resetChecked = props.checked + ? props.checked + : defaultChecked + ? defaultChecked + : _ref.checked; + const valueEvent: any = { + ...event, + target: { + ...event.target, + value: '', + checked: resetChecked + } + }; + state.handleChange(valueEvent, true); + state.handleInput(valueEvent, true); + }, 1); + }, + controller.signal + ); + } + }, [_ref]); + + onUnMount(() => { + state.abortController?.abort(); + }); + // jscpd:ignore-end + return (