Skip to content

Commit f47a958

Browse files
Don’t add onclick listener to React root (#13778)
Fixes #13777 As part of #11927 we introduced a regression by adding onclick handler to the React root. This causes the whole React tree to flash when tapped on iOS devices (for reasons I outlined in #12989 (comment)). To fix this, we should only apply onclick listeners to portal roots. I verified that my proposed fix indeed works by checking out our DOM fixtures and adding regression tests. Strangely, I had to make changes to the DOM fixtures to see the behavior in the first place. This seems to be caused by our normal sites (and thus their React root) being bigger than the viewport: ![](http://cl.ly/3f18f8b85e91/Screen%20Recording%202018-10-05%20at%2001.32%20AM.gif) An alternative approach to finding out if we're appending to a React root would be to add a third parameter to `appendChildToContainer` based on the tag of the parent fiber.
1 parent b2cea90 commit f47a958

File tree

3 files changed

+51
-4
lines changed

3 files changed

+51
-4
lines changed

packages/react-dom/src/__tests__/ReactDOMComponent-test.js

+40
Original file line numberDiff line numberDiff line change
@@ -2659,4 +2659,44 @@ describe('ReactDOMComponent', () => {
26592659
document.body.removeChild(container);
26602660
}
26612661
});
2662+
2663+
describe('iOS Tap Highlight', () => {
2664+
it('adds onclick handler to elements with onClick prop', () => {
2665+
const container = document.createElement('div');
2666+
2667+
const elementRef = React.createRef();
2668+
function Component() {
2669+
return <div ref={elementRef} onClick={() => {}} />;
2670+
}
2671+
2672+
ReactDOM.render(<Component />, container);
2673+
expect(typeof elementRef.current.onclick).toBe('function');
2674+
});
2675+
2676+
it('adds onclick handler to a portal root', () => {
2677+
const container = document.createElement('div');
2678+
const portalContainer = document.createElement('div');
2679+
2680+
function Component() {
2681+
return ReactDOM.createPortal(
2682+
<div onClick={() => {}} />,
2683+
portalContainer,
2684+
);
2685+
}
2686+
2687+
ReactDOM.render(<Component />, container);
2688+
expect(typeof portalContainer.onclick).toBe('function');
2689+
});
2690+
2691+
it('does not add onclick handler to the React root', () => {
2692+
const container = document.createElement('div');
2693+
2694+
function Component() {
2695+
return <div onClick={() => {}} />;
2696+
}
2697+
2698+
ReactDOM.render(<Component />, container);
2699+
expect(typeof container.onclick).not.toBe('function');
2700+
});
2701+
});
26622702
});

packages/react-dom/src/client/ReactDOM.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ if (__DEV__) {
126126

127127
ReactControlledComponent.setRestoreImplementation(restoreControlledState);
128128

129-
type DOMContainer =
129+
export type DOMContainer =
130130
| (Element & {
131131
_reactRootContainer: ?Root,
132132
})

packages/react-dom/src/client/ReactDOMHostConfig.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
DOCUMENT_FRAGMENT_NODE,
3737
} from '../shared/HTMLNodeType';
3838

39+
import type {DOMContainer} from './ReactDOM';
40+
3941
export type Type = string;
4042
export type Props = {
4143
autoFocus?: boolean,
@@ -342,7 +344,7 @@ export function appendChild(
342344
}
343345

344346
export function appendChildToContainer(
345-
container: Container,
347+
container: DOMContainer,
346348
child: Instance | TextInstance,
347349
): void {
348350
let parentNode;
@@ -358,9 +360,14 @@ export function appendChildToContainer(
358360
// through the React tree. However, on Mobile Safari the click would
359361
// never bubble through the *DOM* tree unless an ancestor with onclick
360362
// event exists. So we wouldn't see it and dispatch it.
361-
// This is why we ensure that containers have inline onclick defined.
363+
// This is why we ensure that non React root containers have inline onclick
364+
// defined.
362365
// https://github.com/facebook/react/issues/11918
363-
if (parentNode.onclick === null) {
366+
const reactRootContainer = container._reactRootContainer;
367+
if (
368+
(reactRootContainer === null || reactRootContainer === undefined) &&
369+
parentNode.onclick === null
370+
) {
364371
// TODO: This cast may not be sound for SVG, MathML or custom elements.
365372
trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
366373
}

0 commit comments

Comments
 (0)