From 0ff03a0e45f52723db9c2a3008e0dce120253afa Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Fri, 4 Jul 2025 16:24:38 -0400 Subject: [PATCH 1/6] Fix listbox closing immediately after opening on touch devices --- .../src/components/listbox/listbox.tsx | 11 +++ .../react/pages/listbox/listbox-overlaps.tsx | 81 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 playgrounds/react/pages/listbox/listbox-overlaps.tsx diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index aa59f2606..6d5aa042c 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -441,11 +441,21 @@ function ButtonFn( flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) } else { + if (event.pointerType !== 'mouse') return event.preventDefault() machine.actions.openListbox({ focus: Focus.Nothing }) } }) + let handleClick = useEvent((event: ReactPointerEvent) => { + if (event.button !== 0) return // Only handle left clicks + if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() + if (machine.state.listboxState !== ListboxStates.Closed) return + if (event.pointerType === 'mouse') return + event.preventDefault() + machine.actions.openListbox({ focus: Focus.Nothing }) + }) + // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. let handleKeyPress = useEvent((event: ReactKeyboardEvent) => event.preventDefault()) @@ -487,6 +497,7 @@ function ButtonFn( onKeyUp: handleKeyUp, onKeyPress: handleKeyPress, onPointerDown: handlePointerDown, + onClick: handleClick, }, focusProps, hoverProps, diff --git a/playgrounds/react/pages/listbox/listbox-overlaps.tsx b/playgrounds/react/pages/listbox/listbox-overlaps.tsx new file mode 100644 index 000000000..14b48314e --- /dev/null +++ b/playgrounds/react/pages/listbox/listbox-overlaps.tsx @@ -0,0 +1,81 @@ +import { Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react' +import { useState } from 'react' + +let people = [ + 'Wade Cooper', + 'Arlene Mccoy', + 'Devon Webb', + 'Tom Cook', + 'Tanya Fox', + 'Hellen Schmidt', + 'Caroline Schultz', + 'Mason Heaney', + 'Claudie Smitham', + 'Emil Schaefer', +] + +export default function Home() { + let [active, setActivePerson] = useState(people[0]) + + return ( +
+
+
+ + + +
+ + + {active} + + + + + + + + + + {people.map((name) => ( + + + {name} + + + + + + + + ))} + +
+
+
+
+
+ ) +} From 60566bfa18d4c0cee2e97134d7b59ddf5c9c0256 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 23 Jul 2025 12:00:56 +0200 Subject: [PATCH 2/6] fallback to `pointerType` of `'mouse'` JSDOM Doesn't support PointerEvent yet, so let's add this to the polyfills. --- jest/polyfills.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/jest/polyfills.ts b/jest/polyfills.ts index 620b8c13b..ef403dfc2 100644 --- a/jest/polyfills.ts +++ b/jest/polyfills.ts @@ -29,6 +29,18 @@ class PointerEvent extends Event { // @ts-expect-error JSDOM doesn't support `button` yet... this.button = props.button } + + if (props.pointerType != null) { + // @ts-expect-error JSDOM doesn't support `pointerType` yet... + this.pointerType = props.pointerType + } + + // @ts-expect-error JSDOM doesn't support `pointerType` yet... + if (this.pointerType === undefined) { + // Fallback to `pointerType` of `'mouse'` if not provided. + // @ts-expect-error JSDOM doesn't support `pointerType` yet... + this.pointerType = 'mouse' + } } } // @ts-expect-error JSDOM doesn't support `PointerEvent` yet... From 17d31cf9e232d7e4e7d42cea0ee5841a0276a0e5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 23 Jul 2025 12:17:35 +0200 Subject: [PATCH 3/6] use `MouseButton` constants --- .../@headlessui-react/src/components/listbox/listbox.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 6d5aa042c..819194c11 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -83,6 +83,7 @@ import { import { useDescribedBy } from '../description/description' import { Keys } from '../keyboard' import { Label, useLabelledBy, useLabels, type _internal_ComponentLabel } from '../label/label' +import { MouseButton } from '../mouse' import { Portal } from '../portal/portal' import { ActionTypes, ActivationTrigger, ListboxStates, ValueMode } from './listbox-machine' import { ListboxContext, useListboxMachine, useListboxMachineContext } from './listbox-machine-glue' @@ -435,7 +436,7 @@ function ButtonFn( }) let handlePointerDown = useEvent((event: ReactPointerEvent) => { - if (event.button !== 0) return // Only handle left clicks + if (event.button !== MouseButton.Left) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() if (machine.state.listboxState === ListboxStates.Open) { flushSync(() => machine.actions.closeListbox()) @@ -448,7 +449,7 @@ function ButtonFn( }) let handleClick = useEvent((event: ReactPointerEvent) => { - if (event.button !== 0) return // Only handle left clicks + if (event.button !== MouseButton.Left) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() if (machine.state.listboxState !== ListboxStates.Closed) return if (event.pointerType === 'mouse') return From f081f46d9a0a9293fc01a4ccac247118af11ce00 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 23 Jul 2025 12:18:12 +0200 Subject: [PATCH 4/6] `pointerType` doesn't exist on the `click` event --- packages/@headlessui-react/src/components/listbox/listbox.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index 819194c11..bc509815e 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -452,7 +452,6 @@ function ButtonFn( if (event.button !== MouseButton.Left) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() if (machine.state.listboxState !== ListboxStates.Closed) return - if (event.pointerType === 'mouse') return event.preventDefault() machine.actions.openListbox({ focus: Focus.Nothing }) }) From f69a0fd21b6f69b1f4d4dc711606de17ecdb65b0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 23 Jul 2025 12:53:33 +0200 Subject: [PATCH 5/6] improve click/touch handling Rely on: - `PointerDown` in case `pointerType === 'mouse'` - `Click` in case `pointerType !== 'mouse'` This makes the tests pass, and also makes sure that clicking on the ListboxButton while the Listbox is open doesn't close and auto opens again but stays closed. --- .../src/components/listbox/listbox.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index bc509815e..37051bfb1 100644 --- a/packages/@headlessui-react/src/components/listbox/listbox.tsx +++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx @@ -435,25 +435,35 @@ function ButtonFn( } }) + let pointerTypeRef = useRef<'touch' | 'mouse' | 'pen' | null>(null) let handlePointerDown = useEvent((event: ReactPointerEvent) => { + pointerTypeRef.current = event.pointerType + + if (event.pointerType !== 'mouse') return + if (event.button !== MouseButton.Left) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() if (machine.state.listboxState === ListboxStates.Open) { flushSync(() => machine.actions.closeListbox()) machine.state.buttonElement?.focus({ preventScroll: true }) } else { - if (event.pointerType !== 'mouse') return event.preventDefault() machine.actions.openListbox({ focus: Focus.Nothing }) } }) let handleClick = useEvent((event: ReactPointerEvent) => { + if (pointerTypeRef.current === 'mouse') return + if (event.button !== MouseButton.Left) return // Only handle left clicks if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault() - if (machine.state.listboxState !== ListboxStates.Closed) return - event.preventDefault() - machine.actions.openListbox({ focus: Focus.Nothing }) + if (machine.state.listboxState === ListboxStates.Open) { + flushSync(() => machine.actions.closeListbox()) + machine.state.buttonElement?.focus({ preventScroll: true }) + } else { + event.preventDefault() + machine.actions.openListbox({ focus: Focus.Nothing }) + } }) // This is needed so that we can "cancel" the click event when we use the `Enter` key on a button. From 3b6ed199bd42a5d63c154c1e7bedb7ed80e4c1ea Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 23 Jul 2025 13:18:49 +0200 Subject: [PATCH 6/6] update changelog --- packages/@headlessui-react/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 8d0a151e2..57b597809 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +### Fixed + +- Fix listbox closing immediately after opening on touch devices ([#3755](https://github.com/tailwindlabs/headlessui/pull/3755)) ## [2.2.4] - 2025-05-20