Do you want to request a feature or report a bug?
Bug 馃悶
What is the current behavior?
When you move a component into a React Portal in response to a touchstart the touchmove and touchend events are swallowed for the rest of the interaction
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
Example: https://codesandbox.io/s/nr75vkklnm
You will either need to be on touch device, or enable touch sensors in your browser to see this
Steps:
onTouchStart listener to a componentonTouchStart move the component into a PortalIf the component is already in a component before the touchstart event then the events are emitted correctly: https://codesandbox.io/s/v54x54vp5
I have also created a vanilla js example that has a portal implementation. It moves the element into a portal after touch start. It is correctly allowing touch touchmove and touchend events: https://codesandbox.io/s/r4mn0yj6po
What is the expected behavior?
That the touchmove and touchend events are published
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
Reproduced bug in Firefox and Chrome
React version: tested on 16.1, 16.3 and 16.4.1
Is that a bug though, or just how the DOM works? The element is technically a different one unless I'm missing something.
This is indeed how the DOM behaves normally. The issue here is that touch events have a locked target: When a touchstart event occurs on a given target, all further touch event have the same target (you can read up on this in the spec).
MDN has a pretty good explanation for this issue:
Note that if the target element is removed from the document, events will still be targeted at it, and hence won't necessarily bubble up to the window or document anymore. If there is any risk of an element being removed while it is being touched, the best practice is to attach the touch listeners directly to the target.
Now, as Dan pointed out, the issue is that rendering inside a portal will create a new DOM element (since the element will have a different parent, the reconciler will always create a new element). And since the original element is no longer attached to the same document, events won鈥檛 bubble there anymore. In your vanilla example, this is not the case since the element is moved instead of recreated. Check out this example that shows the same issue.
As a workaround you can probably add native event listeners to the DOM node using componentDidMount and remove them on touchend/touchcancel. Or use pointer events which do not suffer from that issue since there, the target will always be the latest intersection (like with mouse events) - make sure any eventual polyfill handles this correctly though.
@philipp-spiess Is there any action we can take on the React side? I'm considering closing this, but I wanted to check first.
@nhunzaker Nope, I think this works as intended.
Okay. Thanks for giving a fantastic break down of the problem and possible solutions! I'll close this out.
Thanks @philipp-spiess for the detail!! It is much appreciated. I will see if I can work around this DOM behaviour on my end
You are right, my example was moving a DOM element rather than creating a new one - which is where the problem was.
Side point: codesandbox is great for sharing these things 馃憤
Also, thanks for following up @gaearon
Linking for historical purposes: #3965
For those needing a solution for drag/drop and dealing with the touch target changing parents - I wrote up a potential/hacky solution here - https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d in short attaching events during touchstart directly to the target seems to work for some reason.
I can confirm that attaching events to the event.target still publish after the element is removed. We will be shipping this change in react-beautiful-dnd soon https://github.com/atlassian/react-beautiful-dnd/issues/1225
Most helpful comment
This is indeed how the DOM behaves normally. The issue here is that touch events have a locked
target: When atouchstartevent occurs on a giventarget, all further touch event have the sametarget(you can read up on this in the spec).MDN has a pretty good explanation for this issue:
Now, as Dan pointed out, the issue is that rendering inside a portal will create a new DOM element (since the element will have a different parent, the reconciler will always create a new element). And since the original element is no longer attached to the same document, events won鈥檛 bubble there anymore. In your vanilla example, this is not the case since the element is moved instead of recreated. Check out this example that shows the same issue.
As a workaround you can probably add native event listeners to the DOM node using componentDidMount and remove them on touchend/touchcancel. Or use pointer events which do not suffer from that issue since there, the target will always be the latest intersection (like with mouse events) - make sure any eventual polyfill handles this correctly though.