codecks-io / react-sticky-box

Sticky boxes for contents of all sizes
https://react-sticky-box.codecks.io/
ISC License
469 stars 44 forks source link

Allow passing in custom `getScrollParent` #7

Closed archive64 closed 6 years ago

archive64 commented 6 years ago

Problem

getScrollParent isn't working correctly for my particular use case:

(We're including the React app inside of a page written in a legacy framework. This problem occurs because of how we mount the React section of the page.)

Minimal working example

Fork of main example: https://codesandbox.io/s/x2kkkrjm8q

The relevant changes are:

const div = document.createElement("div");
ReactDOM.render(<Page />, div);

const scrollContainer = document.querySelector(".content-container");
scrollContainer.appendChild(div);

Suggested solution

This is a pretty specific edge case that probably isn't worth supporting in this library. That said, I'd like the ability to pass in a custom getScrollParent implementation. Then I could do:

<StickyBox getScrollParent={() => document.getElementById('#scroll-container')}>
danielberndt commented 6 years ago

The current way of identifying the scroll container doesn't rely on the scroll container to be created by react:

https://github.com/codecks-io/react-sticky-box/blob/67600d1ed56dfc5432c1f10a9738367457c36278/src/index.js#L4-L11

Once a StickyBox is mounted it uses its node's native .offsetParent to find the scroll container. So if the current implementation does not identify the correct scroll parent in your case, it might be that there's a more general error that's worth fixing!

archive64 commented 6 years ago

Hi @danielberndt, thank you for the response!

I think the "general error" is that node.offsetParent can change after getScrollParent runs. I added some console.logs to the example to clarify:

const div = document.createElement("div");
ReactDOM.render(<Page />, div); // getScrollParent runs here
console.log(div.offsetParent); // logs null

const scrollContainer = document.querySelector(".content-container");
scrollContainer.appendChild(div); // getScrollParent doesn't run again
console.log(div.offsetParent); // logs .content-container

A more general fix would be to re-run registerContainerRef if node.offsetParent changes. I'm not aware of a simple way to "watch" a node for offsetParent changes, though.

danielberndt commented 6 years ago

Okay, makes sense. Two workarounds. Only mount StickyBox after scrollContainer.appendChild.

You could use a component like this:

class DelayedMount extends Component {
  this.state = {mountChildren: false}
  componentDidMount() {
    setTimeout(() => this.setState({mountChildren: true}))
  }
  render() {
    return this.state.mountChildren ? this.props.children : null
  }
}

// use it like
<DelayedMount><StickyBox>...content...</StickyBox></DelayedMount>

Or you do something like this:

class Page extends React.Component {

  handleRef = (n) => {
    this.stickyBoxInstance = n;
  }

  onPageProperlyMounted = () => {
    // call this function to simulate unmounting and re-mounting the underlying node.

    this.stickyBoxInstance.registerContainerRef(null);
    this.stickyBoxInstance.registerContainerRef(this.stickyBoxInstance.node);
  }

  render() {
    return (
      <div>
        <StickyBox ref={this.handleRef}>
          {...contents}
        </StickyBox>
      </div>
    );
  }
}
archive64 commented 6 years ago

Nice, thank you for sharing workarounds! I tried the first one and it works for us. I'm happy to close this issue as won't fix.

danielberndt commented 6 years ago

Alright :)