我正在尝试在召唤和关闭弹出窗口时将弹出窗口移入和移出视图。
最初呈现为 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>
)
}
当我单独打开弹出窗口时,在打开下一个弹出窗口之前花时间关闭一个弹出窗口,过渡会起作用。但是,如果我在另一个弹出窗口完全消失之前打开一个弹出窗口,则过渡从一开始就会卡在最终状态。
问题是为什么?
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
,但至少它可以工作......