如何滚动 div 以在 ReactJS 中可见?

IT技术 javascript html css scroll reactjs
2021-04-22 11:47:17

我有一个div包含 childdiv垂直列表的弹出列表我添加了向上/向下键盘导航以更改当前突出显示的子项。

现在,如果我按下向下键足够多的次数,突出显示的项目将不再可见。如果滚动视图,向上键也会发生同样的事情。

React 中自动将孩子滚动div到视图中的正确方法是什么

6个回答

我假设您有某种List组件和某种Item组件。在一个项目中的做法是让项目知道它是否处于活动状态;如有必要,该项目会要求列表将其滚动到视图中。考虑以下伪代码:

class List extends React.Component {
  render() {
    return <div>{this.props.items.map(this.renderItem)}</div>;
  }

  renderItem(item) {
    return <Item key={item.id} item={item}
                 active={item.id === this.props.activeId}
                 scrollIntoView={this.scrollElementIntoViewIfNeeded} />
  }

  scrollElementIntoViewIfNeeded(domNode) {
    var containerDomNode = React.findDOMNode(this);
    // Determine if `domNode` fully fits inside `containerDomNode`.
    // If not, set the container's scrollTop appropriately.
  }
}

class Item extends React.Component {
  render() {
    return <div>something...</div>;
  }

  componentDidMount() {
    this.ensureVisible();
  }

  componentDidUpdate() {
    this.ensureVisible();
  }

  ensureVisible() {
    if (this.props.active) {
      this.props.scrollIntoView(React.findDOMNode(this));
    }
  }
}

更好的解决方案可能是让列表负责将项目滚动到视图中(项目不知道它甚至在列表中)。为此,您可以ref为某个项目添加一个属性并使用属性找到它:

class List extends React.Component {
  render() {
    return <div>{this.props.items.map(this.renderItem)}</div>;
  }

  renderItem(item) {
    var active = item.id === this.props.activeId;
    var props = {
      key: item.id,
      item: item,
      active: active
    };
    if (active) {
      props.ref = "activeItem";
    }
    return <Item {...props} />
  }

  componentDidUpdate(prevProps) {
    // only scroll into view if the active item changed last render
    if (this.props.activeId !== prevProps.activeId) {
      this.ensureActiveItemVisible();
    }
  }

  ensureActiveItemVisible() {
    var itemComponent = this.refs.activeItem;
    if (itemComponent) {
      var domNode = React.findDOMNode(itemComponent);
      this.scrollElementIntoViewIfNeeded(domNode);
    }
  }

  scrollElementIntoViewIfNeeded(domNode) {
    var containerDomNode = React.findDOMNode(this);
    // Determine if `domNode` fully fits inside `containerDomNode`.
    // If not, set the container's scrollTop appropriately.
  }
}

如果您不想通过数学运算来确定该项目在列表节点内是否可见,您可以使用DOM 方法scrollIntoView()或特定于 Webkit 的方法scrollIntoViewIfNeeded,它具有可用的 polyfill,因此您可以在非 Webkit 浏览器中使用它。

哇,Element.scrollIntoView()超级棒。
2021-05-23 11:47:17
不推荐将 ref 设置为字符串建议改用回调模式createRef API
2021-05-27 11:47:17
我展示了另一种类似的方法,我在列表底部添加一个空元素,我可以在这个答案中始终 scrollIntoView 到元素
2021-05-28 11:47:17
请注意:在 React 15.x 及更高版本中,您将需要使用ReactDom.findDOMNode 而不是React.findDOMNode
2021-05-31 11:47:17
谢谢,你的第一个例子是我想到的,但我更喜欢你的第二个例子,子节点不需要担心它。
2021-06-01 11:47:17

对于 React 16,正确答案与之前的答案不同:

class Something extends Component {
  constructor(props) {
    super(props);
    this.boxRef = React.createRef();
  }

  render() {
    return (
      <div ref={this.boxRef} />
    );
  }
}

然后滚动,只需添加(在构造函数之后):

componentDidMount() {
  if (this.props.active) { // whatever your test might be
    this.boxRef.current.scrollIntoView();
  }
}

注意:您必须使用“.current”,并且您可以向 scrollIntoView 发送选项:

scrollIntoView({
  behavior: 'smooth',
  block: 'center',
  inline: 'center',
});

(见http://www.albertgao.xyz/2018/06/07/scroll-a-not-in-view-component-into-the-view-using-react/

阅读规范,有点难以理解 block 和 inline 的含义,但是在玩弄它之后,我发现对于垂直滚动列表,block: 'end' 确保元素可见,而无需人为地滚动顶部我的内容离开视口。使用“center”,靠近底部的元素会向上滑动太多,其下方会出现空白空间。但是我的容器是一个带有 justify: 'stretch' 的 flex 父级,因此可能会影响行为。我没有进一步挖掘。隐藏溢出的元素会影响 scrollIntoView 的行为方式,因此您可能必须自己进行试验。

我的应用程序有一个必须在视图中的父级,如果选择了一个子级,它也会滚动到视图中。这很有效,因为父 DidMount 发生在孩子的 DidMount 之前,所以它滚动到父,然后当活跃的孩子被渲染时,进一步滚动以看到那个。

AFAIK,父母的 didMount 发生在孩子之后stackoverflow.com/questions/32814970/...
2021-05-24 11:47:17
是的,这是真的,@gaurav5430。不知道我在想什么!
2021-06-02 11:47:17
如果您收到“scrollIntoView not a function”的错误消息,请记住将 分配ref给标准 DOM 元素,例如div,而不是 React 组件。更多细节在这里
2021-06-02 11:47:17

另一个在 ref 中使用函数而不是字符串的例子

class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = { items:[], index: 0 };
    this._nodes = new Map();

    this.handleAdd = this.handleAdd.bind(this);
    this.handleRemove = this.handleRemove.bind(this);
   }

  handleAdd() {
    let startNumber = 0;
    if (this.state.items.length) {
      startNumber = this.state.items[this.state.items.length - 1];
    }

    let newItems = this.state.items.splice(0);
    for (let i = startNumber; i < startNumber + 100; i++) {
      newItems.push(i);
    }

    this.setState({ items: newItems });
  }

  handleRemove() {
    this.setState({ items: this.state.items.slice(1) });
  }

  handleShow(i) {
    this.setState({index: i});
    const node = this._nodes.get(i);
    console.log(this._nodes);
    if (node) {
      ReactDOM.findDOMNode(node).scrollIntoView({block: 'end', behavior: 'smooth'});
    }
  }

  render() {
    return(
      <div>
        <ul>{this.state.items.map((item, i) => (<Item key={i} ref={(element) => this._nodes.set(i, element)}>{item}</Item>))}</ul>
        <button onClick={this.handleShow.bind(this, 0)}>0</button>
        <button onClick={this.handleShow.bind(this, 50)}>50</button>
        <button onClick={this.handleShow.bind(this, 99)}>99</button>
        <button onClick={this.handleAdd}>Add</button>
        <button onClick={this.handleRemove}>Remove</button>
        {this.state.index}
      </div>
    );
  }
}

class Item extends React.Component
{
  render() {
    return (<li ref={ element => this.listItem = element }>
      {this.props.children}
    </li>);
  }
}

演示:https : //codepen.io/anon/pen/XpqJVe

为了建立在@Michelle Tilley 的回答之上,我有时想在用户的选择发生变化时滚动,所以我在componentDidUpdate. 我还做了一些数学计算来确定滚动多远以及是否需要滚动,对我来说如下所示:

  componentDidUpdate() {
    let panel, node;
    if (this.refs.selectedSection && this.refs.selectedItem) {
      // This is the container you want to scroll.          
      panel = this.refs.listPanel;
      // This is the element you want to make visible w/i the container
      // Note: You can nest refs here if you want an item w/i the selected item          
      node = ReactDOM.findDOMNode(this.refs.selectedItem);
    }

    if (panel && node &&
      (node.offsetTop > panel.scrollTop + panel.offsetHeight || node.offsetTop < panel.scrollTop)) {
      panel.scrollTop = node.offsetTop - panel.offsetTop;
    }
  }

以防万一有人在这里绊倒,我是这样做的

  componentDidMount(){
    const node = this.refs.trackerRef;
    node && node.scrollIntoView({block: "end", behavior: 'smooth'})
  }
  componentDidUpdate() {
    const node = this.refs.trackerRef;
    node && node.scrollIntoView({block: "end", behavior: 'smooth'})
  }

  render() {
    return (

      <div>
        {messages.map((msg, index) => {
          return (
            <Message key={index} msgObj={msg}
              {/*<p>some test text</p>*/}
            </Message>
          )
        })}

        <div style={{height: '30px'}} id='#tracker' ref="trackerRef"></div>
      </div>
    )
  }

scrollIntoView是原生 DOM 功能链接

它总是会显示trackerdiv