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

Add full TouchHitTarget hit slop (experimental event API) to ReactDOM #15308

Merged
merged 13 commits into from
Apr 6, 2019
6 changes: 5 additions & 1 deletion packages/events/EventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export type ResponderContext = {
parentTarget: Element | Document,
) => boolean,
isTargetWithinEventComponent: (Element | Document) => boolean,
isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
isPositionWithinTouchHitTarget: (
doc: Document,
x: number,
y: number,
) => boolean,
addRootEventTypes: (
document: Document,
rootEventTypes: Array<ReactEventResponderEventType>,
Expand Down
26 changes: 21 additions & 5 deletions packages/react-art/src/ReactARTHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,15 +443,31 @@ export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
) {
// TODO: add handleEventComponent implementation
): void {
throw new Error('Not yet implemented.');
}

export function getEventTargetChildElement(
type: Symbol | number,
props: Props,
): null {
throw new Error('Not yet implemented.');
}

export function handleEventTarget(
type: Symbol | number,
props: Props,
parentInstance: Container,
rootContainerInstance: Container,
internalInstanceHandle: Object,
) {
// TODO: add handleEventTarget implementation
): boolean {
throw new Error('Not yet implemented.');
}

export function commitEventTarget(
type: Symbol | number,
props: Props,
instance: Instance,
parentInstance: Instance,
): void {
throw new Error('Not yet implemented.');
}
134 changes: 97 additions & 37 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
isEnabled as ReactBrowserEventEmitterIsEnabled,
setEnabled as ReactBrowserEventEmitterSetEnabled,
} from '../events/ReactBrowserEventEmitter';
import {getChildNamespace} from '../shared/DOMNamespaces';
import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces';
import {
ELEMENT_NODE,
TEXT_NODE,
Expand All @@ -46,6 +46,7 @@ import dangerousStyleValue from '../shared/dangerousStyleValue';
import type {DOMContainer} from './ReactDOM';
import type {ReactEventResponder} from 'shared/ReactTypes';
import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
import {canUseDOM} from 'shared/ExecutionEnvironment';

export type Type = string;
export type Props = {
Expand All @@ -57,6 +58,23 @@ export type Props = {
style?: {
display?: string,
},
bottom?: null | number,
left?: null | number,
right?: null | number,
top?: null | number,
};
export type EventTargetChildElement = {
type: string,
props: null | {
style?: {
position?: string,
zIndex?: number,
bottom?: string,
left?: string,
right?: string,
top?: string,
},
},
};
export type Container = Element | Document;
export type Instance = Element;
Expand All @@ -70,7 +88,6 @@ type HostContextDev = {
eventData: null | {|
isEventComponent?: boolean,
isEventTarget?: boolean,
eventTargetType?: null | Symbol | number,
|},
};
type HostContextProd = string;
Expand All @@ -86,6 +103,8 @@ import {
} from 'shared/ReactFeatureFlags';
import warning from 'shared/warning';

const {html: HTML_NAMESPACE} = Namespaces;

// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
const {
Expand Down Expand Up @@ -190,7 +209,6 @@ export function getChildHostContextForEventComponent(
const eventData = {
isEventComponent: true,
isEventTarget: false,
eventTargetType: null,
};
return {namespace, ancestorInfo, eventData};
}
Expand All @@ -204,17 +222,24 @@ export function getChildHostContextForEventTarget(
if (__DEV__) {
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
const {namespace, ancestorInfo} = parentHostContextDev;
warning(
parentHostContextDev.eventData === null ||
!parentHostContextDev.eventData.isEventComponent ||
type !== REACT_EVENT_TARGET_TOUCH_HIT,
'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
);
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
warning(
parentHostContextDev.eventData === null ||
!parentHostContextDev.eventData.isEventComponent,
'validateDOMNesting: <TouchHitTarget> cannot not be a direct child of an event component. ' +
'Ensure <TouchHitTarget> is a direct child of a DOM element.',
);
const parentNamespace = parentHostContextDev.namespace;
if (parentNamespace !== HTML_NAMESPACE) {
throw new Error(
'<TouchHitTarget> was used in an unsupported DOM namespace. ' +
'Ensure the <TouchHitTarget> is used in an HTML namespace.',
);
}
}
const eventData = {
isEventComponent: false,
isEventTarget: true,
eventTargetType: type,
};
return {namespace, ancestorInfo, eventData};
}
Expand Down Expand Up @@ -249,16 +274,6 @@ export function createInstance(
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext: any): HostContextDev);
if (enableEventAPI) {
const eventData = hostContextDev.eventData;
if (eventData !== null) {
warning(
!eventData.isEventTarget ||
eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
);
}
}
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
Expand Down Expand Up @@ -365,25 +380,12 @@ export function createTextInstance(
if (enableEventAPI) {
const eventData = hostContextDev.eventData;
if (eventData !== null) {
warning(
eventData === null ||
!eventData.isEventTarget ||
eventData.eventTargetType !== REACT_EVENT_TARGET_TOUCH_HIT,
'Warning: validateDOMNesting: <TouchHitTarget> must not have any children.',
);
warning(
!eventData.isEventComponent,
'validateDOMNesting: React event components cannot have text DOM nodes as children. ' +
'Wrap the child text "%s" in an element.',
text,
);
warning(
!eventData.isEventTarget ||
eventData.eventTargetType === REACT_EVENT_TARGET_TOUCH_HIT,
'validateDOMNesting: React event targets cannot have text DOM nodes as children. ' +
'Wrap the child text "%s" in an element.',
text,
);
}
}
}
Expand Down Expand Up @@ -899,16 +901,74 @@ export function handleEventComponent(
}
}

export function getEventTargetChildElement(
type: Symbol | number,
props: Props,
): null | EventTargetChildElement {
if (enableEventAPI) {
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
const {bottom, left, right, top} = props;

if (!bottom && !left && !right && !top) {
return null;
}
return {
type: 'div',
props: {
style: {
position: 'absolute',
zIndex: -1,
bottom: bottom ? `-${bottom}px` : '0px',
left: left ? `-${left}px` : '0px',
right: right ? `-${right}px` : '0px',
top: top ? `-${top}px` : '0px',
},
},
};
}
}
return null;
}

export function handleEventTarget(
type: Symbol | number,
props: Props,
parentInstance: Container,
rootContainerInstance: Container,
internalInstanceHandle: Object,
): boolean {
return false;
}

export function commitEventTarget(
type: Symbol | number,
props: Props,
instance: Instance,
parentInstance: Instance,
): void {
if (enableEventAPI) {
// Touch target hit slop handling
if (type === REACT_EVENT_TARGET_TOUCH_HIT) {
// TODO
if (__DEV__ && canUseDOM) {
// This is done at DEV time because getComputedStyle will
// typically force a style recalculation and force a layout,
// reflow -– both of which are sync are expensive.
const computedStyles = window.getComputedStyle(parentInstance);
const position = computedStyles.getPropertyValue('position');
warning(
position !== '' && position !== 'static',
'<TouchHitTarget> inserts an empty absolutely positioned <div>. ' +
'This requires its parent DOM node to be positioned too, but the ' +
'parent DOM node was found to have the style "position" set to ' +
'either no value, or a value of "static". Try using a "position" ' +
'value of "relative".',
);
warning(
computedStyles.getPropertyValue('zIndex') !== '',
'<TouchHitTarget> inserts an empty <div> with "z-index" of "-1". ' +
'This requires its parent DOM node to have a "z-index" great than "-1",' +
'but the parent DOM node was found to no "z-index" value set.' +
' Try using a "z-index" value of "0" or greater.',
);
}
}
}
}
31 changes: 29 additions & 2 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
PASSIVE_NOT_SUPPORTED,
} from 'events/EventSystemFlags';
import type {AnyNativeEvent} from 'events/PluginModuleType';
import {EventComponent} from 'shared/ReactWorkTags';
import {
EventComponent,
EventTarget as EventTargetWorkTag,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponder,
ReactEventResponderEventType,
Expand Down Expand Up @@ -110,7 +113,31 @@ const eventResponderContext: ResponderContext = {
eventsWithStopPropagation.add(eventObject);
}
},
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
// This isn't available in some environments (JSDOM)
if (typeof doc.elementFromPoint !== 'function') {
return false;
}
const target = doc.elementFromPoint(x, y);
if (target === null) {
return false;
}
const childFiber = getClosestInstanceFromNode(target);
if (childFiber === null) {
return false;
}
const parentFiber = childFiber.return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally we avoid traversing Fibers outside the reconciler but we already cheat this rule in the existing event system - for the same reasons as here. So this is ok.

if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) {
const parentNode = ((target.parentNode: any): Element);
// TODO find another way to do this without using the
// expensive getBoundingClientRect.
const {left, top, right, bottom} = parentNode.getBoundingClientRect();
// Check if the co-ords intersect with the target element's rect.
if (x > left && y > top && x < right && y < bottom) {
return false;
}
return true;
}
return false;
},
isTargetWithinEventComponent(target: Element | Document): boolean {
Expand Down
24 changes: 24 additions & 0 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
REACT_MEMO_TYPE,
REACT_EVENT_COMPONENT_TYPE,
REACT_EVENT_TARGET_TYPE,
REACT_EVENT_TARGET_TOUCH_HIT,
} from 'shared/ReactSymbols';

import {
Expand Down Expand Up @@ -1168,6 +1169,29 @@ class ReactDOMServerRenderer {
case REACT_EVENT_COMPONENT_TYPE:
case REACT_EVENT_TARGET_TYPE: {
if (enableEventAPI) {
if (
elementType.$$typeof === REACT_EVENT_TARGET_TYPE &&
elementType.type === REACT_EVENT_TARGET_TOUCH_HIT
) {
const props = nextElement.props;
const bottom = props.bottom || 0;
const left = props.left || 0;
const right = props.right || 0;
const top = props.top || 0;

if (bottom === 0 && left === 0 && right === 0 && top === 0) {
return '';
}
let topString = top ? `-${top}px` : '0px';
let leftString = left ? `-${left}px` : '0px';
let rightString = right ? `-${right}px` : '0x';
let bottomString = bottom ? `-${bottom}px` : '0px';

return (
`<div style="position:absolute;z-index:-1;bottom:` +
`${bottomString};left:${leftString};right:${rightString};top:${topString}"></div>`
);
}
const nextChildren = toArray(
((nextChild: any): ReactElement).props.children,
);
Expand Down
5 changes: 4 additions & 1 deletion packages/react-events/src/Hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const HoverResponder = {
props: HoverProps,
state: HoverState,
): void {
const {type, nativeEvent} = event;
const {type, target, nativeEvent} = event;

switch (type) {
/**
Expand All @@ -149,6 +149,7 @@ const HoverResponder = {
}
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand Down Expand Up @@ -176,6 +177,7 @@ const HoverResponder = {
if (state.isInHitSlop) {
if (
!context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand All @@ -187,6 +189,7 @@ const HoverResponder = {
} else if (
state.isHovered &&
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand Down
1 change: 1 addition & 0 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ const PressResponder = {
nativeEvent.button === 2 ||
// Ignore pressing on hit slop area with mouse
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand Down
Loading