Skip to content

Commit d2b7345

Browse files
authored
Add optional onClose callback to Combobox component (#3122)
* add optional `onClose` callback to `Combobox` component * update changelog * add tests to ensure `onClose` is called when `Combobox` closes
1 parent 6c9e4b2 commit d2b7345

File tree

3 files changed

+17
-2
lines changed

3 files changed

+17
-2
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Ensure anchored components are properly stacked on top of `Dialog` components ([#3111](https://github.com/tailwindlabs/headlessui/pull/3111))
2525
- Move focus to `ListboxOptions` and `MenuItems` when they are rendered later ([#3112](https://github.com/tailwindlabs/headlessui/pull/3112))
2626
- Ensure anchored components are always rendered in a stacking context ([#3115](https://github.com/tailwindlabs/headlessui/pull/3115))
27+
- Add optional `onClose` callback to `Combobox` component ([#3122](https://github.com/tailwindlabs/headlessui/pull/3122))
2728

2829
### Changed
2930

packages/@headlessui-react/src/components/combobox/combobox.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,14 +542,15 @@ describe('Rendering', () => {
542542
it(
543543
'should close the Combobox when the input is blurred',
544544
suppressConsoleLogs(async () => {
545+
let closeHandler = jest.fn()
545546
let data = [
546547
{ id: 1, name: 'alice', label: 'Alice' },
547548
{ id: 2, name: 'bob', label: 'Bob' },
548549
{ id: 3, name: 'charlie', label: 'Charlie' },
549550
]
550551

551552
render(
552-
<Combobox<(typeof data)[number]> name="assignee" by="id">
553+
<Combobox<(typeof data)[number]> name="assignee" by="id" onClose={closeHandler}>
553554
<Combobox.Input onChange={NOOP} />
554555
<Combobox.Button />
555556
<Combobox.Options>
@@ -569,7 +570,9 @@ describe('Rendering', () => {
569570
assertComboboxList({ state: ComboboxState.Visible })
570571

571572
// Close the combobox
573+
expect(closeHandler).toHaveBeenCalledTimes(0)
572574
await blur(getComboboxInput())
575+
expect(closeHandler).toHaveBeenCalledTimes(1)
573576

574577
// Verify it is closed
575578
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })
@@ -2825,6 +2828,7 @@ describe.each([{ virtual: true }, { virtual: false }])(
28252828
'should be possible to close the combobox with Enter and choose the active combobox option',
28262829
suppressConsoleLogs(async () => {
28272830
let handleChange = jest.fn()
2831+
let closeHandler = jest.fn()
28282832

28292833
function Example() {
28302834
let [value, setValue] = useState<string | undefined>(undefined)
@@ -2833,6 +2837,7 @@ describe.each([{ virtual: true }, { virtual: false }])(
28332837
<MyCombobox
28342838
comboboxProps={{
28352839
value,
2840+
onClose: closeHandler,
28362841
onChange(value: string | undefined) {
28372842
setValue(value)
28382843
handleChange(value)
@@ -2861,7 +2866,9 @@ describe.each([{ virtual: true }, { virtual: false }])(
28612866
await mouseMove(options[0])
28622867

28632868
// Choose option, and close combobox
2869+
expect(closeHandler).toHaveBeenCalledTimes(0)
28642870
await press(Keys.Enter)
2871+
expect(closeHandler).toHaveBeenCalledTimes(1)
28652872

28662873
// Verify it is closed
28672874
assertComboboxButton({ state: ComboboxState.InvisibleUnmounted })
@@ -4883,15 +4890,18 @@ describe.each([{ virtual: true }, { virtual: false }])('Mouse interactions %s',
48834890
it(
48844891
'should be possible to click outside of the combobox which should close the combobox (even if we press the combobox button)',
48854892
suppressConsoleLogs(async () => {
4886-
render(<MyCombobox />)
4893+
let closeHandler = jest.fn()
4894+
render(<MyCombobox comboboxProps={{ onClose: closeHandler }} />)
48874895

48884896
// Open combobox
48894897
await click(getComboboxButton())
48904898
assertComboboxList({ state: ComboboxState.Visible })
48914899
assertActiveElement(getComboboxInput())
48924900

48934901
// Click the combobox button again
4902+
expect(closeHandler).toHaveBeenCalledTimes(0)
48944903
await click(getComboboxButton())
4904+
expect(closeHandler).toHaveBeenCalledTimes(1)
48954905

48964906
// Should be closed now
48974907
assertComboboxList({ state: ComboboxState.InvisibleUnmounted })

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,8 @@ export type ComboboxProps<
588588
disabled?: (value: NoInfer<TValue>) => boolean
589589
} | null
590590

591+
onClose?(): void
592+
591593
__demoMode?: boolean
592594
}
593595
>
@@ -605,6 +607,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
605607
name,
606608
by,
607609
disabled = providedDisabled || false,
610+
onClose,
608611
__demoMode = false,
609612
multiple = false,
610613
immediate = false,
@@ -771,6 +774,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
771774
let closeCombobox = useEvent(() => {
772775
dispatch({ type: ActionTypes.CloseCombobox })
773776
defaultToFirstOption.current = false
777+
onClose?.()
774778
})
775779

776780
let goToOption = useEvent((focus, idx, trigger) => {

0 commit comments

Comments
 (0)