Skip to content

Commit

Permalink
Add full TouchHitTarget hit slop (experimental event API) to ReactDOM (
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Apr 6, 2019
1 parent 958b617 commit 4fbbae8
Show file tree
Hide file tree
Showing 19 changed files with 848 additions and 295 deletions.
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;
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 @@ -196,7 +196,7 @@ const HoverResponder = {
props: HoverProps,
state: HoverState,
): void {
const {type, nativeEvent} = event;
const {type, target, nativeEvent} = event;

switch (type) {
/**
Expand All @@ -218,6 +218,7 @@ const HoverResponder = {
}
if (
context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand All @@ -244,6 +245,7 @@ const HoverResponder = {
if (state.isInHitSlop) {
if (
!context.isPositionWithinTouchHitTarget(
target.ownerDocument,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
Expand All @@ -254,6 +256,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

0 comments on commit 4fbbae8

Please sign in to comment.