Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dismissable Message Component inside a Popup closes the Popup itself #1580

Closed
mattiagalati opened this issue Apr 13, 2017 · 14 comments
Closed

Comments

@mattiagalati
Copy link

mattiagalati commented Apr 13, 2017

Steps

Open your docs at https://react.semantic-ui.com/modules/popup#popup-example-click
and paste the following code in editor to simulate a dismissable Message Component inside a Popup which is triggered by click on a Button

import React from 'react'
import { Button, Popup, Message } from 'semantic-ui-react'

class MessageExampleDismissibleBlock extends React.Component {
  state = { visible: true }

  handleDismiss = () => {
	this.setState({ visible: false })

	setTimeout(() => {
	  this.setState({ visible: true })
	}, 2000)
  }

  render() {
	if (this.state.visible) {
	  return (
		<Message
		  onDismiss={this.handleDismiss}
		  header='Welcome back!'
		  content='This is a special notification which you can dismiss.'
		/>
	  )
	}

	return (
	  <p>
		<br />
		<i>The message will return in 2s</i>
		<br />
		<br />
	  </p>
	)
  }
}

const PopupExampleClick = () => (
  <Popup
	trigger={<Button color='red' icon='flask' content='Activate doomsday device' />}
	on='click'
	position='top right'
  ><MessageExampleDismissibleBlock/></Popup>
)

export default PopupExampleClick

Expected Result

The message will be dismissed but the Popup remains opened

Actual Result

The Popup will be completely destroyed

Version

0.67.2

@levithomason
Copy link
Member

Strange, setting state immediately within the Message onDismiss handler causes the Popup to close, however, waiting one tick does not:

setTimeout(() => {
  this.setState({ visible: false })
}, 0)

This will need some investigation.

@mattiagalati
Copy link
Author

Since I was blocked in my project, I switched to a totally controlled Popup through the "onClose" and "onOpen" props. When i click on an element inside the Popup, and the onClose callback of Popup itself is fired, I check the event target to ensure that if the user is clicking a specific element, the popup should remain opened.

closePopup = (e) => {
	let mustClose = true;
	if(e.target.hasClass('icon') && e.target.hasClass('close')) {
		if(e.target.parentNode.hasClass('message')) {
			log.debug('User has dismissed a notification, no need to close popup');
			return;
		}
	}
	this.setState({ popupIsOpened: false })
}

openPopup = () => {
	this.setState({ popupIsOpened: true })
}

I know it's awful but it works

@jneto
Copy link

jneto commented Jul 13, 2017

I'm having the same issue and it doesn't seem right to me having to rely on setTimeout or having to check the target node of the onClose callback.

@levithomason Was there any further investigation on this? Is there anything I can do to help solve this?

@kevinswarner
Copy link

kevinswarner commented Jul 20, 2017

I am also having the same issue. The contents of my popup are another component that has state and gets re-rendered based on that state. Each time the state of the child component changes and the child component renders, the onClose is fired. I guess my question is why is the onClose fired when a child component of the Popup.Content is re-rendered?

The setTimeout workaround seems to work, but I wonder how reliable it will be.

@ecnaidar
Copy link
Contributor

Apparently connected issue.
If Popup opened from another Popup, any button click in a child Popup closes them both.

https://stackblitz.com/edit/react-bwfzfm

@levithomason
Copy link
Member

@ecnaidar this is due to all event handlers being added to the global scope and executed simultaneously. Pressing ESC will trigger close of all components listening for it.

That said, #1733 was just merged which solves this. Events are now handled in a stack, so only the top most Popup, Modal, Dropdown, etc. will close when ESC is pressed. I will be shipped today.

@levithomason
Copy link
Member

@levithomason Was there any further investigation on this? Is there anything I can do to help solve this?

I've done no investigation here and likely will not for a long time as it is very far down my priority chain.

Yes, you can definitely help out! See the CONTRIBUTING.md for info on setting up the project (pretty straight forward). You can run the docs for testing locally. Just update one of the examples to reproduce the issue, then debug the components involved (Popup, Modal). Open a PR when you figure it out :)

@levithomason
Copy link
Member

The setTimeout workaround seems to work, but I wonder how reliable it will be.

@kevinswarner Definitely agree, please do not rely on the setTimeout hack :) I have a hunch this bug has something to with the Portal which powers the Popup and Modal. The Portal mounts components after it is mounted, it takes two render cycles for the actual component (Popup/Modal) to mount. By waiting one tick, I was testing whether or not this two-step render process was involved. It appears it is. However, it is not meant to be a suggested fix or workaround.

@Francodes
Copy link

Francodes commented Sep 18, 2017

I ran across this bug today and was trying to figure out what the cause was and I think I have narrowed it down to the portal behavior when processing click events

I have recreated the bug here: https://stackblitz.com/edit/react-b5as56
It appears that the portal looks at the event target, and tries to check if the virtual DOM element exists within the Portal contents. If not, it goes ahead and closes the portal.

In the example above, If the click event target changes the contents of the Popup such that the target is no longer in the contents, thus not exist within the Popup, the portal click handler issues a close.

It seems that Virtual DOM diff-ing goes into play here as well because if the element that initiated the click event is replaced by a different element of exactly the same Markup building blocks (switching a <Button> with another <Button>), the virtual DOM only performs smart diffs to change that button (text changes only), but the element itself is not replaced and still lives within the DOM, which then the Portal click event handling behaves as expected.

Don't really know what the best solution to this would be, but I figured I would put my findings here in case someone wants to take a crack at it.

Note: The setTimeout approach works here because it eliminates the race condition where the click event handlers execute before the DOM element gets removed from the DOM (via conditional rendering with the use of setState), thus passing the if condition in the Portal click handler

@levithomason
Copy link
Member

levithomason commented Sep 23, 2017

This could be the problem precisely. I've run into this in the Datetime component #1240 also. It shows a calendar in a popup. We change the calendar (Table) on click of a Table Cell. This would force the Popup to close, even when the Popup was set to open/close on='click' of the trigger only.

The bug is exactly the race condition you describe. The Table Cell click event replaces the Popup contents, so when the Popup asks "Does the Popup contain the clicked node?" the answer is always no. Because the Table Cell that was clicked is no longer in the DOM and therefore no longer contained in the Popup. It was replaced with a new Table.

A Fix ?

I've not tested a solution for this yet, but I proposed an idea in this Datetime PR comment:

// TODO: Fix close on trigger blur, it closes when clicking inside the calendar.
// DatetimeCalendar contents are changed on click, so Popup cannot find the clicked node within the calendar.
// If the clicked node is not within the Portal, it is considered a "blur" and closes.
// Enable close on trigger blur after this is fixed.
// Portal should be able to identify clicks within the portal even with no e.target, perhaps using x y coords.

My final thought there, as you can see, is perhaps using click event x/y coordinates to determine if the click was within the bounds of the Popup. This way, it is not dependant on the child node still existing in the DOM.

@stale
Copy link

stale bot commented Feb 4, 2018

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 30 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Feb 4, 2018
@stale stale bot removed the stale label Feb 18, 2018
@levithomason
Copy link
Member

This is fixed in the latest release.

http://g.recordit.co/G2sSOC0T1o.gif

@aslafy-z
Copy link

I upgraded dependencies of @ecnaidar example, and it doesn't work: https://stackblitz.com/edit/react-f6puqr

@levithomason
Copy link
Member

There is no dismissable message in the example provided. Please file a new issue and complete the entire report.

@Semantic-Org Semantic-Org locked as resolved and limited conversation to collaborators Feb 26, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants