如果另一个模态从视图中移出,则模态不会过渡到视图中

时间:2019-04-03 06:54:48

标签: reactjs css-transitions react-hooks

我正在尝试在弹出式菜单被召唤和关闭时将其移入和移出视图。

最初表示为null的点击,将隐藏的组件安装起来,然后触发CSS中定义的过渡,以将弹出窗口过渡到视图中。

在安装时,该组件会在文档上注册一个单击处理程序,并在弹出窗口之外监听单击,以便首先将其移出视图,然后将其完全卸载,并同时删除事件侦听器。

转换是通过更改组件的style属性来触发的,但是我也尝试过使用className来产生完全相同的结果。

import { useRef, useState, useEffect } from 'react'

/*
 * Popup
 *
 * - [x] auto-dismiss on click outside without hogging the click event
 *       (i.e. without using `stopPropagation()`)
 * - [ ] transition into and out of view
 *       ! No transition when opening a popup while another is still transitionning out out view
 */
function Popup ({ dismiss }) {
  const popupRef = useRef(null)
  const [style, setStyle] = useState(hiddenStyles)
  useEffect(() => {
    setStyle(showingStyles)
  }, [])
  useEffect(() => {
    global.document.addEventListener('click', onClickOutside, false)
    return () => {
      global.document.removeEventListener('click', onClickOutside, false)
    }
  }, [])
  function onClickOutside (event) {
    if (!popupRef.current.contains(event.target)) {
      setStyle(hiddenStyles)
      setTimeout(dismiss, 900) // TODO Find better way to dismiss (unmount) popup on animation end (and move this responsibility to the Item?)
    }
  }
  return (
    <div
      className='popup'
      ref={popupRef}
      style={style}
    >
      <style jsx>{`
        .popup {
          z-index: 1;
          color: black;
          background: white;
          border: 1px solid black;
          position: absolute;
          transition: opacity 0.9s ease;
        }
      `}</style>
      <pre>{JSON.stringify(style, null, 2)}</pre>
    </div>
  )
}

/*
 * Popup-producing item
 *
 * - [x] only render popup when wanted, unmount when dismissed
 */
const hiddenStyles = { opacity: 0 }
const showingStyles = { opacity: 1 }
function Item ({ id, body }) {
  const [showActions, setShowActions] = useState(false)

  function openActions () {
    setShowActions(true)
  }

  function hideActions () {
    setShowActions(false)
  }

  return (
    <li className='row' onClick={openActions}>
      <style jsx>{`
        .row {
          position: relative;
          cursor: pointer;
          padding: 5px;
        }
        .row:hover {
          background: #d636e9;
          color: #ffe2f0;
        }
      `}</style>
      {body}
      {showActions
        ? (
          <Popup dismiss={hideActions} />
        ) : null}
    </li>
  )
}

当我分别打开弹出窗口时,花时间在打开下一个窗口之前先将其关闭,然后进行过渡。但是,如果我在另一个弹出窗口完全消失之前打开一个弹出窗口,则过渡会从一开始就停留在最终状态。

问题是为什么?

1 个答案:

答案 0 :(得分:0)

stackoverflow片段工具不支持React 16.8,因此我将代码从钩子重构为类,并偶然发现了一个解决方案,这在钩子世界中也适用:替换

useEffect(() => {
    setStyle(showingStyles)
  }, [])

useEffect(() => {
  setTimeout(() => {
    setStyle(showingStyles)
  }, 10)
}, [])

我无法使用钩子显示完整的功能代码,但此处使用类显示工作版本:

/*
 * Popup
 *
 * - [x] auto-dismiss on click outside without hogging the click event
 *       (i.e. without using `stopPropagation()`)
 * - [ ] transition into and out of view
 *       ! No transition when opening a popup while another is still transitionning out out view
 */
class Popup extends React.Component {
  constructor (props) {
    super(props)
    this.onClickOutside = this.onClickOutside.bind(this)
    
    this.popupRef = null
    this.state = {
      style: hiddenStyles
    }
  }
  
  componentDidMount () {
    setTimeout(() => {
      this.setState({ style: showingStyles })
    }, 10)
    document.addEventListener('click', this.onClickOutside, false)
  }
  
  componentWillUnmount () {
    document.removeEventListener('click', this.onClickOutside, false)
  }

  onClickOutside (event) {
    if (!this.popupRef.contains(event.target)) {
      this.setState({ style: hiddenStyles })
      setTimeout(this.props.dismiss, 900) // TODO Find better way to dismiss (unmount) popup on animation end (and move this responsibility to the Item?)
    }
  }
  render () {
    return (
      <div
        className='popup'
        ref={(el) => {
          this.popupRef = el
        }}
        style={this.state.style}
      >
        <pre>{JSON.stringify(this.state.style, null, 2)}</pre>
      </div>
    )
  }
}

/*
 * Popup-producing item
 *
 * - [x] only render popup when wanted, unmount when dismissed
 */
const hiddenStyles = { opacity: 0 }
const showingStyles = { opacity: 1 }
class Item extends React.Component {
  constructor (props) {
    super(props)
    this.openActions = this.openActions.bind(this)
    this.hideActions = this.hideActions.bind(this)
    
    this.state = {
      showActions: false
    }
  }

  openActions () {
    this.setState({ showActions: true })
  }

  hideActions () {
    this.setState({ showActions: false })
  }

  render () {
    const { body } = this.props
    return (
      <li className='row' onClick={this.openActions}>
        {body}
        {this.state.showActions
          ? (
            <Popup dismiss={this.hideActions} />
          ) : null}
      </li>
    )
  }
}

/*
 * App
 *
 * - [x] Show list of items
 */
const items = [
  { id: 'a', body: 'Green!' },
  { id: 'b', body: 'Green!' },
  { id: 'c', body: 'Yellow?' },
  { id: 'd', body: 'Red' },
  { id: 'e', body: 'Gray' }
]
class App extends React.Component {
  render () {
    return (
      <ul>
        {items.map((item) => {
          return <Item {...item} key={item.id} />
        })}
      </ul>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('app'))
.row {
  position: relative;
  cursor: pointer;
  padding: 5px;
}
.row:hover {
  background: #d636e9;
  color: #ffe2f0;
}
.popup {
  z-index: 1;
  color: black;
  background: white;
  border: 1px solid black;
  position: absolute;
  transition: opacity 0.9s ease;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app" />

我根本不喜欢这种解决方案,因为我不理解为什么需要setTimeout,但至少可以使用...