React events: keyboard press, types, tests (#15314)

* Add HoverProps type
* Add more Hover event module tests
* Add more Press event module tests
* Change default longPress delay from 1000 to 500
* Rename dispatchPressEvent -> dispatchEvent
* Consolidate state updates in Press event module
* Add keyboard support for Press events
* Add FocusProps type and unit tests
This commit is contained in:
Nicolas Gallagher
2019-04-04 08:55:35 -07:00
committed by GitHub
parent 7a2dc48539
commit 937d262f55
6 changed files with 596 additions and 253 deletions
+23 -10
View File
@@ -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});
}
+41 -24
View File
@@ -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;
}
+182 -157
View File
@@ -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,78 +227,6 @@ const PressResponder = {
const {eventTarget, eventType, event} = context;
switch (eventType) {
case 'keydown': {
if (
!props.onPress ||
context.isTargetOwned(eventTarget) ||
!isValidKeyPress((event: any).key)
) {
return;
}
dispatchPressEvent(context, state, 'press', props.onPress);
break;
}
/**
* Touch event implementations are only needed for Safari, which lacks
* support for pointer events.
*/
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.
if (isAnchorTagElement(eventTarget)) {
state.isAnchorTouched = true;
return;
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
break;
case 'touchend': {
if (state.isAnchorTouched) {
return;
}
if (state.isPressed) {
dispatchPressEndEvents(context, props, state);
if (
eventType !== 'touchcancel' &&
(props.onPress || props.onLongPress)
) {
// 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;
const target = doc.elementFromPoint(
changedTouch.screenX,
changedTouch.screenY,
);
if (
target !== null &&
context.isTargetWithinEventComponent(target)
) {
if (
props.onPress &&
!(
state.isLongPressed &&
props.onLongPressShouldCancelPress &&
props.onLongPressShouldCancelPress()
)
) {
dispatchPressEvent(context, state, 'press', props.onPress);
}
}
}
state.isPressed = false;
state.isLongPressed = false;
state.shouldSkipMouseAfterTouch = true;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
/**
* Respond to pointer events and fall back to mouse.
*/
@@ -309,8 +241,11 @@ const PressResponder = {
(event: any).pointerType === 'mouse' ||
eventType === 'mousedown'
) {
// Ignore if we are pressing on hit slop area with mouse
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,
@@ -318,14 +253,9 @@ const PressResponder = {
) {
return;
}
// Ignore middle- and right-clicks
if (event.button === 2 || event.button === 1) {
return;
}
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
state.isPressed = true;
context.addRootEventTypes(rootEventTypes);
}
break;
@@ -337,52 +267,147 @@ const PressResponder = {
state.shouldSkipMouseAfterTouch = false;
return;
}
const wasLongPressed = state.isLongPressed;
dispatchPressEndEvents(context, props, state);
if (
state.pressTarget !== null &&
(props.onPress || props.onLongPress)
) {
if (state.pressTarget !== null && props.onPress) {
if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
if (
props.onPress &&
!(
state.isLongPressed &&
wasLongPressed &&
props.onLongPressShouldCancelPress &&
props.onLongPressShouldCancelPress()
)
) {
const pressEventListener = e => {
const listener = e => {
props.onPress(e);
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
dispatchPressEvent(context, state, 'press', pressEventListener);
dispatchEvent(context, state, 'press', listener);
}
}
}
state.isPressed = false;
state.isLongPressed = false;
context.removeRootEventTypes(rootEventTypes);
}
state.isAnchorTouched = false;
break;
}
case 'scroll':
case 'touchcancel':
case 'contextmenu':
case 'pointercancel': {
/**
* Touch event implementations are only needed for Safari, which lacks
* support for pointer events.
*/
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.
if (isAnchorTagElement(eventTarget)) {
state.isAnchorTouched = true;
return;
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(rootEventTypes);
}
break;
}
case 'touchend': {
if (state.isAnchorTouched) {
state.isAnchorTouched = false;
return;
}
if (state.isPressed) {
state.shouldSkipMouseAfterTouch = false;
const wasLongPressed = state.isLongPressed;
dispatchPressEndEvents(context, props, state);
state.isPressed = false;
state.isLongPressed = false;
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;
const target = doc.elementFromPoint(
changedTouch.screenX,
changedTouch.screenY,
);
if (
target !== null &&
context.isTargetWithinEventComponent(target)
) {
if (
!(
wasLongPressed &&
props.onLongPressShouldCancelPress &&
props.onLongPressShouldCancelPress()
)
) {
dispatchEvent(context, state, 'press', props.onPress);
}
}
}
state.shouldSkipMouseAfterTouch = true;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
/**
* Keyboard interaction support
* TODO: determine UX for metaKey + validKeyPress interactions
*/
case 'keydown': {
if (
!state.isPressed &&
!state.isLongPressed &&
!context.isTargetOwned(eventTarget) &&
isValidKeyPress((event: any).key)
) {
// Prevent spacebar press from scrolling the window
if ((event: any).key === ' ') {
(event: any).preventDefault();
}
state.pressTarget = eventTarget;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(rootEventTypes);
}
break;
}
case 'keyup': {
if (state.isPressed && isValidKeyPress((event: any).key)) {
const wasLongPressed = state.isLongPressed;
dispatchPressEndEvents(context, props, state);
if (state.pressTarget !== null && props.onPress) {
if (
!(
wasLongPressed &&
props.onLongPressShouldCancelPress &&
props.onLongPressShouldCancelPress()
)
) {
dispatchEvent(context, state, 'press', props.onPress);
}
}
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'contextmenu':
case 'pointercancel':
case 'scroll':
case 'touchcancel': {
if (state.isPressed) {
state.shouldSkipMouseAfterTouch = false;
dispatchPressEndEvents(context, props, state);
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'click': {
if (state.defaultPrevented) {
(event: any).preventDefault();
@@ -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 = (
<Focus onBlur={onBlur}>
<div ref={ref} />
</Focus>
);
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 = (
<Focus onFocus={onFocus}>
<div ref={ref} />
</Focus>
);
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 = (
<Focus onFocusChange={onFocusChange}>
<div ref={ref} />
</Focus>
);
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');
});
});
@@ -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 = [];
describe('onHoverStart', () => {
let onHoverStart, ref;
function handleOnHover(e) {
if (e) {
events.push('hover in');
} else {
events.push('hover out');
}
}
function Component() {
return (
<Hover onHoverChange={handleOnHover}>
<div ref={divRef}>Hover me!</div>
beforeEach(() => {
onHoverStart = jest.fn();
ref = React.createRef();
const element = (
<Hover onHoverStart={onHoverStart}>
<div ref={ref} />
</Hover>
);
}
ReactDOM.render(element, container);
});
ReactDOM.render(<Component />, container);
it('is called after "pointerover" event', () => {
ref.current.dispatchEvent(createPointerEvent('pointerover'));
expect(onHoverStart).toHaveBeenCalledTimes(1);
});
const mouseOverEvent = document.createEvent('Event');
mouseOverEvent.initEvent('mouseover', true, true);
divRef.current.dispatchEvent(mouseOverEvent);
it('is not called if "pointerover" pointerType is touch', () => {
const event = createPointerEvent('pointerover');
event.pointerType = 'touch';
ref.current.dispatchEvent(event);
expect(onHoverStart).not.toBeCalled();
});
const mouseOutEvent = document.createEvent('Event');
mouseOutEvent.initEvent('mouseout', true, true);
divRef.current.dispatchEvent(mouseOutEvent);
it('ignores browser emulated "mouseover" event', () => {
ref.current.dispatchEvent(createPointerEvent('pointerover'));
ref.current.dispatchEvent(createPointerEvent('mouseover'));
expect(onHoverStart).toHaveBeenCalledTimes(1);
});
expect(events).toEqual(['hover in', 'hover out']);
// 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 = [];
describe('onHoverChange', () => {
let onHoverChange, ref;
function handleOnHoverStart() {
events.push('onHoverStart');
}
function handleOnHoverEnd() {
events.push('onHoverEnd');
}
function Component() {
return (
<Hover onHoverStart={handleOnHoverStart} onHoverEnd={handleOnHoverEnd}>
<div ref={divRef}>Hover me!</div>
beforeEach(() => {
onHoverChange = jest.fn();
ref = React.createRef();
const element = (
<Hover onHoverChange={onHoverChange}>
<div ref={ref} />
</Hover>
);
}
ReactDOM.render(element, container);
});
ReactDOM.render(<Component />, 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);
});
const mouseOverEvent = document.createEvent('Event');
mouseOverEvent.initEvent('mouseover', true, true);
divRef.current.dispatchEvent(mouseOverEvent);
// 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 mouseOutEvent = document.createEvent('Event');
mouseOutEvent.initEvent('mouseout', true, true);
divRef.current.dispatchEvent(mouseOutEvent);
describe('onHoverEnd', () => {
let onHoverEnd, ref;
expect(events).toEqual(['onHoverStart', 'onHoverEnd']);
beforeEach(() => {
onHoverEnd = jest.fn();
ref = React.createRef();
const element = (
<Hover onHoverEnd={onHoverEnd}>
<div ref={ref} />
</Hover>
);
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', () => {});
});
it('expect displayName to show up for event component', () => {
expect(Hover.displayName).toBe('Hover');
});
});
@@ -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']);
});
});