加载时如何滚动React页面?

时间:2019-03-22 15:41:31

标签: reactjs react-select

我想在URL(myapp.com#somevalue)中传递一个值作为哈希值,并在页面加载时让页面滚动到该项目-正是自互联网开始以来使用哈希片段的行为。我尝试使用scrollIntoView,但在iOS上失败。然后,我尝试仅取消设置/设置window.location.hash,但似乎存在竞争条件。仅当延迟超过600毫秒时有效。

我想要一个更坚实的解决方案,并且不想引入不必要的延迟。当延迟太短时,它似乎滚动到所需的项目,然后滚动到页面顶部。您不会看到此演示的效果,但是会在我的实际应用https://codesandbox.io/s/pjok544nrx第75行

中发生
  componentDidMount() {
    let self = this;

    let updateSearchControl = hash => {
      let selectedOption = self.state.searchOptions.filter(
        option => option.value === hash
      )[0];
      if (selectedOption) {
        // this doesn't work with Safari on iOS
        // document.getElementById(hash).scrollIntoView(true);

        // this works if delay is 900 but not 500 ms
        setTimeout(() => {
          // unset and set the hash to trigger scrolling to target
          window.location.hash = null;
          window.location.hash = hash;
          // scroll back by the height of the Search box
          window.scrollBy(
            0,
            -document.getElementsByClassName("heading")[0].clientHeight
          );
        }, 900);
      } else if (hash) {
        this.searchRef.current.select.focus();
      }
    };

    // Get the hash
    // I want this to work as a Google Apps Script too which runs in
    // an iframe and has a special way to get the hash
    if (!!window["google"]) {
      let updateHash = location => {
        updateSearchControl(location.hash);
      };
      eval("google.script.url.getLocation(updateHash)");
    } else {
      let hash = window.location.hash.slice(1);
      updateSearchControl(hash);
    }
  }

编辑:我追踪了重新渲染页面的React行,因此在它已经滚动到我告诉它在componentDidMount()中的位置之后,重置了滚动位置。是this onestyle[styleName] = styleValue;在重新渲染页面时,它正在设置反应选择框组件的输入框组件的width样式属性。在执行此操作之前的堆栈跟踪看起来像

(anonymous) @   VM38319:1
setValueForStyles   @   react-dom.development.js:6426
updateDOMProperties @   react-dom.development.js:7587
updateProperties    @   react-dom.development.js:7953
commitUpdate    @   react-dom.development.js:8797
commitWork  @   react-dom.development.js:17915
commitAllHostEffects    @   react-dom.development.js:18634
callCallback    @   react-dom.development.js:149
invokeGuardedCallbackDev    @   react-dom.development.js:199
invokeGuardedCallback   @   react-dom.development.js:256
commitRoot  @   react-dom.development.js:18867
(anonymous) @   react-dom.development.js:20372
unstable_runWithPriority    @   scheduler.development.js:255
completeRoot    @   react-dom.development.js:20371
performWorkOnRoot   @   react-dom.development.js:20300
performWork @   react-dom.development.js:20208
performSyncWork @   react-dom.development.js:20182
requestWork @   react-dom.development.js:20051
scheduleWork    @   react-dom.development.js:19865
scheduleRootUpdate  @   react-dom.development.js:20526
updateContainerAtExpirationTime @   react-dom.development.js:20554
updateContainer @   react-dom.development.js:20611
ReactRoot.render    @   react-dom.development.js:20907
(anonymous) @   react-dom.development.js:21044
unbatchedUpdates    @   react-dom.development.js:20413
legacyRenderSubtreeIntoContainer    @   react-dom.development.js:21040
render  @   react-dom.development.js:21109
(anonymous) @   questionsPageIndex.jsx:10
./src/client/questionsPageIndex.jsx @   index.html:673
__webpack_require__ @   index.html:32
(anonymous) @   index.html:96
(anonymous) @   index.html:99

我不知道将指令滚动到何处。设置这些样式后,它必须发生在componentDidMount()之后。

编辑:我需要更好地明确说明我需要执行的操作。很抱歉以前没有这样做。

必须拥有

这必须在所有常见的台式机和移动设备上起作用。

页面加载时,可能会出现三种情况,具体取决于#之后的网址中提供的查询:

  • 1)没有提供查询-没有任何反应
  • 2)提供了有效的查询-页面立即滚动到所需的锚点,页面的标题更改为选项的标签
  • 3)提供了无效的查询-将查询输入到搜索框中,搜索框处于焦点状态,并且菜单打开时,其选项受所提供的查询的限制,就好像键入了它们一样。光标位于查询末尾的搜索框,以便用户可以修改查询。

有效查询是与选项之一的值匹配的查询。无效查询就是无效查询。

浏览器的前进和后退按钮应相应地移动滚动位置。

每当从“搜索”框中选择一个选项时,页面应立即滚动到该锚点,查询应清除并返回到占位符“ Search ...”,URL应更新,并且文档标题应更改为选项的标签。

可选,但很好

选择后,菜单关闭,查询返回到“搜索...”,或者选择

  • “搜索”框将保留焦点,以便用户可以开始键入另一个查询。下次单击时,菜单打开,并且恢复选择之前的Searchbox查询,菜单的过滤器和菜单滚动位置(首选),或者
  • 搜索框失去焦点。下次聚焦时,菜单将打开,并且将恢复(首选)从菜单选择,菜单的过滤器和菜单滚动位置开始的搜索框查询,或者
  • “搜索”框将保留焦点,以便用户可以开始键入另一个查询。先前的查询和菜单滚动位置将丢失。

4 个答案:

答案 0 :(得分:1)

使用{ path: 'posts', canActivate: [AuthGuardService], children: [ { path: '', component: HomeView, }, { path: 'new', component: PostItDialogContainerComponent, outlet: 'modal' }, { path: ':id/edit', component: PostItDialogContainerComponent, outlet: 'modal' } ]} 库可能更容易做到这一点:

react-scrollable-anchor

您可以通过以下方法查看它的工作:https://zwoj0xw503.codesandbox.io/#doge

答案 1 :(得分:1)

我在导入之后将其放在组件文件中:

let initialValidSlug = '';

window.addEventListener('load', () => {
  window.location.hash = '';
  window.location.hash = (initialValidSlug ? initialValidSlug : '');
  window.scrollBy(0, -document.getElementsByClassName("heading")[0].clientHeight);
  console.log('window.addEventListener')
});

并将其用于我的componentDidMount()

  componentDidMount() {
    const { pathname, isValid } = this.state;
    if(!isValid && pathname) {               // if there's a pathname but it's invalid,
      this.searchRef.current.select.focus(); // focus on the search bar
    }
    initialValidSlug = pathname;  // sets the value to be used in the $.ready() above
  }

答案 2 :(得分:0)

您可以通过将react-router-dom的{​​{3}}与React的ref.current.scrollIntoView()window.scrollTo(ref.currrent.offsetTop)结合使用来简化代码。

当前经过测试并可以在Chrome(台式机/ android),Firefox(台式机),Silk Browser(android),Saumsung Internet(android)和Safari(iOS)上运行,但需要注意的是,它会立即跳转到所选内容。虽然我能够测试并确认它在沙盒环境的iframe中正常运行,但是您必须在独立的iframe中对其进行改编/测试。

我还要指出,您的方法非常像jQuery,并且您应该避免直接尝试利用或操纵DOM和/或window。另外,您似乎在整个代码中使用了许多let变量,但是它们保持不变。相反,它们应该是const变量,因为React每次重新渲染时都会评估变量,因此,除非您计划在重新渲染过程中更改该变量,否则无需使用let

工作示例HashRouter

https://v6y5211n4l.codesandbox.io


组件/滚动条

import React, { Component } from "react";
import PropTypes from "prop-types";
import Helmet from "react-helmet";
import Select from "react-select";
import { withRouter } from "react-router-dom";
import "./styles.css";

const scrollOpts = {
  behavior: "smooth",
  block: "start"
};

class ScrollBar extends Component {
  constructor(props) {
    super(props);

    const titleRefs = props.searchOptions.map(
      ({ label }) => (this[label] = React.createRef())
    ); // map over options and use "label" as a ref name; ex: this.orange
    this.topWindow = React.createRef();
    this.searchRef = React.createRef();
    const pathname = props.history.location.pathname.replace(/\//g, ""); // set current pathname without the "/"

    this.state = {
      titleRefs,
      menuIsOpen: false,
      isValid: props.searchOptions.some(({ label }) => label === pathname), // check if the current pathname matches any of the labels
      pathname
    };
  }

  componentDidMount() {
    const { pathname, isValid } = this.state;
    if (isValid) this.scrollToElement(); // if theres a pathname and it matches a label, scroll to it
    if (!isValid && pathname) this.searchRef.current.select.focus(); // if there's a pathname but it's invalid, focus on the search bar
  }

  // allows us to update state based upon prop updates (in this case
  // we're looking for changes in the history.location.pathname)
  static getDerivedStateFromProps(props, state) {
    const nextPath = props.history.location.pathname.replace(/\//g, "");
    const isValid = props.searchOptions.some(({ label }) => label === nextPath);
    return state.pathname !== nextPath ? { pathname: nextPath, isValid } : null; // if the path has changed, update the pathname and whether or not it is valid
  }

  // allows us to compare new state to old state (in this case
  // we're looking for changes in the pathname)
  componentDidUpdate(prevProps, prevState) {
    if (this.state.pathname !== prevState.pathname) {
      this.scrollToElement();
    }
  }

  scrollToElement = () => {
    const { isValid, pathname } = this.state;
    const elem = isValid ? pathname : "topWindow";
    setTimeout(() => {
     window.scrollTo({
        behavior: "smooth",
        top: this[elem].current.offsetTop - 45
      });
    }, 100);
  };

  onInputChange = (options, { action }) => {
    if (action === "menu-close") {
      this.setState({ menuIsOpen: false }, () =>
        this.searchRef.current.select.blur()
      );
    } else {
      this.setState({ menuIsOpen: true });
    }
  };

  onChange = ({ value }) => {
    const { history } = this.props;
    if (history.location.pathname.replace(/\//g, "") !== value) { // if the select value is different than the previous selected value, update the url -- required, otherwise, react-router will complain about pushing the same pathname/value that is current in the url
      history.push(value);
    }
    this.searchRef.current.select.blur();
  };

  onFocus = () => this.setState({ menuIsOpen: true });

  render() {
    const { pathname, menuIsOpen, titleRefs } = this.state;
    const { searchOptions, styles } = this.props;

    return (
      <div className="container">
        <Helmet title={this.state.pathname || "Select an option"} />
        <div className="heading">
          <Select
            placeholder="Search..."
            isClearable={true}
            isSearchable={true}
            options={searchOptions}
            defaultInputValue={pathname}
            onFocus={this.onFocus}
            onInputChange={this.onInputChange}
            onChange={this.onChange}
            menuIsOpen={menuIsOpen}
            ref={this.searchRef}
            value={null}
            styles={styles || {}}
          />
        </div>
        <div className="fruits-list">
          <div ref={this.topWindow} />
          {searchOptions.map(({ label, value }, key) => (
            <div key={value} id={value}>
              <p ref={titleRefs[key]}>{label}</p>
              {[1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14].map(num => (
                <p key={num}>{num}</p>
              ))}
            </div>
          ))}
        </div>
      </div>
    );
  }
}

ScrollBar.propTypes = {
  searchOptions: PropTypes.arrayOf(
    PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  styles: PropTypes.objectOf(PropTypes.func)
};

export default withRouter(ScrollBar);

index.js

import React from "react";
import { render } from "react-dom";
import { HashRouter } from "react-router-dom";
import Helmet from "react-helmet";
import ScrollBar from "../src/components/ScrollBar";

const config = {
  htmlAttributes: { lang: "en" },
  title: "My App",
  titleTemplate: "Scroll Search - %s",
  meta: [
    {
      name: "description",
      content: "The best app in the world."
    }
  ]
};

const searchOptions = [
  {
    label: "apple",
    value: "apple"
  },
  {
    label: "orange",
    value: "orange"
  },
  {
    label: "banana",
    value: "banana"
  },
  {
    label: "strawberry",
    value: "strawberry"
  }
];

const styles = {
  menu: base => ({ ...base, width: "100%", height: "75vh" }),
  menuList: base => ({ ...base, minHeight: "75vh" }),
  option: base => ({ ...base, color: "blue" })
};

const App = () => (
  <HashRouter>
    <Helmet {...config} />
    <ScrollBar searchOptions={searchOptions} styles={styles} />
  </HashRouter>
);

render(<App />, document.getElementById("root"));

答案 3 :(得分:-1)

您可以使用https://www.npmjs.com/package/element.scrollintoviewifneeded-polyfill

只要您的应用程序中有可能,就导入库

import 'element.scrollintoviewifneeded-polyfill';

比起您可以在所有dom节点上使用方法scrollIntoViewIfNeeded();而言。 该库将填充该方法,以便您可以在所有浏览器中使用此方法。

我建议您将其与react ref一起使用,这样就不必调用getElementById或其他文档方法来检索dom节点。

const ref = React.createRef();
<div id="somevalue" ref={ref} />

与您的componentDidMount相比,您可以使用与您描述的方法相同的方法来检索要滚动的div的ID,获取相关的引用并调用

    ref.current.scrollIntoViewIfNeeded();

在引用是正确的div引用的地方,您显然可以将所有引用存储在地图中,而不是在组件方法中进行检索。