From a03968e77944728e899ea1a59521cb6c9f4487b4 Mon Sep 17 00:00:00 2001 From: dev-rb Date: Fri, 22 Dec 2023 15:56:20 -0500 Subject: [PATCH] fix: inputs for checkbox and radiogroup to trigger change event --- packages/core/src/checkbox/checkbox-input.tsx | 46 +++++++++++++----- .../radio-group/radio-group-item-input.tsx | 47 ++++++++++++++----- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/packages/core/src/checkbox/checkbox-input.tsx b/packages/core/src/checkbox/checkbox-input.tsx index ea626ec6..8751e1fb 100644 --- a/packages/core/src/checkbox/checkbox-input.tsx +++ b/packages/core/src/checkbox/checkbox-input.tsx @@ -14,7 +14,7 @@ import { OverrideComponentProps, visuallyHiddenStyles, } from "@kobalte/utils"; -import { createEffect, JSX, on, splitProps } from "solid-js"; +import { createEffect, createSignal, JSX, on, splitProps } from "solid-js"; import { createFormControlField, @@ -54,23 +54,28 @@ export function CheckboxInput(props: CheckboxInputProps) { const { fieldProps } = createFormControlField(formControlFieldProps); + const [isInternalChangeEvent, setIsInternalChangeEvent] = createSignal(false); + const onChange: JSX.ChangeEventHandlerUnion = e => { callHandler(e, local.onChange); e.stopPropagation(); - const target = e.target as HTMLInputElement; - - context.setIsChecked(target.checked); - - // Unlike in React, inputs `checked` state can be out of sync with our toggle state. - // for example a readonly `` is always "checkable". - // - // Also, even if an input is controlled (ex: ``, - // clicking on the input will change its internal `checked` state. - // - // To prevent this, we need to force the input `checked` state to be in sync with the toggle state. - target.checked = context.checked(); + if (!isInternalChangeEvent()) { + const target = e.target as HTMLInputElement; + + context.setIsChecked(target.checked); + + // Unlike in React, inputs `checked` state can be out of sync with our toggle state. + // for example a readonly `` is always "checkable". + // + // Also, even if an input is controlled (ex: ``, + // clicking on the input will change its internal `checked` state. + // + // To prevent this, we need to force the input `checked` state to be in sync with the toggle state. + target.checked = context.checked(); + } + setIsInternalChangeEvent(false); }; const onFocus: JSX.FocusEventHandlerUnion = e => { @@ -83,6 +88,21 @@ export function CheckboxInput(props: CheckboxInputProps) { context.setIsFocused(false); }; + createEffect( + on( + [() => context.checked(), () => context.value()], + () => { + setIsInternalChangeEvent(true); + + ref?.dispatchEvent(new Event("input", { bubbles: true, cancelable: true })); + ref?.dispatchEvent(new Event("change", { bubbles: true, cancelable: true })); + }, + { + defer: true, + }, + ), + ); + // indeterminate is a property, but it can only be set via javascript // https://css-tricks.com/indeterminate-checkboxes/ // Unlike in React, inputs `indeterminate` state can be out of sync with our. diff --git a/packages/core/src/radio-group/radio-group-item-input.tsx b/packages/core/src/radio-group/radio-group-item-input.tsx index 1251aa59..1529172b 100644 --- a/packages/core/src/radio-group/radio-group-item-input.tsx +++ b/packages/core/src/radio-group/radio-group-item-input.tsx @@ -13,7 +13,7 @@ import { OverrideComponentProps, visuallyHiddenStyles, } from "@kobalte/utils"; -import { createEffect, JSX, onCleanup, splitProps } from "solid-js"; +import { createEffect, createSignal, JSX, on, onCleanup, splitProps } from "solid-js"; import { useFormControlContext } from "../form-control"; import { useRadioGroupContext } from "./radio-group-context"; @@ -73,23 +73,28 @@ export function RadioGroupItemInput(props: RadioGroupItemInputProps) { ); }; + const [isInternalChangeEvent, setIsInternalChangeEvent] = createSignal(false); + const onChange: JSX.ChangeEventHandlerUnion = e => { callHandler(e, local.onChange); e.stopPropagation(); - radioGroupContext.setSelectedValue(radioContext.value()); - - const target = e.target as HTMLInputElement; - - // Unlike in React, inputs `checked` state can be out of sync with our state. - // for example a readonly `` is always "checkable". - // - // Also, even if an input is controlled (ex: ``, - // clicking on the input will change its internal `checked` state. - // - // To prevent this, we need to force the input `checked` state to be in sync with our state. - target.checked = radioContext.isSelected(); + if (!isInternalChangeEvent()) { + radioGroupContext.setSelectedValue(radioContext.value()); + + const target = e.target as HTMLInputElement; + + // Unlike in React, inputs `checked` state can be out of sync with our state. + // for example a readonly `` is always "checkable". + // + // Also, even if an input is controlled (ex: ``, + // clicking on the input will change its internal `checked` state. + // + // To prevent this, we need to force the input `checked` state to be in sync with our state. + target.checked = radioContext.isSelected(); + } + setIsInternalChangeEvent(false); }; const onFocus: JSX.FocusEventHandlerUnion = e => { @@ -102,6 +107,22 @@ export function RadioGroupItemInput(props: RadioGroupItemInputProps) { radioContext.setIsFocused(false); }; + createEffect( + on( + [() => radioContext.isSelected(), () => radioContext.value()], + c => { + if (!c[0] && c[1] === radioContext.value()) return; + setIsInternalChangeEvent(true); + + const ref = radioContext.inputRef(); + ref?.dispatchEvent(new Event("input", { bubbles: true, cancelable: true })); + ref?.dispatchEvent(new Event("change", { bubbles: true, cancelable: true })); + }, + { + defer: true, + }, + ), + ); createEffect(() => onCleanup(radioContext.registerInput(others.id!))); return (