diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index d7701e0..196c2de 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix `` crash ([#1889](https://github.com/tailwindlabs/headlessui/pull/1889))
- Expose `close` function for `Menu` and `Menu.Item` components ([#1897](https://github.com/tailwindlabs/headlessui/pull/1897))
- Fix `useOutsideClick`, add improvements for ShadowDOM ([#1914](https://github.com/tailwindlabs/headlessui/pull/1914))
+- Fire ``'s `onChange` handler when changing the value internally ([#1916](https://github.com/tailwindlabs/headlessui/pull/1916))
### Added
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
index 5f21d35..66c11c0 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.test.tsx
@@ -2851,6 +2851,56 @@ describe('Keyboard interactions', () => {
expect(getComboboxInput()?.value).toBe('option-b')
})
)
+
+ it(
+ 'The onChange handler is fired when the input value is changed internally',
+ suppressConsoleLogs(async () => {
+ let currentSearchQuery: string = ''
+
+ render(
+
+ {
+ currentSearchQuery = event.target.value
+ }}
+ />
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ )
+
+ // Open combobox
+ await click(getComboboxButton())
+
+ // Verify that the current search query is empty
+ expect(currentSearchQuery).toBe('')
+
+ // Look for "Option C"
+ await type(word('Option C'), getComboboxInput())
+
+ // The input should be updated
+ expect(getComboboxInput()?.value).toBe('Option C')
+
+ // The current search query should reflect the input value
+ expect(currentSearchQuery).toBe('Option C')
+
+ // Close combobox
+ await press(Keys.Escape)
+
+ // The input should be empty
+ expect(getComboboxInput()?.value).toBe('')
+
+ // The current search query should be empty like the input
+ expect(currentSearchQuery).toBe('')
+
+ // The combobox should be closed
+ assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
+ })
+ )
})
describe('`ArrowDown` key', () => {
@@ -5501,7 +5551,7 @@ describe('Form compatibility', () => {
}}
>
-
+
Trigger
Pizza Delivery
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index e1a5df8..d4a3714 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -666,6 +666,33 @@ let Input = forwardRefWithAs(function Input<
let id = `headlessui-combobox-input-${useId()}`
let d = useDisposables()
+ let shouldIgnoreOpenOnChange = false
+ function updateInputAndNotify(newValue: string) {
+ let input = data.inputRef.current
+ if (!input) {
+ return
+ }
+
+ // The value is already the same, so we can bail out early
+ if (input.value === newValue) {
+ return
+ }
+
+ // Skip React's value setting which causes the input event to not be fired because it de-dupes input/change events
+ let descriptor = Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value')
+ descriptor?.set?.call(input, newValue)
+
+ // Fire an input event which causes the browser to trigger the user's `onChange` handler.
+ // We have to prevent the combobox from opening when this happens. Since these events
+ // fire synchronously `shouldIgnoreOpenOnChange` will be correct during `handleChange`
+ shouldIgnoreOpenOnChange = true
+ input.dispatchEvent(new Event('input', { bubbles: true }))
+
+ // Now we can inform react that the input value has changed
+ input.value = newValue
+ shouldIgnoreOpenOnChange = false
+ }
+
let currentValue = useMemo(() => {
if (typeof displayValue === 'function') {
return displayValue(data.value as unknown as TType) ?? ''
@@ -682,7 +709,7 @@ let Input = forwardRefWithAs(function Input<
([currentValue, state], [oldCurrentValue, oldState]) => {
if (!data.inputRef.current) return
if (oldState === ComboboxState.Open && state === ComboboxState.Closed) {
- data.inputRef.current.value = currentValue
+ updateInputAndNotify(currentValue)
} else if (currentValue !== oldCurrentValue) {
data.inputRef.current.value = currentValue
}
@@ -787,7 +814,9 @@ let Input = forwardRefWithAs(function Input<
})
let handleChange = useEvent((event: React.ChangeEvent) => {
- actions.openCombobox()
+ if (!shouldIgnoreOpenOnChange) {
+ actions.openCombobox()
+ }
onChange?.(event)
})
diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md
index 270efcc..210a7f4 100644
--- a/packages/@headlessui-vue/CHANGELOG.md
+++ b/packages/@headlessui-vue/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Expose `close` function for `Menu` and `MenuItem` components ([#1897](https://github.com/tailwindlabs/headlessui/pull/1897))
- Fix `useOutsideClick`, add improvements for ShadowDOM ([#1914](https://github.com/tailwindlabs/headlessui/pull/1914))
- Prevent default slot warning when using a component for `as` prop ([#1915](https://github.com/tailwindlabs/headlessui/pull/1915))
+- Fire ``'s `@change` handler when changing the value internally ([#1916](https://github.com/tailwindlabs/headlessui/pull/1916))
## [1.7.3] - 2022-09-30
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index 9c8968d..a0fdf89 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -2931,6 +2931,60 @@ describe('Keyboard interactions', () => {
expect(getComboboxInput()?.value).toBe('option-b')
})
)
+
+ it(
+ 'The onChange handler is fired when the input value is changed internally',
+ suppressConsoleLogs(async () => {
+ let currentSearchQuery: string = ''
+
+ renderTemplate({
+ template: html`
+
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `,
+ setup: () => ({
+ value: ref(null),
+ onChange: (evt: InputEvent & { target: HTMLInputElement }) => {
+ currentSearchQuery = evt.target.value
+ },
+ }),
+ })
+
+ // Open combobox
+ await click(getComboboxButton())
+
+ // Verify that the current search query is empty
+ expect(currentSearchQuery).toBe('')
+
+ // Look for "Option C"
+ await type(word('Option C'), getComboboxInput())
+
+ // The input should be updated
+ expect(getComboboxInput()?.value).toBe('Option C')
+
+ // The current search query should reflect the input value
+ expect(currentSearchQuery).toBe('Option C')
+
+ // Close combobox
+ await press(Keys.Escape)
+
+ // The input should be empty
+ expect(getComboboxInput()?.value).toBe('')
+
+ // The current search query should be empty like the input
+ expect(currentSearchQuery).toBe('')
+
+ // The combobox should be closed
+ assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
+ })
+ )
})
describe('`ArrowDown` key', () => {
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts
index d287ac7..25162e0 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts
@@ -632,6 +632,23 @@ export let ComboboxInput = defineComponent({
// Workaround Vue bug where watching [ref(undefined)] is not fired immediately even when value is true
const __fixVueImmediateWatchBug__ = ref('')
+ let shouldIgnoreOpenOnChange = false
+ function updateInputAndNotify(currentValue: string) {
+ let input = dom(api.inputRef)
+ if (!input) {
+ return
+ }
+
+ input.value = currentValue
+
+ // Fire an input event which causes the browser to trigger the user's `onChange` handler.
+ // We have to prevent the combobox from opening when this happens. Since these events
+ // fire synchronously `shouldIgnoreOpenOnChange` will be correct during `handleChange`
+ shouldIgnoreOpenOnChange = true
+ input.dispatchEvent(new Event('input', { bubbles: true }))
+ shouldIgnoreOpenOnChange = false
+ }
+
onMounted(() => {
watch(
[api.value, __fixVueImmediateWatchBug__],
@@ -650,7 +667,7 @@ export let ComboboxInput = defineComponent({
let input = dom(api.inputRef)
if (!input) return
if (oldState === ComboboxStates.Open && state === ComboboxStates.Closed) {
- input.value = currentValue
+ updateInputAndNotify(currentValue)
} else if (currentValue !== oldCurrentValue) {
input.value = currentValue
}
@@ -756,7 +773,9 @@ export let ComboboxInput = defineComponent({
}
function handleInput(event: Event & { target: HTMLInputElement }) {
- api.openCombobox()
+ if (!shouldIgnoreOpenOnChange) {
+ api.openCombobox()
+ }
emit('change', event)
}