react/packages/react-interactions/events/src/dom/Scroll.js

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);
}