Skip to content

Commit

Permalink
[Flare] Add more functionality to Scroll event resonder (facebook#16036)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Jul 3, 2019
1 parent 4dc279d commit e9aa2e5
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 27 deletions.
125 changes: 102 additions & 23 deletions packages/react-events/src/dom/Scroll.js
Expand Up @@ -23,14 +23,16 @@ type ScrollProps = {
onScroll: ScrollEvent => void,
onScrollDragStart: ScrollEvent => void,
onScrollDragEnd: ScrollEvent => void,
onScrollMomentumStart: ScrollEvent => void,
onScrollMomentumEnd: ScrollEvent => void,
};

type ScrollState = {
direction: ScrollDirection,
pointerType: PointerType,
scrollTarget: null | Element | Document,
isPointerDown: boolean,
isDragging: boolean,
isTouching: boolean,
scrollLeft: number,
scrollTop: number,
};

type ScrollEventType =
Expand Down Expand Up @@ -58,15 +60,22 @@ type ScrollEvent = {|
y: null | number,
|};

const targetEventTypes = ['scroll', 'pointerdown', 'keyup'];
const rootEventTypes = ['pointermove', 'pointerup', 'pointercancel'];
const targetEventTypes = [
'scroll',
'pointerdown',
'touchstart',
'keyup',
'wheel',
];
const rootEventTypes = ['touchcancel', 'touchend'];

function createScrollEvent(
event: ?ReactDOMResponderEvent,
context: ReactDOMResponderContext,
type: ScrollEventType,
target: Element | Document,
pointerType: PointerType,
direction: ScrollDirection,
): ScrollEvent {
let clientX = null;
let clientY = null;
Expand All @@ -84,7 +93,7 @@ function createScrollEvent(
target,
type,
pointerType,
direction: '', // TODO
direction,
timeStamp: context.getTimeStamp(),
clientX,
clientY,
Expand All @@ -107,12 +116,14 @@ function dispatchEvent(
): 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);
}
Expand All @@ -122,9 +133,12 @@ const ScrollResponder: ReactDOMEventResponder = {
targetEventTypes,
createInitialState() {
return {
direction: '',
isTouching: false,
pointerType: '',
prevScrollTop: 0,
prevScrollLeft: 0,
scrollTarget: null,
isPointerDown: false,
};
},
allowMultipleHostChildren: true,
Expand All @@ -138,17 +152,68 @@ const ScrollResponder: ReactDOMEventResponder = {
const {pointerType, target, type} = event;

if (props.disabled) {
if (state.isPointerDown) {
state.isPointerDown = false;
if (state.isTouching) {
state.isTouching = false;
state.scrollTarget = null;
context.addRootEventTypes(rootEventTypes);
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;
if (props.onScrollDragStart) {
dispatchEvent(
event,
context,
state,
'scrolldragstart',
props.onScrollDragStart,
UserBlockingEvent,
);
}
}
if (props.onScroll) {
dispatchEvent(
event,
Expand All @@ -165,13 +230,19 @@ const ScrollResponder: ReactDOMEventResponder = {
state.pointerType = pointerType;
break;
}
case 'wheel': {
state.pointerType = 'mouse';
break;
}
case 'pointerdown': {
state.pointerType = pointerType;
if (!state.isPointerDown) {
state.isPointerDown = true;
break;
}
case 'touchstart': {
if (!state.isTouching) {
state.isTouching = true;
context.addRootEventTypes(rootEventTypes);
}
break;
}
}
},
Expand All @@ -181,20 +252,28 @@ const ScrollResponder: ReactDOMEventResponder = {
props: ScrollProps,
state: ScrollState,
) {
const {pointerType, type} = event;
const {type} = event;

switch (type) {
case 'pointercancel':
case 'pointerup': {
state.pointerType = pointerType;
if (state.isPointerDown) {
state.isPointerDown = false;
case 'touchcancel':
case 'touchend': {
if (state.isTouching) {
if (state.isDragging && props.onScrollDragEnd) {
dispatchEvent(
event,
context,
state,
'scrolldragend',
props.onScrollDragEnd,
UserBlockingEvent,
);
}
state.isTouching = false;
state.isDragging = false;
state.scrollTarget = null;
state.pointerType = '';
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'pointermove': {
state.pointerType = pointerType;
}
}
},
Expand Down
151 changes: 147 additions & 4 deletions packages/react-events/src/dom/__tests__/Scroll-test.internal.js
Expand Up @@ -90,7 +90,31 @@ describe('Scroll event responder', () => {
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse', type: 'scroll'}),
expect.objectContaining({
pointerType: 'mouse',
type: 'scroll',
direction: '',
}),
);
onScroll.mockReset();
ref.current.scrollTop = -1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'mouse',
type: 'scroll',
direction: 'up',
}),
);
onScroll.mockReset();
ref.current.scrollTop = 1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'mouse',
type: 'scroll',
direction: 'down',
}),
);
});

Expand All @@ -103,7 +127,31 @@ describe('Scroll event responder', () => {
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch', type: 'scroll'}),
expect.objectContaining({
pointerType: 'touch',
type: 'scroll',
direction: '',
}),
);
onScroll.mockReset();
ref.current.scrollTop = -1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'touch',
type: 'scroll',
direction: 'up',
}),
);
onScroll.mockReset();
ref.current.scrollTop = 1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'touch',
type: 'scroll',
direction: 'down',
}),
);
});

Expand All @@ -116,7 +164,31 @@ describe('Scroll event responder', () => {
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'pen', type: 'scroll'}),
expect.objectContaining({
pointerType: 'pen',
type: 'scroll',
direction: '',
}),
);
onScroll.mockReset();
ref.current.scrollTop = -1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'pen',
type: 'scroll',
direction: 'up',
}),
);
onScroll.mockReset();
ref.current.scrollTop = 1;
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'pen',
type: 'scroll',
direction: 'down',
}),
);
});

Expand All @@ -134,9 +206,80 @@ describe('Scroll event responder', () => {
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'keyboard', type: 'scroll'}),
expect.objectContaining({
pointerType: 'keyboard',
type: 'scroll',
direction: '',
}),
);
});
});
});

describe('onScrollDragStart', () => {
let onScrollDragStart, ref;

beforeEach(() => {
onScrollDragStart = jest.fn();
ref = React.createRef();
const element = (
<Scroll onScrollDragStart={onScrollDragStart}>
<div ref={ref} />
</Scroll>
);
ReactDOM.render(element, container);
});

it('works as expected with touch events', () => {
ref.current.dispatchEvent(
createEvent('pointerdown', {
pointerType: 'touch',
}),
);
ref.current.dispatchEvent(createEvent('touchstart'));
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScrollDragStart).toHaveBeenCalledTimes(1);
expect(onScrollDragStart).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'touch',
type: 'scrolldragstart',
direction: '',
}),
);
});
});

describe('onScrollDragEnd', () => {
let onScrollDragEnd, ref;

beforeEach(() => {
onScrollDragEnd = jest.fn();
ref = React.createRef();
const element = (
<Scroll onScrollDragEnd={onScrollDragEnd}>
<div ref={ref} />
</Scroll>
);
ReactDOM.render(element, container);
});

it('works as expected with touch events', () => {
ref.current.dispatchEvent(
createEvent('pointerdown', {
pointerType: 'touch',
}),
);
ref.current.dispatchEvent(createEvent('touchstart'));
ref.current.dispatchEvent(createEvent('scroll'));
ref.current.dispatchEvent(createEvent('touchend'));
expect(onScrollDragEnd).toHaveBeenCalledTimes(1);
expect(onScrollDragEnd).toHaveBeenCalledWith(
expect.objectContaining({
pointerType: 'touch',
type: 'scrolldragend',
direction: '',
}),
);
});
});
});

0 comments on commit e9aa2e5

Please sign in to comment.