Skip to content

Commit 4fbbae8

Browse files
authored
Add full TouchHitTarget hit slop (experimental event API) to ReactDOM (#15308)
1 parent 958b617 commit 4fbbae8

19 files changed

+848
-295
lines changed

packages/events/EventTypes.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ export type ResponderContext = {
3434
parentTarget: Element | Document,
3535
) => boolean,
3636
isTargetWithinEventComponent: (Element | Document) => boolean,
37-
isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
37+
isPositionWithinTouchHitTarget: (
38+
doc: Document,
39+
x: number,
40+
y: number,
41+
) => boolean,
3842
addRootEventTypes: (
3943
document: Document,
4044
rootEventTypes: Array<ReactEventResponderEventType>,

packages/react-art/src/ReactARTHostConfig.js

+21-5
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,31 @@ export function handleEventComponent(
443443
eventResponder: ReactEventResponder,
444444
rootContainerInstance: Container,
445445
internalInstanceHandle: Object,
446-
) {
447-
// TODO: add handleEventComponent implementation
446+
): void {
447+
throw new Error('Not yet implemented.');
448+
}
449+
450+
export function getEventTargetChildElement(
451+
type: Symbol | number,
452+
props: Props,
453+
): null {
454+
throw new Error('Not yet implemented.');
448455
}
449456

450457
export function handleEventTarget(
451458
type: Symbol | number,
452459
props: Props,
453-
parentInstance: Container,
460+
rootContainerInstance: Container,
454461
internalInstanceHandle: Object,
455-
) {
456-
// TODO: add handleEventTarget implementation
462+
): boolean {
463+
throw new Error('Not yet implemented.');
464+
}
465+
466+
export function commitEventTarget(
467+
type: Symbol | number,
468+
props: Props,
469+
instance: Instance,
470+
parentInstance: Instance,
471+
): void {
472+
throw new Error('Not yet implemented.');
457473
}

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

+97-37
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
isEnabled as ReactBrowserEventEmitterIsEnabled,
3434
setEnabled as ReactBrowserEventEmitterSetEnabled,
3535
} from '../events/ReactBrowserEventEmitter';
36-
import {getChildNamespace} from '../shared/DOMNamespaces';
36+
import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
3737
import {
3838
ELEMENT_NODE,
3939
TEXT_NODE,
@@ -46,6 +46,7 @@ import dangerousStyleValue from '../shared/dangerousStyleValue';
4646
import type {DOMContainer} from './ReactDOM';
4747
import type {ReactEventResponder} from 'shared/ReactTypes';
4848
import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
49+
import {canUseDOM} from 'shared/ExecutionEnvironment';
4950

5051
export type Type = string;
5152
export type Props = {
@@ -57,6 +58,23 @@ export type Props = {
5758
style?: {
5859
display?: string,
5960
},
61+
bottom?: null | number,
62+
left?: null | number,
63+
right?: null | number,
64+
top?: null | number,
65+
};
66+
export type EventTargetChildElement = {
67+
type: string,
68+
props: null | {
69+
style?: {
70+
position?: string,
71+
zIndex?: number,
72+
bottom?: string,
73+
left?: string,
74+
right?: string,
75+
top?: string,
76+
},
77+
},
6078
};
6179
export type Container = Element | Document;
6280
export type Instance = Element;
@@ -70,7 +88,6 @@ type HostContextDev = {
7088
eventData: null | {|
7189
isEventComponent?: boolean,
7290
isEventTarget?: boolean,
73-
eventTargetType?: null | Symbol | number,
7491
|},
7592
};
7693
type HostContextProd = string;
@@ -86,6 +103,8 @@ import {
86103
} from 'shared/ReactFeatureFlags';
87104
import warning from 'shared/warning';
88105

106+
const {html: HTML_NAMESPACE} = Namespaces;
107+
89108
// Intentionally not named imports because Rollup would
90109
// use dynamic dispatch for CommonJS interop named imports.
91110
const {
@@ -190,7 +209,6 @@ export function getChildHostContextForEventComponent(
190209
const eventData = {
191210
isEventComponent: true,
192211
isEventTarget: false,
193-
eventTargetType: null,
194212
};
195213
return {namespace, ancestorInfo, eventData};
196214
}
@@ -204,17 +222,24 @@ export function getChildHostContextForEventTarget(
204222
if (__DEV__) {
205223
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
206224
const {namespace, ancestorInfo} = parentHostContextDev;
207-
warning(
208-
parentHostContextDev.eventData === null ||
209-
!parentHostContextDev.eventData.isEventComponent ||
210-
type !== REACT_EVENT_TARGET_TOUCH_HIT,
211-
'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
212-
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
213-
);
225+
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
226+
warning(
227+
parentHostContextDev.eventData === null ||
228+
!parentHostContextDev.eventData.isEventComponent,
229+
'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
230+
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
231+
);
232+
const parentNamespace = parentHostContextDev.namespace;
233+
if (parentNamespace !== HTML_NAMESPACE) {
234+
throw new Error(
235+
'<TouchHitTarget> was used in an unsupported DOM namespace. ' +
236+
'Ensure the <TouchHitTarget> is used in an HTML namespace.',
237+
);
238+
}
239+
}
214240
const eventData = {
215241
isEventComponent: false,
216242
isEventTarget: true,
217-
eventTargetType: type,
218243
};
219244
return {namespace, ancestorInfo, eventData};
220245
}
@@ -249,16 +274,6 @@ export function createInstance(
249274
if (__DEV__) {
250275
// TODO: take namespace into account when validating.
251276
const hostContextDev = ((hostContext: any): HostContextDev);
252-
if (enableEventAPI) {
253-
const eventData = hostContextDev.eventData;
254-
if (eventData !== null) {
255-
warning(
256-
!eventData.isEventTarget ||
257-
eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
258-
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
259-
);
260-
}
261-
}
262277
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
263278
if (
264279
typeof props.children === 'string' ||
@@ -365,25 +380,12 @@ export function createTextInstance(
365380
if (enableEventAPI) {
366381
const eventData = hostContextDev.eventData;
367382
if (eventData !== null) {
368-
warning(
369-
eventData === null ||
370-
!eventData.isEventTarget ||
371-
eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
372-
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
373-
);
374383
warning(
375384
!eventData.isEventComponent,
376385
'validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
377386
'Wrap the child text "%s" in an element.',
378387
text,
379388
);
380-
warning(
381-
!eventData.isEventTarget ||
382-
eventData.eventTargetType === REACT_EVENT_TARGET_TOUCH_HIT,
383-
'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
384-
'Wrap the child text "%s" in an element.',
385-
text,
386-
);
387389
}
388390
}
389391
}
@@ -899,16 +901,74 @@ export function handleEventComponent(
899901
}
900902
}
901903

904+
export function getEventTargetChildElement(
905+
type: Symbol | number,
906+
props: Props,
907+
): null | EventTargetChildElement {
908+
if (enableEventAPI) {
909+
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
910+
const {bottom, left, right, top} = props;
911+
912+
if (!bottom && !left && !right && !top) {
913+
return null;
914+
}
915+
return {
916+
type: 'div',
917+
props: {
918+
style: {
919+
position: 'absolute',
920+
zIndex: -1,
921+
bottom: bottom ? `-${bottom}px` : '0px',
922+
left: left ? `-${left}px` : '0px',
923+
right: right ? `-${right}px` : '0px',
924+
top: top ? `-${top}px` : '0px',
925+
},
926+
},
927+
};
928+
}
929+
}
930+
return null;
931+
}
932+
902933
export function handleEventTarget(
903934
type: Symbol | number,
904935
props: Props,
905-
parentInstance: Container,
936+
rootContainerInstance: Container,
906937
internalInstanceHandle: Object,
938+
): boolean {
939+
return false;
940+
}
941+
942+
export function commitEventTarget(
943+
type: Symbol | number,
944+
props: Props,
945+
instance: Instance,
946+
parentInstance: Instance,
907947
): void {
908948
if (enableEventAPI) {
909-
// Touch target hit slop handling
910949
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
911-
// TODO
950+
if (__DEV__ && canUseDOM) {
951+
// This is done at DEV time because getComputedStyle will
952+
// typically force a style recalculation and force a layout,
953+
// reflow -– both of which are sync are expensive.
954+
const computedStyles = window.getComputedStyle(parentInstance);
955+
const position = computedStyles.getPropertyValue('position');
956+
warning(
957+
position !== '' && position !== 'static',
958+
'<TouchHitTarget> inserts an empty absolutely positioned <div>. ' +
959+
'This requires its parent DOM node to be positioned too, but the ' +
960+
'parent DOM node was found to have the style "position" set to ' +
961+
'either no value, or a value of "static". Try using a "position" ' +
962+
'value of "relative".',
963+
);
964+
warning(
965+
computedStyles.getPropertyValue('zIndex') !== '',
966+
'<TouchHitTarget> inserts an empty <div> with "z-index" of "-1". ' +
967+
'This requires its parent DOM node to have a "z-index" great than "-1",' +
968+
'but the parent DOM node was found to no "z-index" value set.' +
969+
' Try using a "z-index" value of "0" or greater.',
970+
);
971+
}
912972
}
913973
}
914974
}

packages/react-dom/src/events/DOMEventResponderSystem.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
PASSIVE_NOT_SUPPORTED,
1818
} from 'events/EventSystemFlags';
1919
import type {AnyNativeEvent} from 'events/PluginModuleType';
20-
import {EventComponent} from 'shared/ReactWorkTags';
20+
import {
21+
EventComponent,
22+
EventTarget as EventTargetWorkTag,
23+
} from 'shared/ReactWorkTags';
2124
import type {
2225
ReactEventResponder,
2326
ReactEventResponderEventType,
@@ -110,7 +113,31 @@ const eventResponderContext: ResponderContext = {
110113
eventsWithStopPropagation.add(eventObject);
111114
}
112115
},
113-
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
116+
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
117+
// This isn't available in some environments (JSDOM)
118+
if (typeof doc.elementFromPoint !== 'function') {
119+
return false;
120+
}
121+
const target = doc.elementFromPoint(x, y);
122+
if (target === null) {
123+
return false;
124+
}
125+
const childFiber = getClosestInstanceFromNode(target);
126+
if (childFiber === null) {
127+
return false;
128+
}
129+
const parentFiber = childFiber.return;
130+
if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) {
131+
const parentNode = ((target.parentNode: any): Element);
132+
// TODO find another way to do this without using the
133+
// expensive getBoundingClientRect.
134+
const {left, top, right, bottom} = parentNode.getBoundingClientRect();
135+
// Check if the co-ords intersect with the target element's rect.
136+
if (x > left && y > top && x < right && y < bottom) {
137+
return false;
138+
}
139+
return true;
140+
}
114141
return false;
115142
},
116143
isTargetWithinEventComponent(target: Element | Document): boolean {

packages/react-dom/src/server/ReactPartialRenderer.js

+24
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
REACT_MEMO_TYPE,
4040
REACT_EVENT_COMPONENT_TYPE,
4141
REACT_EVENT_TARGET_TYPE,
42+
REACT_EVENT_TARGET_TOUCH_HIT,
4243
} from 'shared/ReactSymbols';
4344

4445
import {
@@ -1168,6 +1169,29 @@ class ReactDOMServerRenderer {
11681169
case REACT_EVENT_COMPONENT_TYPE:
11691170
case REACT_EVENT_TARGET_TYPE: {
11701171
if (enableEventAPI) {
1172+
if (
1173+
elementType.$$typeof === REACT_EVENT_TARGET_TYPE &&
1174+
elementType.type === REACT_EVENT_TARGET_TOUCH_HIT
1175+
) {
1176+
const props = nextElement.props;
1177+
const bottom = props.bottom || 0;
1178+
const left = props.left || 0;
1179+
const right = props.right || 0;
1180+
const top = props.top || 0;
1181+
1182+
if (bottom === 0 && left === 0 && right === 0 && top === 0) {
1183+
return '';
1184+
}
1185+
let topString = top ? `-${top}px` : '0px';
1186+
let leftString = left ? `-${left}px` : '0px';
1187+
let rightString = right ? `-${right}px` : '0x';
1188+
let bottomString = bottom ? `-${bottom}px` : '0px';
1189+
1190+
return (
1191+
`<div style="position:absolute;z-index:-1;bottom:` +
1192+
`${bottomString};left:${leftString};right:${rightString};top:${topString}"></div>`
1193+
);
1194+
}
11711195
const nextChildren = toArray(
11721196
((nextChild: any): ReactElement).props.children,
11731197
);

packages/react-events/src/Hover.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ const HoverResponder = {
196196
props: HoverProps,
197197
state: HoverState,
198198
): void {
199-
const {type, nativeEvent} = event;
199+
const {type, target, nativeEvent} = event;
200200

201201
switch (type) {
202202
/**
@@ -218,6 +218,7 @@ const HoverResponder = {
218218
}
219219
if (
220220
context.isPositionWithinTouchHitTarget(
221+
target.ownerDocument,
221222
(nativeEvent: any).x,
222223
(nativeEvent: any).y,
223224
)
@@ -244,6 +245,7 @@ const HoverResponder = {
244245
if (state.isInHitSlop) {
245246
if (
246247
!context.isPositionWithinTouchHitTarget(
248+
target.ownerDocument,
247249
(nativeEvent: any).x,
248250
(nativeEvent: any).y,
249251
)
@@ -254,6 +256,7 @@ const HoverResponder = {
254256
} else if (
255257
state.isHovered &&
256258
context.isPositionWithinTouchHitTarget(
259+
target.ownerDocument,
257260
(nativeEvent: any).x,
258261
(nativeEvent: any).y,
259262
)

packages/react-events/src/Press.js

+1
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ const PressResponder = {
259259
nativeEvent.button === 2 ||
260260
// Ignore pressing on hit slop area with mouse
261261
context.isPositionWithinTouchHitTarget(
262+
target.ownerDocument,
262263
(nativeEvent: any).x,
263264
(nativeEvent: any).y,
264265
)

0 commit comments

Comments
 (0)