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... 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 diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx index aa59f2606..37051bfb1 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' @@ -434,8 +435,27 @@ function ButtonFn( } }) + let pointerTypeRef = useRef<'touch' | 'mouse' | 'pen' | null>(null) let handlePointerDown = useEvent((event: ReactPointerEvent) => { - if (event.button !== 0) return // Only handle left clicks + 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 { + 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.Open) { flushSync(() => machine.actions.closeListbox()) @@ -487,6 +507,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} + + + + + + + + ))} + +
+
+
+
+
+ ) +}