Sunday, August 27, 2017

Scroll to bottom of an overflowing div in React

Leave a Comment

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> 

5 Answers

Answers 1

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:

<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> 

Then the CSS

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%; } 

EDIT

And to scroll to the bottom of the list with javascript

var list = document.getElementById("list"); list.scrollTop = list.offsetHeight; 

Answers 2

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

Answers 3

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.

Answers 4

Simply use position:sticky s.t. the last list item is always shown.

li:last-child {   position:sticky;   bottom:0; } 

Demo

Caniuse

Answers 5

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;       } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment