diff --git a/packages/react-events/src/Focus.js b/packages/react-events/src/Focus.js
index e56379f8871dd..0b26bde9757bf 100644
--- a/packages/react-events/src/Focus.js
+++ b/packages/react-events/src/Focus.js
@@ -10,10 +10,12 @@
import type {EventResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
-const targetEventTypes = [
- {name: 'focus', passive: true, capture: true},
- {name: 'blur', passive: true, capture: true},
-];
+type FocusProps = {
+ disabled: boolean,
+ onBlur: (e: FocusEvent) => void,
+ onFocus: (e: FocusEvent) => void,
+ onFocusChange: boolean => void,
+};
type FocusState = {
isFocused: boolean,
@@ -27,6 +29,11 @@ type FocusEvent = {|
type: FocusEventType,
|};
+const targetEventTypes = [
+ {name: 'focus', passive: true, capture: true},
+ {name: 'blur', passive: true, capture: true},
+];
+
function createFocusEvent(
type: FocusEventType,
target: Element | Document,
@@ -39,7 +46,10 @@ function createFocusEvent(
};
}
-function dispatchFocusInEvents(context: EventResponderContext, props: Object) {
+function dispatchFocusInEvents(
+ context: EventResponderContext,
+ props: FocusProps,
+) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
@@ -53,19 +63,22 @@ function dispatchFocusInEvents(context: EventResponderContext, props: Object) {
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
- const focusChangeEventListener = () => {
+ const listener = () => {
props.onFocusChange(true);
};
const syntheticEvent = createFocusEvent(
'focuschange',
eventTarget,
- focusChangeEventListener,
+ listener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
-function dispatchFocusOutEvents(context: EventResponderContext, props: Object) {
+function dispatchFocusOutEvents(
+ context: EventResponderContext,
+ props: FocusProps,
+) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
@@ -75,13 +88,13 @@ function dispatchFocusOutEvents(context: EventResponderContext, props: Object) {
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
- const focusChangeEventListener = () => {
+ const listener = () => {
props.onFocusChange(false);
};
const syntheticEvent = createFocusEvent(
'focuschange',
eventTarget,
- focusChangeEventListener,
+ listener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
diff --git a/packages/react-events/src/Hover.js b/packages/react-events/src/Hover.js
index 226430b838d09..ff42aac232e79 100644
--- a/packages/react-events/src/Hover.js
+++ b/packages/react-events/src/Hover.js
@@ -10,12 +10,14 @@
import type {EventResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
-const targetEventTypes = [
- 'pointerover',
- 'pointermove',
- 'pointerout',
- 'pointercancel',
-];
+type HoverProps = {
+ disabled: boolean,
+ delayHoverEnd: number,
+ delayHoverStart: number,
+ onHoverChange: boolean => void,
+ onHoverEnd: (e: HoverEvent) => void,
+ onHoverStart: (e: HoverEvent) => void,
+};
type HoverState = {
isHovered: boolean,
@@ -31,6 +33,21 @@ type HoverEvent = {|
type: HoverEventType,
|};
+// const DEFAULT_HOVER_END_DELAY_MS = 0;
+// const DEFAULT_HOVER_START_DELAY_MS = 0;
+
+const targetEventTypes = [
+ 'pointerover',
+ 'pointermove',
+ 'pointerout',
+ 'pointercancel',
+];
+
+// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
+if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
+ targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
+}
+
function createHoverEvent(
type: HoverEventType,
target: Element | Document,
@@ -43,16 +60,9 @@ function createHoverEvent(
};
}
-// In the case we don't have PointerEvents (Safari), we listen to touch events
-// too
-if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
- targetEventTypes.push('touchstart', 'mouseover', 'mouseout');
-}
-
function dispatchHoverStartEvents(
context: EventResponderContext,
- props: Object,
- state: HoverState,
+ props: HoverProps,
): void {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
@@ -67,19 +77,22 @@ function dispatchHoverStartEvents(
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onHoverChange) {
- const hoverChangeEventListener = () => {
+ const listener = () => {
props.onHoverChange(true);
};
const syntheticEvent = createHoverEvent(
'hoverchange',
eventTarget,
- hoverChangeEventListener,
+ listener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
-function dispatchHoverEndEvents(context: EventResponderContext, props: Object) {
+function dispatchHoverEndEvents(
+ context: EventResponderContext,
+ props: HoverProps,
+) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
@@ -93,13 +106,13 @@ function dispatchHoverEndEvents(context: EventResponderContext, props: Object) {
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onHoverChange) {
- const hoverChangeEventListener = () => {
+ const listener = () => {
props.onHoverChange(false);
};
const syntheticEvent = createHoverEvent(
'hoverchange',
eventTarget,
- hoverChangeEventListener,
+ listener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
@@ -116,18 +129,22 @@ const HoverResponder = {
},
handleEvent(
context: EventResponderContext,
- props: Object,
+ props: HoverProps,
state: HoverState,
): void {
const {eventType, eventTarget, event} = context;
switch (eventType) {
- case 'touchstart':
- // Touch devices don't have hover support
+ /**
+ * Prevent hover events when touch is being used.
+ */
+ case 'touchstart': {
if (!state.isTouched) {
state.isTouched = true;
}
break;
+ }
+
case 'pointerover':
case 'mouseover': {
if (
@@ -148,7 +165,7 @@ const HoverResponder = {
state.isInHitSlop = true;
return;
}
- dispatchHoverStartEvents(context, props, state);
+ dispatchHoverStartEvents(context, props);
state.isHovered = true;
}
break;
@@ -172,7 +189,7 @@ const HoverResponder = {
(event: any).y,
)
) {
- dispatchHoverStartEvents(context, props, state);
+ dispatchHoverStartEvents(context, props);
state.isHovered = true;
state.isInHitSlop = false;
}
diff --git a/packages/react-events/src/Press.js b/packages/react-events/src/Press.js
index b79c38758b9a8..69251a1fa3ef5 100644
--- a/packages/react-events/src/Press.js
+++ b/packages/react-events/src/Press.js
@@ -10,27 +10,6 @@
import type {EventResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
-// const DEFAULT_PRESS_DELAY_MS = 0;
-// const DEFAULT_PRESS_END_DELAY_MS = 0;
-// const DEFAULT_PRESS_START_DELAY_MS = 0;
-const DEFAULT_LONG_PRESS_DELAY_MS = 1000;
-
-const targetEventTypes = [
- {name: 'click', passive: false},
- {name: 'keydown', passive: false},
- 'pointerdown',
- 'pointercancel',
- 'contextmenu',
-];
-const rootEventTypes = [{name: 'pointerup', passive: false}, 'scroll'];
-
-// In the case we don't have PointerEvents (Safari), we listen to touch events
-// too
-if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
- targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel');
- rootEventTypes.push({name: 'mouseup', passive: false});
-}
-
type PressProps = {
disabled: boolean,
delayLongPress: number,
@@ -70,6 +49,30 @@ type PressEvent = {|
type: PressEventType,
|};
+// const DEFAULT_PRESS_DELAY_MS = 0;
+// const DEFAULT_PRESS_END_DELAY_MS = 0;
+// const DEFAULT_PRESS_START_DELAY_MS = 0;
+const DEFAULT_LONG_PRESS_DELAY_MS = 500;
+
+const targetEventTypes = [
+ {name: 'click', passive: false},
+ {name: 'keydown', passive: false},
+ 'pointerdown',
+ 'pointercancel',
+ 'contextmenu',
+];
+const rootEventTypes = [
+ {name: 'keyup', passive: false},
+ {name: 'pointerup', passive: false},
+ 'scroll',
+];
+
+// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
+if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
+ targetEventTypes.push('touchstart', 'touchend', 'mousedown', 'touchcancel');
+ rootEventTypes.push({name: 'mouseup', passive: false});
+}
+
function createPressEvent(
type: PressEventType,
target: Element | Document,
@@ -82,7 +85,7 @@ function createPressEvent(
};
}
-function dispatchPressEvent(
+function dispatchEvent(
context: EventResponderContext,
state: PressState,
name: PressEventType,
@@ -93,23 +96,40 @@ function dispatchPressEvent(
context.dispatchEvent(syntheticEvent, {discrete: true});
}
+function dispatchPressChangeEvent(
+ context: EventResponderContext,
+ props: PressProps,
+ state: PressState,
+): void {
+ const listener = () => {
+ props.onPressChange(state.isPressed);
+ };
+ dispatchEvent(context, state, 'presschange', listener);
+}
+
+function dispatchLongPressChangeEvent(
+ context: EventResponderContext,
+ props: PressProps,
+ state: PressState,
+): void {
+ const listener = () => {
+ props.onLongPressChange(state.isLongPressed);
+ };
+ dispatchEvent(context, state, 'longpresschange', listener);
+}
+
function dispatchPressStartEvents(
context: EventResponderContext,
props: PressProps,
state: PressState,
): void {
- function dispatchPressChangeEvent(bool) {
- const pressChangeEventListener = () => {
- props.onPressChange(bool);
- };
- dispatchPressEvent(context, state, 'presschange', pressChangeEventListener);
- }
+ state.isPressed = true;
if (props.onPressStart) {
- dispatchPressEvent(context, state, 'pressstart', props.onPressStart);
+ dispatchEvent(context, state, 'pressstart', props.onPressStart);
}
if (props.onPressChange) {
- dispatchPressChangeEvent(true);
+ dispatchPressChangeEvent(context, props, state);
}
if ((props.onLongPress || props.onLongPressChange) && !state.isLongPressed) {
const delayLongPress = calculateDelayMS(
@@ -125,31 +145,18 @@ function dispatchPressStartEvents(
state.longPressTimeout = null;
if (props.onLongPress) {
- const longPressEventListener = e => {
+ const listener = e => {
props.onLongPress(e);
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
- dispatchPressEvent(
- context,
- state,
- 'longpress',
- longPressEventListener,
- );
+ dispatchEvent(context, state, 'longpress', listener);
}
if (props.onLongPressChange) {
- const longPressChangeEventListener = () => {
- props.onLongPressChange(true);
- };
- dispatchPressEvent(
- context,
- state,
- 'longpresschange',
- longPressChangeEventListener,
- );
+ dispatchLongPressChangeEvent(context, props, state);
}
}),
delayLongPress,
@@ -167,24 +174,21 @@ function dispatchPressEndEvents(
state.longPressTimeout = null;
}
if (props.onPressEnd) {
- dispatchPressEvent(context, state, 'pressend', props.onPressEnd);
+ dispatchEvent(context, state, 'pressend', props.onPressEnd);
}
- if (props.onPressChange) {
- const pressChangeEventListener = () => {
- props.onPressChange(false);
- };
- dispatchPressEvent(context, state, 'presschange', pressChangeEventListener);
+
+ if (state.isPressed) {
+ state.isPressed = false;
+ if (props.onPressChange) {
+ dispatchPressChangeEvent(context, props, state);
+ }
}
- if (props.onLongPressChange && state.isLongPressed) {
- const longPressChangeEventListener = () => {
- props.onLongPressChange(false);
- };
- dispatchPressEvent(
- context,
- state,
- 'longpresschange',
- longPressChangeEventListener,
- );
+
+ if (state.isLongPressed) {
+ state.isLongPressed = false;
+ if (props.onLongPressChange) {
+ dispatchLongPressChangeEvent(context, props, state);
+ }
}
}
@@ -223,15 +227,74 @@ const PressResponder = {
const {eventTarget, eventType, event} = context;
switch (eventType) {
- case 'keydown': {
+ /**
+ * Respond to pointer events and fall back to mouse.
+ */
+ case 'pointerdown':
+ case 'mousedown': {
if (
- !props.onPress ||
- context.isTargetOwned(eventTarget) ||
- !isValidKeyPress((event: any).key)
+ !state.isPressed &&
+ !context.isTargetOwned(eventTarget) &&
+ !state.shouldSkipMouseAfterTouch
) {
- return;
+ if (
+ (event: any).pointerType === 'mouse' ||
+ eventType === 'mousedown'
+ ) {
+ if (
+ // Ignore right- and middle-clicks
+ event.button === 1 ||
+ event.button === 2 ||
+ // Ignore pressing on hit slop area with mouse
+ context.isPositionWithinTouchHitTarget(
+ (event: any).x,
+ (event: any).y,
+ )
+ ) {
+ return;
+ }
+ }
+ state.pressTarget = eventTarget;
+ dispatchPressStartEvents(context, props, state);
+ context.addRootEventTypes(rootEventTypes);
}
- dispatchPressEvent(context, state, 'press', props.onPress);
+ break;
+ }
+ case 'pointerup':
+ case 'mouseup': {
+ if (state.isPressed) {
+ if (state.shouldSkipMouseAfterTouch) {
+ state.shouldSkipMouseAfterTouch = false;
+ return;
+ }
+
+ const wasLongPressed = state.isLongPressed;
+
+ dispatchPressEndEvents(context, props, state);
+
+ if (state.pressTarget !== null && props.onPress) {
+ if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
+ if (
+ !(
+ wasLongPressed &&
+ props.onLongPressShouldCancelPress &&
+ props.onLongPressShouldCancelPress()
+ )
+ ) {
+ const listener = e => {
+ props.onPress(e);
+ // TODO address this again at some point
+ // if (e.nativeEvent.defaultPrevented) {
+ // state.defaultPrevented = true;
+ // }
+ };
+ dispatchEvent(context, state, 'press', listener);
+ }
+ }
+ }
+ context.removeRootEventTypes(rootEventTypes);
+ }
+ state.isAnchorTouched = false;
break;
}
@@ -239,7 +302,7 @@ const PressResponder = {
* Touch event implementations are only needed for Safari, which lacks
* support for pointer events.
*/
- case 'touchstart':
+ case 'touchstart': {
if (!state.isPressed && !context.isTargetOwned(eventTarget)) {
// We bail out of polyfilling anchor tags, given the same heuristics
// explained above in regards to needing to use click events.
@@ -249,21 +312,21 @@ const PressResponder = {
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
- state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
-
break;
+ }
case 'touchend': {
if (state.isAnchorTouched) {
+ state.isAnchorTouched = false;
return;
}
if (state.isPressed) {
+ const wasLongPressed = state.isLongPressed;
+
dispatchPressEndEvents(context, props, state);
- if (
- eventType !== 'touchcancel' &&
- (props.onPress || props.onLongPress)
- ) {
+
+ if (eventType !== 'touchcancel' && props.onPress) {
// Find if the X/Y of the end touch is still that of the original target
const changedTouch = (event: any).changedTouches[0];
const doc = (eventTarget: any).ownerDocument;
@@ -276,19 +339,16 @@ const PressResponder = {
context.isTargetWithinEventComponent(target)
) {
if (
- props.onPress &&
!(
- state.isLongPressed &&
+ wasLongPressed &&
props.onLongPressShouldCancelPress &&
props.onLongPressShouldCancelPress()
)
) {
- dispatchPressEvent(context, state, 'press', props.onPress);
+ dispatchEvent(context, state, 'press', props.onPress);
}
}
}
- state.isPressed = false;
- state.isLongPressed = false;
state.shouldSkipMouseAfterTouch = true;
context.removeRootEventTypes(rootEventTypes);
}
@@ -296,93 +356,58 @@ const PressResponder = {
}
/**
- * Respond to pointer events and fall back to mouse.
+ * Keyboard interaction support
+ * TODO: determine UX for metaKey + validKeyPress interactions
*/
- case 'pointerdown':
- case 'mousedown': {
+ case 'keydown': {
if (
!state.isPressed &&
+ !state.isLongPressed &&
!context.isTargetOwned(eventTarget) &&
- !state.shouldSkipMouseAfterTouch
+ isValidKeyPress((event: any).key)
) {
- if (
- (event: any).pointerType === 'mouse' ||
- eventType === 'mousedown'
- ) {
- // Ignore if we are pressing on hit slop area with mouse
- if (
- context.isPositionWithinTouchHitTarget(
- (event: any).x,
- (event: any).y,
- )
- ) {
- return;
- }
- // Ignore middle- and right-clicks
- if (event.button === 2 || event.button === 1) {
- return;
- }
+ // Prevent spacebar press from scrolling the window
+ if ((event: any).key === ' ') {
+ (event: any).preventDefault();
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
- state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
break;
}
- case 'pointerup':
- case 'mouseup': {
- if (state.isPressed) {
- if (state.shouldSkipMouseAfterTouch) {
- state.shouldSkipMouseAfterTouch = false;
- return;
- }
+ case 'keyup': {
+ if (state.isPressed && isValidKeyPress((event: any).key)) {
+ const wasLongPressed = state.isLongPressed;
dispatchPressEndEvents(context, props, state);
- if (
- state.pressTarget !== null &&
- (props.onPress || props.onLongPress)
- ) {
- if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
- if (
- props.onPress &&
- !(
- state.isLongPressed &&
- props.onLongPressShouldCancelPress &&
- props.onLongPressShouldCancelPress()
- )
- ) {
- const pressEventListener = e => {
- props.onPress(e);
- // TODO address this again at some point
- // if (e.nativeEvent.defaultPrevented) {
- // state.defaultPrevented = true;
- // }
- };
- dispatchPressEvent(context, state, 'press', pressEventListener);
- }
+ if (state.pressTarget !== null && props.onPress) {
+ if (
+ !(
+ wasLongPressed &&
+ props.onLongPressShouldCancelPress &&
+ props.onLongPressShouldCancelPress()
+ )
+ ) {
+ dispatchEvent(context, state, 'press', props.onPress);
}
}
- state.isPressed = false;
- state.isLongPressed = false;
context.removeRootEventTypes(rootEventTypes);
}
- state.isAnchorTouched = false;
break;
}
- case 'scroll':
- case 'touchcancel':
case 'contextmenu':
- case 'pointercancel': {
+ case 'pointercancel':
+ case 'scroll':
+ case 'touchcancel': {
if (state.isPressed) {
state.shouldSkipMouseAfterTouch = false;
dispatchPressEndEvents(context, props, state);
- state.isPressed = false;
- state.isLongPressed = false;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
+
case 'click': {
if (state.defaultPrevented) {
(event: any).preventDefault();
diff --git a/packages/react-events/src/__tests__/Focus-test.internal.js b/packages/react-events/src/__tests__/Focus-test.internal.js
new file mode 100644
index 0000000000000..0b1287773e7ab
--- /dev/null
+++ b/packages/react-events/src/__tests__/Focus-test.internal.js
@@ -0,0 +1,111 @@
+/**
+ * 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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactFeatureFlags;
+let ReactDOM;
+let Focus;
+
+const createFocusEvent = type => {
+ const event = document.createEvent('Event');
+ event.initEvent(type, true, true);
+ return event;
+};
+
+describe('Focus event responder', () => {
+ let container;
+
+ beforeEach(() => {
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.enableEventAPI = true;
+ React = require('react');
+ ReactDOM = require('react-dom');
+ Focus = require('react-events/focus');
+
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ container = null;
+ });
+
+ describe('onBlur', () => {
+ let onBlur, ref;
+
+ beforeEach(() => {
+ onBlur = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "blur" event', () => {
+ ref.current.dispatchEvent(createFocusEvent('focus'));
+ ref.current.dispatchEvent(createFocusEvent('blur'));
+ expect(onBlur).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('onFocus', () => {
+ let onFocus, ref;
+
+ beforeEach(() => {
+ onFocus = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "focus" event', () => {
+ ref.current.dispatchEvent(createFocusEvent('focus'));
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('onFocusChange', () => {
+ let onFocusChange, ref;
+
+ beforeEach(() => {
+ onFocusChange = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "blur" and "focus" events', () => {
+ ref.current.dispatchEvent(createFocusEvent('focus'));
+ expect(onFocusChange).toHaveBeenCalledTimes(1);
+ expect(onFocusChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createFocusEvent('blur'));
+ expect(onFocusChange).toHaveBeenCalledTimes(2);
+ expect(onFocusChange).toHaveBeenCalledWith(false);
+ });
+ });
+
+ it('expect displayName to show up for event component', () => {
+ expect(Focus.displayName).toBe('Focus');
+ });
+});
diff --git a/packages/react-events/src/__tests__/Hover-test.internal.js b/packages/react-events/src/__tests__/Hover-test.internal.js
index 1250a49917ab3..bd449d8f2348b 100644
--- a/packages/react-events/src/__tests__/Hover-test.internal.js
+++ b/packages/react-events/src/__tests__/Hover-test.internal.js
@@ -14,6 +14,12 @@ let ReactFeatureFlags;
let ReactDOM;
let Hover;
+const createPointerEvent = type => {
+ const event = document.createEvent('Event');
+ event.initEvent(type, true, true);
+ return event;
+};
+
describe('Hover event responder', () => {
let container;
@@ -34,69 +40,154 @@ describe('Hover event responder', () => {
container = null;
});
- it('should support onHover', () => {
- let divRef = React.createRef();
- let events = [];
-
- function handleOnHover(e) {
- if (e) {
- events.push('hover in');
- } else {
- events.push('hover out');
- }
- }
-
- function Component() {
- return (
-
- Hover me!
+ describe('onHoverStart', () => {
+ let onHoverStart, ref;
+
+ beforeEach(() => {
+ onHoverStart = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
);
- }
-
- ReactDOM.render(, container);
-
- const mouseOverEvent = document.createEvent('Event');
- mouseOverEvent.initEvent('mouseover', true, true);
- divRef.current.dispatchEvent(mouseOverEvent);
-
- const mouseOutEvent = document.createEvent('Event');
- mouseOutEvent.initEvent('mouseout', true, true);
- divRef.current.dispatchEvent(mouseOutEvent);
-
- expect(events).toEqual(['hover in', 'hover out']);
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "pointerover" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ expect(onHoverStart).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called if "pointerover" pointerType is touch', () => {
+ const event = createPointerEvent('pointerover');
+ event.pointerType = 'touch';
+ ref.current.dispatchEvent(event);
+ expect(onHoverStart).not.toBeCalled();
+ });
+
+ it('ignores browser emulated "mouseover" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ ref.current.dispatchEvent(createPointerEvent('mouseover'));
+ expect(onHoverStart).toHaveBeenCalledTimes(1);
+ });
+
+ // No PointerEvent fallbacks
+ it('is called after "mouseover" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('mouseover'));
+ expect(onHoverStart).toHaveBeenCalledTimes(1);
+ });
+ it('is not called after "touchstart"', () => {
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
+ ref.current.dispatchEvent(createPointerEvent('touchend'));
+ ref.current.dispatchEvent(createPointerEvent('mouseover'));
+ expect(onHoverStart).not.toBeCalled();
+ });
+
+ // TODO: complete delayHoverStart tests
+ // describe('delayHoverStart', () => {});
});
- it('should support onHoverStart and onHoverEnd', () => {
- let divRef = React.createRef();
- let events = [];
-
- function handleOnHoverStart() {
- events.push('onHoverStart');
- }
+ describe('onHoverChange', () => {
+ let onHoverChange, ref;
- function handleOnHoverEnd() {
- events.push('onHoverEnd');
- }
-
- function Component() {
- return (
-
- Hover me!
+ beforeEach(() => {
+ onHoverChange = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
);
- }
-
- ReactDOM.render(, container);
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "pointerover" and "pointerout" events', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ expect(onHoverChange).toHaveBeenCalledTimes(1);
+ expect(onHoverChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
+ expect(onHoverChange).toHaveBeenCalledTimes(2);
+ expect(onHoverChange).toHaveBeenCalledWith(false);
+ });
+
+ // No PointerEvent fallbacks
+ it('is called after "mouseover" and "mouseout" events', () => {
+ ref.current.dispatchEvent(createPointerEvent('mouseover'));
+ expect(onHoverChange).toHaveBeenCalledTimes(1);
+ expect(onHoverChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createPointerEvent('mouseout'));
+ expect(onHoverChange).toHaveBeenCalledTimes(2);
+ expect(onHoverChange).toHaveBeenCalledWith(false);
+ });
+ });
- const mouseOverEvent = document.createEvent('Event');
- mouseOverEvent.initEvent('mouseover', true, true);
- divRef.current.dispatchEvent(mouseOverEvent);
+ describe('onHoverEnd', () => {
+ let onHoverEnd, ref;
- const mouseOutEvent = document.createEvent('Event');
- mouseOutEvent.initEvent('mouseout', true, true);
- divRef.current.dispatchEvent(mouseOutEvent);
+ beforeEach(() => {
+ onHoverEnd = jest.fn();
+ ref = React.createRef();
+ const element = (
+
+
+
+ );
+ ReactDOM.render(element, container);
+ });
+
+ it('is called after "pointerout" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called if "pointerover" pointerType is touch', () => {
+ const event = createPointerEvent('pointerover');
+ event.pointerType = 'touch';
+ ref.current.dispatchEvent(event);
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
+ expect(onHoverEnd).not.toBeCalled();
+ });
+
+ it('ignores browser emulated "mouseout" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
+ ref.current.dispatchEvent(createPointerEvent('mouseout'));
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+
+ it('is called after "pointercancel" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ ref.current.dispatchEvent(createPointerEvent('pointercancel'));
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called again after "pointercancel" event if it follows "pointerout"', () => {
+ ref.current.dispatchEvent(createPointerEvent('pointerover'));
+ ref.current.dispatchEvent(createPointerEvent('pointerout'));
+ ref.current.dispatchEvent(createPointerEvent('pointercancel'));
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+
+ // No PointerEvent fallbacks
+ it('is called after "mouseout" event', () => {
+ ref.current.dispatchEvent(createPointerEvent('mouseover'));
+ ref.current.dispatchEvent(createPointerEvent('mouseout'));
+ expect(onHoverEnd).toHaveBeenCalledTimes(1);
+ });
+ it('is not called after "touchend"', () => {
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
+ ref.current.dispatchEvent(createPointerEvent('touchend'));
+ ref.current.dispatchEvent(createPointerEvent('mouseout'));
+ expect(onHoverEnd).not.toBeCalled();
+ });
+
+ // TODO: complete delayHoverStart tests
+ // describe('delayHoverEnd', () => {});
+ });
- expect(events).toEqual(['onHoverStart', 'onHoverEnd']);
+ it('expect displayName to show up for event component', () => {
+ expect(Hover.displayName).toBe('Hover');
});
});
diff --git a/packages/react-events/src/__tests__/Press-test.internal.js b/packages/react-events/src/__tests__/Press-test.internal.js
index af3b93239e480..504f7e49d8d85 100644
--- a/packages/react-events/src/__tests__/Press-test.internal.js
+++ b/packages/react-events/src/__tests__/Press-test.internal.js
@@ -14,7 +14,7 @@ let ReactFeatureFlags;
let ReactDOM;
let Press;
-const DEFAULT_LONG_PRESS_DELAY = 1000;
+const DEFAULT_LONG_PRESS_DELAY = 500;
const createPointerEvent = type => {
const event = document.createEvent('Event');
@@ -69,12 +69,31 @@ describe('Event responder: Press', () => {
expect(onPressStart).toHaveBeenCalledTimes(1);
});
- it('ignores emulated "mousedown" event', () => {
+ it('ignores browser emulated "mousedown" event', () => {
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
ref.current.dispatchEvent(createPointerEvent('mousedown'));
expect(onPressStart).toHaveBeenCalledTimes(1);
});
+ it('is called once after "keydown" events for Enter', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ });
+
+ it('is called once after "keydown" events for Spacebar', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
+ expect(onPressStart).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called after "keydown" for other keys', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'a'}));
+ expect(onPressStart).not.toBeCalled();
+ });
+
// No PointerEvent fallbacks
it('is called after "mousedown" event', () => {
ref.current.dispatchEvent(createPointerEvent('mousedown'));
@@ -109,20 +128,37 @@ describe('Event responder: Press', () => {
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
- it('ignores emulated "mouseup" event', () => {
+ it('ignores browser emulated "mouseup" event', () => {
ref.current.dispatchEvent(createPointerEvent('touchstart'));
ref.current.dispatchEvent(createPointerEvent('touchend'));
ref.current.dispatchEvent(createPointerEvent('mouseup'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
+ it('is called after "keyup" event for Enter', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ });
+
+ it('is called after "keyup" event for Spacebar', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '}));
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: ' '}));
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called after "keyup" event for other keys', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'a'}));
+ expect(onPressEnd).not.toBeCalled();
+ });
+
// No PointerEvent fallbacks
it('is called after "mouseup" event', () => {
ref.current.dispatchEvent(createPointerEvent('mousedown'));
ref.current.dispatchEvent(createPointerEvent('mouseup'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
-
it('is called after "touchend" event', () => {
ref.current.dispatchEvent(createPointerEvent('touchstart'));
ref.current.dispatchEvent(createPointerEvent('touchend'));
@@ -155,6 +191,33 @@ describe('Event responder: Press', () => {
expect(onPressChange).toHaveBeenCalledTimes(2);
expect(onPressChange).toHaveBeenCalledWith(false);
});
+
+ it('is called after valid "keydown" and "keyup" events', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
+ });
+
+ // No PointerEvent fallbacks
+ it('is called after "mousedown" and "mouseup" events', () => {
+ ref.current.dispatchEvent(createPointerEvent('mousedown'));
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createPointerEvent('mouseup'));
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
+ });
+ it('is called after "touchstart" and "touchend" events', () => {
+ ref.current.dispatchEvent(createPointerEvent('touchstart'));
+ expect(onPressChange).toHaveBeenCalledTimes(1);
+ expect(onPressChange).toHaveBeenCalledWith(true);
+ ref.current.dispatchEvent(createPointerEvent('touchend'));
+ expect(onPressChange).toHaveBeenCalledTimes(2);
+ expect(onPressChange).toHaveBeenCalledWith(false);
+ });
});
describe('onPress', () => {
@@ -176,6 +239,20 @@ describe('Event responder: Press', () => {
ref.current.dispatchEvent(createPointerEvent('pointerup'));
expect(onPress).toHaveBeenCalledTimes(1);
});
+
+ it('is called after valid "keyup" event', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
+ expect(onPress).toHaveBeenCalledTimes(1);
+ });
+
+ // No PointerEvent fallbacks
+ // TODO: jsdom missing APIs
+ //it('is called after "touchend" event', () => {
+ //ref.current.dispatchEvent(createPointerEvent('touchstart'));
+ //ref.current.dispatchEvent(createPointerEvent('touchend'));
+ //expect(onPress).toHaveBeenCalledTimes(1);
+ //});
});
describe('onLongPress', () => {
@@ -192,7 +269,7 @@ describe('Event responder: Press', () => {
ReactDOM.render(element, container);
});
- it('is called if press lasts default delay', () => {
+ it('is called if "pointerdown" lasts default delay', () => {
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
expect(onLongPress).not.toBeCalled();
@@ -200,7 +277,7 @@ describe('Event responder: Press', () => {
expect(onLongPress).toHaveBeenCalledTimes(1);
});
- it('is not called if press is released before delay', () => {
+ it('is not called if "pointerup" is dispatched before delay', () => {
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
ref.current.dispatchEvent(createPointerEvent('pointerup'));
@@ -208,6 +285,22 @@ describe('Event responder: Press', () => {
expect(onLongPress).not.toBeCalled();
});
+ it('is called if valid "keydown" lasts default delay', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
+ expect(onLongPress).not.toBeCalled();
+ jest.advanceTimersByTime(1);
+ expect(onLongPress).toHaveBeenCalledTimes(1);
+ });
+
+ it('is not called if valid "keyup" is dispatched before delay', () => {
+ ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
+ jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY - 1);
+ ref.current.dispatchEvent(createKeyboardEvent('keyup', {key: 'Enter'}));
+ jest.advanceTimersByTime(1);
+ expect(onLongPress).not.toBeCalled();
+ });
+
describe('delayLongPress', () => {
it('can be configured', () => {
const element = (
@@ -339,7 +432,7 @@ describe('Event responder: Press', () => {
describe('nested responders', () => {
it('dispatch events in the correct order', () => {
- let events = [];
+ const events = [];
const ref = React.createRef();
const createEventHandler = msg => () => {
events.push(msg);
@@ -385,13 +478,6 @@ describe('Event responder: Press', () => {
'outer: onPressChange',
'outer: onPress',
]);
-
- events = [];
- ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
- // TODO update this test once we have a form of stopPropagation in
- // the responder system again. This test had to be updated because
- // we have removed stopPropagation() from synthetic events.
- expect(events).toEqual(['keydown', 'inner: onPress', 'outer: onPress']);
});
});