I have a ul
with a max-height
and overflow-y: auto
.
When the user enters enough li
elements, the ul
starts to scroll, but I want the last li
with the form
in it to always be present and viewable to the user.
I've tried implementing a scrollToBottom
function that looks like this:
scrollToBottom() {
this.formLi.scrollIntoView({ behavior: 'smooth' });
}
But that just makes the ul
jump to the top of the screen and show the li
with the form in it as the only visible thing.
Is there a better way to accomplish this? Answers I find tend to be a bit older and use ReactDOM. Thanks!
CSS:
.prompt-box .options-holder {
list-style: none;
padding: 0;
width: 95%;
margin: 12px auto;
max-height: 600px;
overflow-y: auto;
}
HTML:
<ul className='options-holder'>
{
this.state.items.map((item, index) => (
<li key={item.id} className={`option ${index === 0 ? 'first' : ''}`}>
<div className='circle' onClick={this.removeItem} />
<p className='item-text'>{ item.text }</p>
</li>
))
}
<li key={0} className={`option form ${this.state.items.length === 0 ? 'only' : ''}`} ref={el => (this.formLi = el)}>
<div className='circle form' />
<form className='new-item-form' onSubmit={this.handleSubmit}>
<input
autoFocus
className='new-item-input'
placeholder='Type something and press return...'
onChange={this.handleChange}
value={this.state.text}
ref={(input) => (this.formInput = input)} />
</form>
</li>
</ul>
I have a container that fills the height of the page. This container has display flex and flex direction column. Then I have a Messages
component which is a ul
with one li
per message plus a special AlwaysScrollToBottom
component as the last child that, with the help of useEffect
, scrolls to the bottom of the list.
const AlwaysScrollToBottom = () => {
const elementRef = useRef();
useEffect(() => elementRef.current.scrollIntoView());
return <div ref={elementRef} />;
};
const Messages = ({ items }) => (
<ul>
{items.map(({id, text}) => <li key={id}>{text}</li>)}
<AlwaysScrollToBottom />
</ul>
)
const App = () => (
<div className="container">
<Messages items={[...]}>
<MessageComposer />
</div>
)
The CSS is
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
ul {
flex-grow: 1;
overflow-y: auto;
}
MessageComposer
has been omitted for brevity, it contains the form, the input field, etc. to send messages.
Use react-scroll library. However you will have to explicitly set ID of your ul
.
import { animateScroll } from "react-scroll";
scrollToBottom() {
animateScroll.scrollToBottom({
containerId: "options-holder"
});
}
You can call scrollToBottom
on setState callback.
For example
this.setState({ items: newItems}, this.scrollToBottom);
Or by using setTimeout.
Or you can even set Element
from react-scroll
and scroll to a certain li
.
Seems like you're building a chat-like interface. You can try wrapping the <ul>
and the div.circle-form
in a separate div like below:
ul{ border:1px solid red; height:13em; overflow-y:auto; position:relative; } ul li{ border-bottom:1px solid #ddd; list-style:none; margin-left:0; padding:6px; } .wrapper{ background:#eee; height:15em; position:relative; } .circle-form{ background:#ddd; height:2em; padding:3px; position:absolute; bottom:0; left:0; right:0; z-index:2; } .circle-form input[type=text]{ padding:8px; width:50%; }
<div class="wrapper"> <ul id="list"> <li>item</li> <li>item</li> <li>item</li> <li>item</li> <li>item</li> <li>item</li> <li>item</li> <li>item</li> </ul> <div class="circle-form"> <input type="text" name="type_here" placeholder="Enter something..." autofocus/> </div> </div>
EDIT
And to scroll to the bottom of the list with javascript
var list = document.getElementById("list");
list.scrollTop = list.offsetHeight;
I had a similar issue when it came to rendering an array of components that came from a user import. I ended up using the componentDidUpdate() function to get mine to work.
componentDidUpdate() {
// I was not using an li but may work to keep your div scrolled to the bottom as li's are getting pushed to the div
const objDiv = document.getElementById('div');
objDiv.scrollTop = objDiv.scrollHeight;
}
I have a script that I've used in one of my projects to scroll top Smoothly, I made a little refactor to scroll the height of your div (scroll bottom) I hope it helps.
scroll.js
function currentYPosition() {
if (self.pageYOffset) return self.pageYOffset;
if (document.documentElement && document.documentElement.scrollHeight) return document.documentElement.scrollHeight;
if (document.body.scrollHeight) return document.body.scrollHeight;
return 0;
}
function elmYPosition(eID) {
let elm = document.getElementById(eID);
let y = elm.offsetHeight;
let node = elm;
while (node.offsetParent && node.offsetParent != document.body) {
node = node.offsetParent;
y += node.offsetHeight;
}
return y;
}
export default function smoothScroll(eID, string) {
let startY = currentYPosition();
let stopY = elmYPosition(eID);
let distance = stopY > startY ? stopY - startY : startY - stopY;
let speed = Math.round(distance / 10);
let speedTimeout = 250;
if (speed >= 100) speed = 100;
if (string) speed = 1;
let step = Math.round(distance / 25);
let leapY = stopY > startY ? startY + step : startY - step;
let timer = 0;
if (stopY > startY) {
for (let i = startY; i < stopY; i += step) {
setTimeout('window.scrollTo(0, ' + leapY + ')', timer * speed);
leapY += step;
if (leapY > stopY) leapY = stopY;
timer++;
}
return;
}
for (let i = startY; i > stopY; i -= step) {
setTimeout('window.scrollTo(0, ' + (leapY) + ')', timer * speed);
leapY -= step;
if (leapY < stopY){
leapY = stopY;
}
timer++;
}
}
You should import this inside your component, there are 2 parameters(the ID of your element, in this case, you can use ref. The second one is a string that I've used to treat the speed of the scrolling.
import scroll from './your-path/scroll.js';
.
.
.
<ul className='options-holder'>
{
this.state.items.map((item, index) => (
<li key={item.id} className={`option ${index === 0 ? 'first' : ''}`} ref={el => (this.formLi = el)}>
<div className='circle' onClick={this.removeItem} />
<p className='item-text'>{ item.text }</p>
</li>
))
}
<li key={0} className={`option form ${this.state.items.length === 0 ? 'only' : ''}`} ref={el => (this.formLi = el)}>
<div className='circle form' />
<form className='new-item-form' onSubmit={this.handleSubmit}>
<input
autoFocus
className='new-item-input'
placeholder='Type something and press return...'
onChange={this.handleChange}
value={this.state.text}
ref={(input) => (this.formInput = input)} />
</form>
</li>
Idk how you are mapping this LI inside your render but you should make a verification and if there's the property Overflow you should run the scroll.
There's a reasonable answer for your component is jumping to the first element, you're hitting the ref for the FIRST element, not the last.
Possible workaround:
scroll(this.state.items[this.state.items.length - 1]);
Update 1: Gist of the original scroll.js, scrolling to the top
i solved it with a general utility function that works outside of any specific component. you need to define a ref on the parent (list box, multiline edit, whatever) and the last child element (or wherever you wish to scroll to).
export const scrollToRef = (parentRef, childRef) => {
trace("scrollToRef:ref.current.offsetTop", childRef.current.offsetTop);
parentRef.current.scrollTo(0, childRef.current.offsetTop)
}
Im using it a functional component and calling it as an effect whenever my parent container gets updated.
You can use "react-scroll-to-bottom" module to do it.
Auto scroll to bottom when new child added.
import ScrollToBottom from 'react-scroll-to-bottom';
<ScrollToBottom className={ROOT_CSS}>
<p>
Nostrud nisi duis veniam ex esse laboris consectetur officia et. Velit
cillum est veniam culpa magna sit
</p>
</ScrollToBottom>
The answer here didn't quite work for me because I am using CSS Grid instead of Flexbox so the added div
created another grid row.
To work around this I elaborated to create a Hook which takes a ref
and gets the last child to scrollIntoView()
.
import { useEffect } from 'react'
export const useScrollToBottom = ref => {
useEffect(() => {
ref?.current?.querySelector(':scope > :last-child')?.scrollIntoView()
}, [ref])
}
const Messages = ({ items }) => {
const scrollContainer = useRef()
useScrollToBottom(scrollContainer)
return (
<ul ref={scrollContainer}>
{items.map(({id, text}) => <li key={id}>{text}</li>)}
</ul>
)
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.