-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
🙋 Documentation Request
Issue Description
When using Virtualizer from the react-aria-components package together with ListBox, I need to scroll the list to a specific item (scroll-to-item). However, due to the current virtualization implementation, this is problematic because:
Virtualizer wraps the list items in a div that is inaccessible from outside.
The parent ListBox has a fixed height, but the inner div (where the items actually live) becomes much taller and does not respect the parent’s boundaries.
Hence, there’s no direct way to call scrollTo, scrollIntoView, etc. on the desired element within the Virtualizer. Below is a shortened version of my code.
Код
import cn from 'clsx'
import { useTranslations } from 'next-intl'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { Button, ListBox, ListBoxItem, ListLayout, Virtualizer } from 'react-aria-components'
import { useForm } from 'react-final-form'
import { MdCheck as IconCheck } from 'react-icons/md'
import { ListBoxPropTypes } from '@/components/_fields/FieldSelectNew/_types'
import { styles } from '@/components/_fields/FieldSelectNew/stylesFieldSelectNew'
import { compareWithWrongKeyboardLayout } from '@/utils/compareWithWrongKeyboardLayout/compareWithWrongKeyboardLayout'
import { useCheckViewport } from '@/utils/useCheckViewport/useCheckViewport'
export function ListBoxSelect({
handleChange,
inputValue,
isInputChange,
isOpen,
name,
optionId,
options,
rounded,
updateSelectState
}: ListBoxPropTypes): JSX.Element {
const { isMobile } = useCheckViewport()
const t = useTranslations()
const form = useForm()
const itemRefs = useRef<Record<string, HTMLButtonElement | null>>({})
const getItemRef = (id: string | number) => (node: HTMLButtonElement | null) => {
if (!node) return
itemRefs.current[id.toString()] = node
}
const matchedComposers = useMemo(() => {
if (isInputChange) {
return options?.filter((composer) =>
compareWithWrongKeyboardLayout({
compareText: composer?.name,
fromFirstIndex: true,
ignoreNonAlphanumeric: false,
search: inputValue
})
)
}
return []
}, [inputValue, isInputChange, options])
const data = isInputChange ? matchedComposers : options
const onSelectionChange = useCallback((optionValue: string, optionName: string) => {
handleChange?.(optionValue)
form.batch(() => {
form.change(name, optionValue)
})
updateSelectState('optionId', optionValue)
updateSelectState('inputValue', optionName)
updateSelectState('isOpen', false)
}, [form, name, handleChange, updateSelectState])
useEffect(() => {
if (isOpen && optionId) {
setTimeout(() => {
const refEl = itemRefs.current[optionId.toString()]
if (refEl) {
// Attempt to scroll to the required element
refEl.scrollIntoView({
behavior: 'instant',
block: 'center'
})
}
}, 0)
}
}, [isOpen, optionId])
return (
<Virtualizer
layout={ListLayout}
layoutOptions={{ rowHeight: isMobile ? 46 : 40 }}
>
<ListBox
aria-label='Virtualized ListBox'
className={cn(
styles.rounded[rounded],
'max-h-280 block w-full overflow-auto p-0 outline-none'
)}
items={data}
renderEmptyState={() => (
<span className={cn(
'h-46 text-16 flex w-full max-w-full items-center px-16 py-2 text-left font-medium',
'sm:px-20'
)}>
{t('common.select.emptyText')}
</span>
)}
>
{(item) => (
<ListBoxItem
className={'group flex h-full w-full select-none items-center rounded outline-none'}
data-id={item?.id}
key={`${item?.id}key${item?.name}`}
>
{({ isSelected }) => (
<Button
className={cn(
'text-16 transition-bg relative flex h-full w-full max-w-full cursor-pointer items-center px-16 text-left font-medium sm:px-20',
item?.id?.toString() === optionId?.toString()
? 'bg-purple-light text-white'
: 'hover:bg-purple-lightest text-black'
)}
onPress={() => onSelectionChange(item?.id, item?.name)}
ref={getItemRef(item.id)}
>
<span className={'group-selected:font-medium flex flex-1 items-center gap-3 truncate font-normal'}>
<span className={'truncate'}>
{item?.name}
</span>
</span>
{isSelected && (
<span className={'text-20 flex items-center text-sky-600 group-focus:text-white'}>
<IconCheck className={'h-24 w-24'} />
</span>
)}
</Button>
)}
</ListBoxItem>
)}
</ListBox>
</Virtualizer>
)
}
Current Behavior
The list is virtualized.
The scrollIntoView() method is called, but it does not work as expected: the element does not scroll into the correct position within the list, likely because the virtualized container is “taller” and not strictly bound to the parent’s boundaries.
Expected Behavior
Ability to control scrolling within the virtualized list: for example, “scroll” to the needed item by optionId or call a method/callback that does it inside the Virtualizer.
Optionally, extend or add a built-in API that allows passing scrollToItem() or a similar method without having to go through the inner div.
Question
Will Virtualizer (or the relevant part of the library) include the ability to scroll to a specific item (scroll-to-item) in the near future? If so, is there any timeframe or example of how to achieve this with the library?
I would greatly appreciate any advice or ready-made solution on how to implement “scroll to item” functionality using Virtualizer.
Additional Information
React: 19.0.0
react-aria-components: 1.7.1
OS: MacOS

Browsers: Chrome
P.S. Thank you in advance for any help or guidance on how to implement this feature!
🧢 Your Company/Team
No response