Fix Unexpected undefined crash in Combobox component with virtual mode (#3678)

This PR fixes an issue where the `Combobox` component crashes if you are
using the `virtual` option and you quickly type something such that the
`Combobox` opens but no valid options are available.

We already check if the current active index is available in the
internal `options` list. However, if you then call
`virtualizer.scrollToIndex(data.activeOptionIndex)` it will crash if you
are too fast.


https://github.com/user-attachments/assets/f48172e6-4098-4a31-aa16-67ce21f074d1

If you are typing slowly, then it will work as expected.


https://github.com/user-attachments/assets/9d522bd5-5b54-4c12-9250-a2d92a511b35

I did find an open issue on TanStack's repo about this:
https://github.com/TanStack/virtual/issues/879

This PR is basically a workaround by delaying the call. But it does have
the expected behavior now.


https://github.com/user-attachments/assets/2e5e47a5-b021-4897-b098-568711723b77


Fixes: #3583
This commit is contained in:
Robin Malfait
2025-04-04 14:16:42 +02:00
committed by GitHub
parent 9a4c030003
commit 9d3b0ff611
3 changed files with 12 additions and 5 deletions
+1
View File
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@tanstack/react-virtual` to fix warnings in React 19 projects ([#3588](https://github.com/tailwindlabs/headlessui/pull/3588))
- Fix `aria-invalid` attributes to have a valid `'true'` value ([#3639](https://github.com/tailwindlabs/headlessui/pull/3639))
- Add missing `invalid` prop to `Combobox` component ([#3677](https://github.com/tailwindlabs/headlessui/pull/3677))
- Fix `Unexpected undefined` crash in `Combobox` component with `virtual` mode ([#3678](https://github.com/tailwindlabs/headlessui/pull/3678))
## [2.2.0] - 2024-10-25
@@ -479,6 +479,7 @@ function VirtualProvider(props: {
children: (data: { option: unknown; open: boolean }) => React.ReactElement
}) {
let data = useData('VirtualProvider')
let d = useDisposables()
let { options } = data.virtual!
let [paddingStart, paddingEnd] = useMemo(() => {
@@ -528,6 +529,7 @@ function VirtualProvider(props: {
}}
ref={(el) => {
if (!el) {
d.dispose()
return
}
@@ -537,9 +539,13 @@ function VirtualProvider(props: {
}
// Scroll to the active index
if (data.activeOptionIndex !== null && options.length > data.activeOptionIndex) {
virtualizer.scrollToIndex(data.activeOptionIndex)
}
//
// Workaround for: https://github.com/TanStack/virtual/issues/879
d.nextFrame(() => {
if (data.activeOptionIndex !== null && options.length > data.activeOptionIndex) {
virtualizer.scrollToIndex(data.activeOptionIndex)
}
})
}}
>
{items.map((item) => {
@@ -108,7 +108,7 @@ function Example({ virtual = true, data, initial }: { virtual?: boolean; data; i
<Combobox.Options
transition
anchor="bottom start"
className="w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-1000 [--anchor-gap:theme(spacing.1)] [--anchor-max-height:theme(spacing.60)] focus:outline-none data-[closed]:opacity-0 sm:text-sm sm:leading-5"
className="w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-300 [--anchor-gap:theme(spacing.1)] [--anchor-max-height:theme(spacing.60)] focus:outline-none data-[closed]:opacity-0 sm:text-sm sm:leading-5"
>
{({ option }) => {
return (
@@ -157,7 +157,7 @@ function Example({ virtual = true, data, initial }: { virtual?: boolean; data; i
<Combobox.Options
transition
anchor="bottom start"
className="w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-1000 [--anchor-gap:theme(spacing.1)] [--anchor-max-height:theme(spacing.60)] focus:outline-none data-[closed]:opacity-0 sm:text-sm sm:leading-5"
className="w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-300 [--anchor-gap:theme(spacing.1)] [--anchor-max-height:theme(spacing.60)] focus:outline-none data-[closed]:opacity-0 sm:text-sm sm:leading-5"
>
{timezones.map((timezone, idx) => {
return (