Skip to content

Virtualizer-scroll-to-element #7986

@NarekPoghosyan

Description

@NarekPoghosyan

🙋 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

Image

Browsers: Chrome

P.S. Thank you in advance for any help or guidance on how to implement this feature!

🧢 Your Company/Team

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions