320 lines
7.5 KiB
JavaScript
320 lines
7.5 KiB
JavaScript
/**
|
|
* 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 {
|
|
ReactDOMResponderEvent,
|
|
ReactDOMResponderContext,
|
|
PointerType,
|
|
} from 'shared/ReactDOMTypes';
|
|
import {UserBlockingEvent} from 'shared/ReactTypes';
|
|
import type {
|
|
EventPriority,
|
|
ReactEventResponderListener,
|
|
} from 'shared/ReactTypes';
|
|
|
|
import React from 'react';
|
|
|
|
type ScrollProps = {
|
|
disabled: boolean,
|
|
onScroll: ScrollEvent => void,
|
|
onScrollDragStart: ScrollEvent => void,
|
|
onScrollDragEnd: ScrollEvent => void,
|
|
...
|
|
};
|
|
|
|
type ScrollState = {
|
|
direction: ScrollDirection,
|
|
pointerType: PointerType,
|
|
scrollTarget: null | Element | Document,
|
|
isDragging: boolean,
|
|
isTouching: boolean,
|
|
scrollLeft: number,
|
|
scrollTop: number,
|
|
...
|
|
};
|
|
|
|
type ScrollEventType =
|
|
| 'scroll'
|
|
| 'scrolldragstart'
|
|
| 'scrolldragend'
|
|
| 'scrollmomentumstart'
|
|
| 'scrollmomentumend';
|
|
|
|
type ScrollDirection = '' | 'up' | 'down' | 'left' | 'right';
|
|
|
|
type ScrollEvent = {|
|
|
direction: ScrollDirection,
|
|
target: Element | Document,
|
|
type: ScrollEventType,
|
|
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,
|
|
|};
|
|
|
|
const hasPointerEvents =
|
|
typeof window !== 'undefined' && window.PointerEvent !== undefined;
|
|
|
|
const targetEventTypes = hasPointerEvents
|
|
? ['scroll', 'pointerdown', 'keyup', 'wheel']
|
|
: ['scroll', 'mousedown', 'touchstart', 'keyup', 'wheel'];
|
|
|
|
const rootEventTypes = hasPointerEvents
|
|
? ['pointercancel', 'pointerup']
|
|
: ['touchcancel', 'touchend'];
|
|
|
|
function isFunction(obj): boolean {
|
|
return typeof obj === 'function';
|
|
}
|
|
|
|
function createScrollEvent(
|
|
event: ?ReactDOMResponderEvent,
|
|
context: ReactDOMResponderContext,
|
|
type: ScrollEventType,
|
|
target: Element | Document,
|
|
pointerType: PointerType,
|
|
direction: ScrollDirection,
|
|
): ScrollEvent {
|
|
let clientX = null;
|
|
let clientY = null;
|
|
let pageX = null;
|
|
let pageY = null;
|
|
let screenX = null;
|
|
let screenY = null;
|
|
|
|
if (event) {
|
|
const nativeEvent = (event.nativeEvent: any);
|
|
({clientX, clientY, pageX, pageY, screenX, screenY} = nativeEvent);
|
|
}
|
|
|
|
return {
|
|
target,
|
|
type,
|
|
pointerType,
|
|
direction,
|
|
timeStamp: context.getTimeStamp(),
|
|
clientX,
|
|
clientY,
|
|
pageX,
|
|
pageY,
|
|
screenX,
|
|
screenY,
|
|
x: clientX,
|
|
y: clientY,
|
|
};
|
|
}
|
|
|
|
function dispatchEvent(
|
|
event: ?ReactDOMResponderEvent,
|
|
listener: ScrollEvent => void,
|
|
context: ReactDOMResponderContext,
|
|
state: ScrollState,
|
|
name: ScrollEventType,
|
|
eventPriority: EventPriority,
|
|
): void {
|
|
const target = ((state.scrollTarget: any): Element | Document);
|
|
const pointerType = state.pointerType;
|
|
const direction = state.direction;
|
|
const syntheticEvent = createScrollEvent(
|
|
event,
|
|
context,
|
|
name,
|
|
target,
|
|
pointerType,
|
|
direction,
|
|
);
|
|
context.dispatchEvent(syntheticEvent, listener, eventPriority);
|
|
}
|
|
|
|
const scrollResponderImpl = {
|
|
targetEventTypes,
|
|
getInitialState() {
|
|
return {
|
|
direction: '',
|
|
isTouching: false,
|
|
pointerType: '',
|
|
prevScrollTop: 0,
|
|
prevScrollLeft: 0,
|
|
scrollTarget: null,
|
|
};
|
|
},
|
|
onEvent(
|
|
event: ReactDOMResponderEvent,
|
|
context: ReactDOMResponderContext,
|
|
props: ScrollProps,
|
|
state: ScrollState,
|
|
): void {
|
|
const {pointerType, target, type} = event;
|
|
|
|
if (props.disabled) {
|
|
if (state.isTouching) {
|
|
state.isTouching = false;
|
|
state.scrollTarget = null;
|
|
state.isDragging = false;
|
|
state.direction = '';
|
|
context.removeRootEventTypes(rootEventTypes);
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'scroll': {
|
|
const prevScrollTarget = state.scrollTarget;
|
|
let scrollLeft = 0;
|
|
let scrollTop = 0;
|
|
|
|
// Check if target is the document
|
|
if (target.nodeType === 9) {
|
|
const bodyNode = ((target: any): Document).body;
|
|
if (bodyNode !== null) {
|
|
scrollLeft = bodyNode.offsetLeft;
|
|
scrollTop = bodyNode.offsetTop;
|
|
}
|
|
} else {
|
|
scrollLeft = ((target: any): Element).scrollLeft;
|
|
scrollTop = ((target: any): Element).scrollTop;
|
|
}
|
|
|
|
if (prevScrollTarget !== null) {
|
|
if (scrollTop === state.scrollTop) {
|
|
if (scrollLeft > state.scrollLeft) {
|
|
state.direction = 'right';
|
|
} else {
|
|
state.direction = 'left';
|
|
}
|
|
} else {
|
|
if (scrollTop > state.scrollTop) {
|
|
state.direction = 'down';
|
|
} else {
|
|
state.direction = 'up';
|
|
}
|
|
}
|
|
} else {
|
|
state.direction = '';
|
|
}
|
|
state.scrollTarget = ((target: any): Element | Document);
|
|
state.scrollLeft = scrollLeft;
|
|
state.scrollTop = scrollTop;
|
|
|
|
if (state.isTouching && !state.isDragging) {
|
|
state.isDragging = true;
|
|
const onScrollDragStart = props.onScrollDragStart;
|
|
|
|
if (isFunction(onScrollDragStart)) {
|
|
dispatchEvent(
|
|
event,
|
|
onScrollDragStart,
|
|
context,
|
|
state,
|
|
'scrolldragstart',
|
|
UserBlockingEvent,
|
|
);
|
|
}
|
|
}
|
|
const onScroll = props.onScroll;
|
|
|
|
if (isFunction(onScroll)) {
|
|
dispatchEvent(
|
|
event,
|
|
onScroll,
|
|
context,
|
|
state,
|
|
'scroll',
|
|
UserBlockingEvent,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case 'keyup': {
|
|
state.pointerType = pointerType;
|
|
break;
|
|
}
|
|
case 'mousedown':
|
|
case 'wheel': {
|
|
state.pointerType = 'mouse';
|
|
break;
|
|
}
|
|
case 'pointerdown': {
|
|
state.pointerType = pointerType;
|
|
if (pointerType === 'touch' && !state.isTouching) {
|
|
state.isTouching = true;
|
|
context.addRootEventTypes(rootEventTypes);
|
|
}
|
|
break;
|
|
}
|
|
case 'touchstart': {
|
|
if (!state.isTouching) {
|
|
state.isTouching = true;
|
|
state.pointerType = 'touch';
|
|
context.addRootEventTypes(rootEventTypes);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onRootEvent(
|
|
event: ReactDOMResponderEvent,
|
|
context: ReactDOMResponderContext,
|
|
props: ScrollProps,
|
|
state: ScrollState,
|
|
) {
|
|
const {type} = event;
|
|
|
|
switch (type) {
|
|
case 'pointercancel':
|
|
case 'pointerup':
|
|
case 'touchcancel':
|
|
case 'touchend': {
|
|
if (state.isTouching) {
|
|
const onScrollDragEnd = props.onScrollDragEnd;
|
|
|
|
if (state.isDragging && isFunction(onScrollDragEnd)) {
|
|
dispatchEvent(
|
|
event,
|
|
onScrollDragEnd,
|
|
context,
|
|
state,
|
|
'scrolldragend',
|
|
UserBlockingEvent,
|
|
);
|
|
}
|
|
state.isTouching = false;
|
|
state.isDragging = false;
|
|
state.scrollTarget = null;
|
|
state.pointerType = '';
|
|
context.removeRootEventTypes(rootEventTypes);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onUnmount(
|
|
context: ReactDOMResponderContext,
|
|
props: ScrollProps,
|
|
state: ScrollState,
|
|
) {
|
|
// TODO
|
|
},
|
|
};
|
|
|
|
export const ScrollResponder = React.DEPRECATED_createResponder(
|
|
'Scroll',
|
|
scrollResponderImpl,
|
|
);
|
|
|
|
export function useScroll(
|
|
props: ScrollProps,
|
|
): ReactEventResponderListener<any, any> {
|
|
return React.DEPRECATED_useResponder(ScrollResponder, props);
|
|
}
|