【问题标题】:Clicking link from focused side nav opens page scrolled down单击焦点侧导航中的链接打开向下滚动的页面
【发布时间】:2020-02-28 03:01:16
【问题描述】:

我继承了一个 react/node/prismic 应用程序,它有一个 ScrollToTop.js 文件,以确保页面加载在顶部,当从我们的主导航菜单访问时它们会这样做。

此应用程序还有一个小侧导航,在您向下滚动之前它会一直隐藏(由 tabIndex 控制)。

错误:当您从侧面导航点击链接时,无论您在打开侧面导航时滚动了多远,结果页面都会出现。相反,我希望这些从顶部开始。

我们有一个用于整体布局的 Layout.js 文件,以及用于那个小侧导航的特定 SideNav.js。我是 react/javascript 的新手,我无法弄清楚如何(a)将 ScrollToTop 逻辑应用于这些 sidenav 链接或(b)为这种特殊情况添加额外的 window.scrollTo(0,0)。谁能推荐如何/在哪里可以更新?

SideNav.js:

import React from 'react'
import CTAButton from './CTAButton'
import { Link } from 'react-router-dom'
import { Link as PrismicLink } from 'prismic-reactjs'
import PrismicConfig from '../../../prismic-configuration'
import PropTypes from 'prop-types'
import * as PrismicTypes from '../Utils/Types'

const matchPaths = {
  'about': 'About us',
  'projects': 'Projects',
  'news': 'News'
}

class SideNav extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      expanded: false
    }
    this.handleKey = this.handleKey.bind(this)
    this.expandMenu = this.expandMenu.bind(this)
    this.keypressed = undefined
    this.main = undefined
    this.bug = undefined
    this.toggle = false
    this.windowTop = 0
  }

  checkMobile() {
    if (window.innerWidth <= 800) {
      this.setState({scrollThreshold: 50})
    }
  }

  componentDidMount() {
    this.checkMobile()
    this.keypressed = this.handleKey
    window.addEventListener('keydown', this.keypressed)
    this.main = document.getElementsByClassName('top-nav-logo')[0]
    this.bug = document.getElementsByClassName('side-nav-bug-wrap')[0]

  }

  componentWillUnMount() {
    window.removeEventListener('keydown', this.keypressed)
  }

  handleKey(e) {
    if (e.keyCode === 27 && this.state.expanded) {
      this.expandMenu()
    }
  }

  expandMenu() {
    const el = document.scrollingElement || document.documentElement
    if (!this.state.expanded) {
      this.windowTop = el.scrollTop
    } else {
      el.scrollTop = this.windowTop
    }

    this.setState({
      expanded: !this.state.expanded
    })
    document.body.classList.toggle('nav-lock')
  }

  render() {
    const expanded = this.state.expanded ? 'expanded' : 'contracted'
    const tabIndex = this.state.expanded ? '1' : '-1'

    if (this.state.scrollThreshold === 50 && this.state.expanded) {
      this.props.removeListener()
      this.main.setAttribute('style', 'display:none')
      this.bug.setAttribute('style', 'display:none; position:absolute')
    }
    else if (this.state.scrollThreshold === 50 && !this.state.expanded) {
      this.props.addListener()
      this.main.setAttribute('style', 'display:inline')
      this.bug.setAttribute('style', 'display:flex; position:fixed')
    }

    const menu = this.props.menu.map((menuItem, index) => {
      const menuLink = PrismicLink.url(menuItem.link, PrismicConfig.linkResolver)
      const label = menuItem.label[0].text
      let marker
      if (typeof this.props.location !== 'undefined') {
        // Match label to window location to move indicator dot
        marker = label === matchPaths[this.props.location] ? this.props.location : 'inactive'
      }
      return (
        <li key={index} className="side-nav-li">
          <Link to={label} className="side-nav-link" onClick={this.expandMenu} tabIndex={tabIndex}>{label}</Link>
             <div className={`side-nav-marker ${marker}`}/>
        </li>
      )
    })

    return (
      <div className='side-nav-wrapper'>
        <Link to='/' className={`side-nav-logo-main ${this.props.visibility}`} alt=""/>
        <div className={`side-nav-bug-wrap ${this.props.visibility}`} onClick={this.expandMenu}>
          <div className='side-nav-bug-icon' />
        </div>
        <div className={`side-nav ${expanded}`}>
            <div className={'side-nav-menu-wrap'}>
              <div className="side-nav-control-wrap">
                <Link to="/" className="side-nav-logo" alt="Count Me In logo" onClick={this.expandMenu} tabIndex={tabIndex}/>
                <button className="side-nav-exit" onClick={this.expandMenu} tabIndex={tabIndex}/>
              </div>
              <nav>
                <ul className="side-nav-menu-items">
                  { menu }
                  <CTAButton />
                </ul>
              </nav>
            </div>
          </div>
        <div className={`side-nav-overlay ${expanded}`} onClick={this.expandMenu}/>
      </div>
    )
  }
}


SideNav.propTypes = {
  removeListener: PropTypes.func,
  addListener: PropTypes.func,
  location: PropTypes.string,
  visibility: PropTypes.string,
  menu: PropTypes.arrayOf(PropTypes.shape({
      label: PrismicTypes.PrismicTextTypes,
      link: PropTypes.shape({
          id: PropTypes.string,
          isBroken: PropTypes.bool,
          lang: PropTypes.string,
          link_type: PropTypes.string,
          slug: PropTypes.string,
          tags: PropTypes.array,
          type: PropTypes.string,
          uid: PropTypes.string
      })
  }))
}

export default SideNav

Layout.js:

const matchPaths = {
  'about': 'About us',
  'projects': 'Projects',
  'news': 'News'
}

class Layout extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      location: undefined,
      displayMobile: false,
      visibility: 'offscreen',
      scrollThreshold: 100
    }

    this.onMobileClick = this.onMobileClick.bind(this)
    this.logoClick = this.logoClick.bind(this)
    this.scrollCheck = this.scrollCheck.bind(this)
    this.addScrollCheck = this.addScrollCheck.bind(this)
    this.removeScrollCheck = this.removeScrollCheck.bind(this)
    this.addChildScroll = this.addChildScroll.bind(this)
    this.removeChildScroll = this.removeChildScroll.bind(this)

    this.checkCount = 0
    this.childCheckCount = 0
    this.scrollCheckToggle = true
    this.childCheckToggle = true
  }

  getBodyScrollTop () {
    const el = document.scrollingElement || document.documentElement
    return el.scrollTop
  }

  // Need to do this on didMount so window object is available.
  componentDidMount() {
    const loc = window.location.pathname.substr(1)
    this.setState({ location: loc })

    //Hide the loader and display the site content.
    setTimeout(function() {
      document.body.classList.add('is-loaded')
    }, 50)

    this.addScrollCheck()

    if (this.getBodyScrollTop() > this.state.scrollThreshold) {
      this.setState({
        visibility: 'onscreen'
      })
    }    
  }

  addChildScroll() {
    if (this.childCheckCount < 1 ) {
      window.addEventListener('scroll', this.scrollCheck)
      this.childCheckCount++
      this.childCheckToggle = true
    } 
  }

  removeChildScroll() {
    if (this.childCheckCount === 1) {
      window.removeEventListener('scroll', this.scrollCheck)
      this.childCheckCount--
      this.childCheckToggle = false
    }
  }


  addScrollCheck() {
    if (this.checkCount < 1 ) {
      window.addEventListener('scroll', this.scrollCheck)
      this.checkCount++
      this.scrollCheckToggle = true
    } 
  }

  removeScrollCheck() {
    if (this.checkCount === 1) {
      window.removeEventListener('scroll', this.scrollCheck)
      this.checkCount--
      this.scrollCheckToggle = false
    }
  }

  componentWillUnMount() {
    this.removeScrollCheck()
  }

  scrollCheck() {
    const scrollPos = this.getBodyScrollTop()
    const newVis = scrollPos > this.state.scrollThreshold ? 'onscreen' : 'offscreen'
    const curVis = this.state.visibility
    newVis !== curVis && (
      this.setState({
        visibility: scrollPos > this.state.scrollThreshold ? 'onscreen' : 'offscreen'
      })
    )
  }

  // Need to do it again on didUpdate to handle nav updates
  componentDidUpdate() {
    let loc = window.location.pathname
    loc = loc.substr(1)
    if (loc !== this.state.location) {
      this.setState({ location: loc })
    }
  }

  // this class assignment sets all the mobile menu style changes & transitions
  onMobileClick() {
    // but we only want to do this setting on mobile
    if (window.innerWidth < 800) {
      this.scrollCheckToggle ? this.removeScrollCheck() : this.addScrollCheck()
      this.setState({
        displayMobile: this.state.displayMobile === true ? false : true
      })
      const appContainer = document.getElementsByClassName('app-container')[0]
      appContainer.classList.toggle('locked')
    }
  }

  logoClick() {
    if (this.state.displayMobile === true) {
      this.setState({displayMobile: false})
      const appContainer = document.getElementsByClassName('app-container')[0]
      appContainer.classList.toggle('locked')
    }
  }

  render() {
    const mobileDisplay = this.state.displayMobile === true ? '-active' : '-inactive'
    const menu = this.props.menu.map((menuItem, index) => {
      const menuLink = PrismicLink.url(menuItem.link, PrismicConfig.linkResolver)
      const label = menuItem.label[0].text
      let marker
      if (typeof this.state.location !== 'undefined') {
        // Match label to window location to move indicator dot
        marker = label === matchPaths[this.state.location] ? this.state.location : 'inactive'
      }
      return (
        <li key={index} className="top-nav-li">
          <Link to={label} className="top-nav-link" onClick={this.onMobileClick}>{label}</Link>
             <div className={`top-nav-marker ${marker}`} />
        </li>
      )
    })

    return (
      <div className="app-container">
        <Loader />
        <SideNav menu={this.props.menu} location={this.state.location} visibility={this.state.visibility} addListener={this.addChildScroll} removeListener={this.removeChildScroll}/>
        <header className='top-nav-container CONSTRAIN'>
          <Link to="/" className={`top-nav-logo ${this.state.visibility}`} onClick={this.logoClick} alt="Count Me In logo"/>
          <div className="top-nav-mobile-wrapper">
            <div className={'top-nav-mobile-title'} onClick={this.onMobileClick} tabIndex="0">
              <span className={`top-nav-mobile-title-text${mobileDisplay}`}>Menu</span>
              <div className={`top-nav-mobile-icon${mobileDisplay}`} />
            </div>
          </div>
          <nav className={`top-nav-menu-container${mobileDisplay}`}>
            <ul className="top-nav-ul">
              {menu}
              <CTAButton/>
            </ul>
          </nav>
        </header>
        <main>
        {this.props.children}
      </main>
      <Footer projects={this.props.projects} footerData={this.props.footerData} />
      </div>
    )
  }
}

Layout.propTypes = {
    children: PropTypes.node,
    menu: PropTypes.arrayOf(PropTypes.shape({
        label: PrismicTypes.PrismicTextTypes,
        link: PropTypes.shape({
            id: PropTypes.string,
            isBroken: PropTypes.bool,
            lang: PropTypes.string,
            link_type: PropTypes.string,
            slug: PropTypes.string,
            tags: PropTypes.array,
            type: PropTypes.string,
            uid: PropTypes.string
        })
    })),
    projects: PropTypes.arrayOf(PropTypes.shape({
      data: PropTypes.shape({
          project_name: PrismicTypes.PrismicTextTypes,
          learn_more_link: PropTypes.shape({
              link_type: PropTypes.string,
              target: PropTypes.string,
              url: PropTypes.string
          })
      })
    })),
    footerData: PropTypes.arrayOf(PropTypes.shape({
      label: PrismicTypes.PrismicTextTypes,
      link: PropTypes.shape({
        link_type: PropTypes.string,
        target: PropTypes.string,
        url: PropTypes.string
      })
    }))
}
export default Layout

ScrollToTop.js:

import React from 'react'

class ScrollToTop extends React.Component {
    componentDidUpdate(prevProps) {
        if (this.props.location !== prevProps.location) {
            window.scrollTo(0, 0)
        }
    }
    componentDidMount() {
        window.scrollTo(0, 0)
    }

    render() {
        return this.props.children
    }
}

export default (ScrollToTop)

router.js:

import React from 'react'
import { Route, Switch } from 'react-router-dom'
import ScrollToTop from './app/Utils/ScrollToTop'
import routes from './routes'

export default (({prismicCtx, PRISMIC_UNIVERSAL_DATA}) => {
  return (
    <ScrollToTop>
      <Switch>
        {routes(prismicCtx, PRISMIC_UNIVERSAL_DATA).map((route, index) => {
          const copyRoute = Object.assign({}, route)
          if (copyRoute.render) delete copyRoute.component
          return <Route key={`route-${index}`} {...copyRoute} />
        })}
      </Switch>
    </ScrollToTop>
  )
})

【问题讨论】:

  • 我猜 ScrollToTop 没有更新也没有得到location。我认为location 也是一个对象,所以你需要比较location.pathname 这是一个字符串。但是你需要使用withRouter看这篇帖子stackoverflow.com/a/58588297/7015138或者reacttraining.com/react-router/web/api/withRouter
  • 谢谢@Vl4dimyr!是的,这是另一个问题:这个分支用于更新一堆包版本,随着这些更新,我们开始得到“你不应该在 之外使用 或 withRouter()”。我删除了'withRouter',我正在尝试看看我是否可以通过其他方式让它工作......
  • 我认为Route的直接子组件或通过Route显示的组件将自动注入location,只需注销提供给Route的组件中的props可能这将起作用。

标签: javascript reactjs scrollto


【解决方案1】:

查看不同的 react 生命周期步骤,我为 componentWillUpdate 添加了相同的 scrollTo 行为(在 ScrollToTop.js 中),这些页面再次从该菜单加载到顶部!

    componentWillUpdate() {
    window.scrollTo(0, 0)
}

【讨论】:

    猜你喜欢
    • 2013-05-02
    • 2014-09-13
    • 2015-09-08
    • 1970-01-01
    • 1970-01-01
    • 2021-09-11
    • 1970-01-01
    • 2019-12-19
    • 1970-01-01
    相关资源
    最近更新 更多