Some background, I'm attempting to create a message chat feed.
I have a NextUI ListBox
component that has the property isVirtualized
and the appropriate params. It is created using the following:
<>
<h1>{selectedServer ? selectedServer.serverAlias : "Chat Title"}</h1>
<ListboxWrapper ref={containerRef}>
<Listbox
isVirtualized
aria-label="Dynamic Actions"
items={chats}
selectedKeys={selectedKeys}
selectionMode="none"
onSelectionChange={setSelectedKeys}
virtualization={{
maxListboxHeight: 600,
itemHeight: 40,
}}
>
{(item) => (
<ListboxItem key={item.id}>
{item.text}
</ListboxItem>
)}
</Listbox>
</ListboxWrapper>
<Input
label="Send Message"
type={selectedServer ? "text" : "hidden"}
raduis={"full"}
onKeyUp={onKeyPressHandler}
value={textValue}
onChange={onChangeHandler}
/>
</>
And the ListboxWrapper
:
const ListboxWrapper = ({children}) => (
<div className="grid row-span-6 w-full h-full border-small px-1 py-2 rounded-small border-default-200 dark:border-default-100">
{children}
</div>
);
The items of the list are loaded dynamically. I would like the last item in the list to always be at the bottom of the containing div.
I currently have:
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({behavior: 'smooth' , block: 'end'})
}
Which scrolls to the top. It doesn't seem to matter what block I set (end
or start
), the behaviour is the same. Am I doing something wrong or is there an easier way to do this?
I had the same results with document.getElementById(..).scrollIntoView(..)
. I also moved the ref
to the last list item, but when I do that, the last list item appears at the top of the container and the other items are not visible and cannot be scrolled.
Thanks in advanced!
UPDATE:
I tried the solution suggested below but same behaviour.
It seems whenever I type text into the input box, the ListBox
scrolls to the top. I have updated the code above to include the full return
of the component.
The onKeyPressHandler
and onChangeHandler
functions with their hooks:
const [selectedKeys, setSelectedKeys] = React.useState(new Set(["text"]));
const selectedValue = React.useMemo(() => Array.from(selectedKeys).join(", "), [selectedKeys]);
const onKeyPressHandler = e => {
if (e.key === "Enter") {
const message = new ApiCall()
const chatRequest = new ChatRequest()
chatRequest.setChattext(textValue)
chatRequest.setServerid(selectedServer.serverID)
message.setChatrequest(chatRequest)
const apiCall = message.serializeBinary()
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/x-protobuf'},
body: apiCall
};
console.log("fetching chats")
fetch('http://localhost:8080/api/chats', requestOptions)
.then(response => {
if (response.ok) {
getChats()
}
changeTextValue("");
})
}
};
const onChangeHandler = e => {
changeTextValue(e.target.value);
};
And finally, the getChats
function, that gets the chats from the backend and calls scrollToBottom
when its complete:
const getChats = () => {
fetch("http://localhost:8080/api/chats?serverID=" + selectedServer.serverID)
.then(res => res.json())
.then(data => {
setChats(data);
scrollToBottom()
})
}
UPDATE2:
I added a console.log
to the scrollToBottom
function and it is only being called when the chats are updated. However, it isn't scrolling. Every time I type anything into the input box, the list scrolls to the top, almost like it is being redrawn?
UPDATE3:
I went back to the drawing board and did some research into grid and flex layouts. I have also switched from using the virtualised scroll that comes with ListBox
to css scrolling using overflow
, making the answer below work.
The issue arises because when working with virtualized lists, not all items in the list are rendered in the DOM simultaneously. Virtualization dynamically renders only the visible items to improve performance, so scrollIntoView might not behave as expected because the target item might not actually exist in the DOM.
To achieve the desired behavior of keeping the last item in view, you'll need to ensure that the virtualization engine correctly accounts for the scroll position. Here's how you can fix this manually:
Instead of relying on scrollIntoView, you can directly adjust the scroll position of the ListBox container: 1.Add a ref to the container: Attach a ref to the outer container of the ListBox.
const containerRef = useRef(null);
2.Scroll to the Bottom on Updates: After dynamically loading items, adjust the scroll position manually.
const scrollToBottom = () => {
const container = containerRef.current;
if (container) {
container.scrollTop = container.scrollHeight;
}
};
3.Trigger scrollToBottom: Use useEffect to scroll to the bottom whenever chats (your list of items) updates.
useEffect(() => {
scrollToBottom();
}, [chats]);
4.Attach the ref to the ListboxWrapper: Modify the ListboxWrapper to accept the ref.
const ListboxWrapper = React.forwardRef(({ children }, ref) => (
<div
ref={ref}
className="grid row-span-6 w-full h-full border-small px-1 py-2 rounded-small border-default-200 dark:border-default-100"
>
{children}
</div>
));
5.Pass the ref to the Wrapper: Use the containerRef when rendering the ListboxWrapper.
<ListboxWrapper ref={containerRef}>
<Listbox
isVirtualized
aria-label="Dynamic Actions"
items={chats}
selectedKeys={selectedKeys}
selectionMode="single"
onSelectionChange={setSelectedKeys}
virtualization={{
maxListboxHeight: 600,
itemHeight: 40,
}}
>
{(item) => (
<ListboxItem key={item.id}>
{item.text}
</ListboxItem>
)}
</Listbox>
</ListboxWrapper>
If you want smooth scrolling, use scrollTo with the behavior: 'smooth' option instead of directly setting scrollTop.
const scrollToBottom = () => {
const container = containerRef.current;
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
});
}
};