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

[Flare] Add InteractOutside event responder surface #15791

Closed
wants to merge 14 commits into from
14 changes: 14 additions & 0 deletions packages/react-events/interact-outside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

'use strict';

const InteractOutside = require('./src/InteractOutside');

module.exports = InteractOutside.default || InteractOutside;
7 changes: 7 additions & 0 deletions packages/react-events/npm/interact-outside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-interact-outside.production.min.js');
} else {
module.exports = require('./cjs/react-events-interact-outside.development.js');
}
1 change: 1 addition & 0 deletions packages/react-events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"LICENSE",
"README.md",
"press.js",
"interact-outside.js",
"hover.js",
"focus.js",
"swipe.js",
Expand Down
309 changes: 309 additions & 0 deletions packages/react-events/src/InteractOutside.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {
ReactResponderEvent,
ReactResponderContext,
} from 'shared/ReactTypes';

import React from 'react';

type InteractOutsideProps = {
disabled: boolean,
interactOnBlur: boolean,
interactOnScroll: boolean,
onInteractOutside: (e: InteractOutsideEvent) => void,
};

type PointerType = '' | 'keyboard' | 'mouse' | 'pen' | 'touch';

type InteractOutsideState = {
ignoreEmulatedMouseEvents: boolean,
pointerType: PointerType,
pressStatus: 0 | 1 | 2,
pressTarget: null | Document | Element,
};

type InteractOutsideEventType = 'interactoutside';

type InteractOutsideEvent = {|
target: Element | Document,
type: InteractOutsideEventType,
pointerType: PointerType,
timeStamp: number,
clientX: null | number,
clientY: null | number,
pageX: null | number,
pageY: null | number,
screenX: null | number,
screenY: null | number,
x: null | number,
y: null | number,
altKey: boolean,
ctrlKey: boolean,
metaKey: boolean,
shiftKey: boolean,
|};

const isMac =
typeof window !== 'undefined' && window.navigator != null
? /^Mac/.test(window.navigator.platform)
: false;

const rootEventTypes = [
'pointerdown',
'pointerup',
'pointercancel',
'focus',
'keyup',
'scroll',
];

// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
rootEventTypes.push(
'touchstart',
'touchend',
'touchcancel',
'mousedown',
'mouseup',
// Used as a 'cancel' signal for mouse interactions
'dragstart',
);
}

const NOT_PRESSED = 0;
const PRESSED_INSIDE = 1;
const PRESSED_OUTSIDE = 2;

function isTouchEvent(nativeEvent: Event): boolean {
return Array.isArray((nativeEvent: any).changedTouches);
}

function getTouchFromPressEvent(nativeEvent: TouchEvent): Touch {
const {changedTouches, touches} = nativeEvent;
return changedTouches.length > 0
? changedTouches[0]
: touches.length > 0
? touches[0]
: (nativeEvent: any);
}

function createInteractOutsideEvent(
context: ReactResponderContext,
type: InteractOutsideEventType,
target: Element | Document,
pointerType: PointerType,
event: ?ReactResponderEvent,
): InteractOutsideEvent {
const timeStamp = context.getTimeStamp();
let clientX = null;
let clientY = null;
let pageX = null;
let pageY = null;
let screenX = null;
let screenY = null;
let altKey = false;
let ctrlKey = false;
let metaKey = false;
let shiftKey = false;

if (event) {
const nativeEvent = (event.nativeEvent: any);
({altKey, ctrlKey, metaKey, shiftKey} = nativeEvent);
// Only check for one property, checking for all of them is costly. We can assume
// if clientX exists, so do the rest.
let eventObject;
if (nativeEvent.clientX !== undefined) {
eventObject = (nativeEvent: any);
} else if (isTouchEvent(nativeEvent)) {
eventObject = getTouchFromPressEvent(nativeEvent);
}
if (eventObject) {
({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
}
}
return {
target,
type,
pointerType,
timeStamp,
clientX,
clientY,
pageX,
pageY,
screenX,
screenY,
x: clientX,
y: clientY,
altKey,
ctrlKey,
metaKey,
shiftKey,
};
}

function dispatchEvent(
event: ?ReactResponderEvent,
context: ReactResponderContext,
state: InteractOutsideState,
name: InteractOutsideEventType,
listener: (e: Object) => void,
discrete: boolean,
): void {
const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType;
const syntheticEvent = createInteractOutsideEvent(
context,
name,
target,
pointerType,
event,
);
context.dispatchEvent(syntheticEvent, listener, discrete);
}

const InteractOutsideResponder = {
rootEventTypes,
createInitialState(): InteractOutsideState {
return {
ignoreEmulatedMouseEvents: false,
pointerType: '',
pressStatus: NOT_PRESSED,
pressTarget: null,
};
},
allowMultipleHostChildren: true,
onRootEvent(
event: ReactResponderEvent,
context: ReactResponderContext,
props: InteractOutsideProps,
state: InteractOutsideState,
): void {
const {target, type} = event;

if (props.disabled) {
state.ignoreEmulatedMouseEvents = false;
state.pressStatus = NOT_PRESSED;
return;
}
const nativeEvent: any = event.nativeEvent;
const pointerType = context.getEventPointerType(event);

switch (type) {
case 'pointerdown':
case 'mousedown':
case 'touchstart': {
if (state.pressStatus === NOT_PRESSED) {
if (type === 'pointerdown' || type === 'touchstart') {
state.ignoreEmulatedMouseEvents = true;
}

state.pointerType = pointerType;
state.pressTarget = context.getEventCurrentTarget(event);

// Ignore any device buttons except left-mouse and touch/pen contact.
// Additionally we ignore left-mouse + ctrl-key with Macs as that
// acts like right-click and opens the contextmenu.
if (
nativeEvent.button > 0 ||
(isMac && pointerType === 'mouse' && nativeEvent.ctrlKey)
) {
return;
}

// Ignore emulated mouse events
if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) {
return;
}
if (context.isTargetWithinEventComponent(target)) {
state.pressStatus = PRESSED_INSIDE;
} else {
state.pressStatus = PRESSED_OUTSIDE;
}
}
break;
}
case 'pointerup':
case 'mouseup':
case 'touchend': {
if (state.pressStatus !== NOT_PRESSED) {
if (state.pressStatus === PRESSED_OUTSIDE) {
if (
state.pressTarget !== null &&
props.onInteractOutside &&
!context.isTargetWithinEventComponent(target)
) {
dispatchEvent(
event,
context,
state,
'interactoutside',
props.onInteractOutside,
true,
);
}
}
state.pressStatus = NOT_PRESSED;
} else if (type === 'mouseup' && state.ignoreEmulatedMouseEvents) {
state.ignoreEmulatedMouseEvents = false;
}
break;
}
case 'keyup': {
if (state.pressStatus === NOT_PRESSED) {
state.pointerType = pointerType;
}
break;
}
case 'scroll':
case 'focus': {
if (type === 'focus' && props.interactOnBlur === false) {
return;
}
if (type === 'scroll' && !props.interactOnScroll) {
return;
}
if (
props.onInteractOutside &&
!context.isTargetWithinEventComponent(target)
) {
state.pressTarget = target;
dispatchEvent(
event,
context,
state,
'interactoutside',
props.onInteractOutside,
true,
);
state.pressStatus = NOT_PRESSED;
}
break;
}
case 'pointercancel':
case 'touchcancel':
case 'dragstart': {
state.pressStatus = NOT_PRESSED;
}
}
},
onOwnershipChange(
context: ReactResponderContext,
props: InteractOutsideProps,
state: InteractOutsideState,
) {
state.pressStatus = NOT_PRESSED;
},
};

export default React.unstable_createEventComponent(
InteractOutsideResponder,
'InteractOutside',
);
7 changes: 4 additions & 3 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ function dispatchCancel(
props: PressProps,
state: PressState,
): void {
state.ignoreEmulatedMouseEvents = false;
if (state.isPressed) {
state.ignoreEmulatedMouseEvents = false;
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
} else if (state.allowPressReentry) {
Expand Down Expand Up @@ -623,7 +623,6 @@ const PressResponder = {
return {
activationPosition: null,
addedRootEvents: false,
didDispatchEvent: false,
isActivePressed: false,
isActivePressStart: false,
isLongPressed: false,
Expand Down Expand Up @@ -652,7 +651,9 @@ const PressResponder = {

if (props.disabled) {
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
if (state.isPressed) {
dispatchPressEndEvents(event, context, props, state);
}
state.ignoreEmulatedMouseEvents = false;
return;
}
Expand Down
Loading