Skip to content

Commit

Permalink
[Flare] Add basic Scroll event responder module (#15827)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Jun 5, 2019
1 parent c72dcef commit 73c27d8
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/react-events/npm/scroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-scroll.production.min.js');
} else {
module.exports = require('./cjs/react-events-scroll.development.js');
}
1 change: 1 addition & 0 deletions packages/react-events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"focus.js",
"swipe.js",
"drag.js",
"scroll.js",
"focus-scope.js",
"index.js",
"build-info.json",
Expand Down
14 changes: 14 additions & 0 deletions packages/react-events/scroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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
*/

'use strict';

const Scroll = require('./src/Scroll');

module.exports = Scroll.default || Scroll;
218 changes: 218 additions & 0 deletions packages/react-events/src/Scroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/**
* 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 {
ReactResponderEvent,
ReactResponderContext,
} from 'shared/ReactTypes';
import {UserBlockingEvent} from 'shared/ReactTypes';
import type {EventPriority} from 'shared/ReactTypes';

import React from 'react';

type ScrollProps = {
disabled: boolean,
onScroll: ScrollEvent => void,
onScrollDragStart: ScrollEvent => void,
onScrollDragEnd: ScrollEvent => void,
onScrollMomentumStart: ScrollEvent => void,
onScrollMomentumEnd: ScrollEvent => void,
};

type ScrollState = {
pointerType: PointerType,
scrollTarget: null | Element | Document,
isPointerDown: boolean,
};

type ScrollEventType =
| 'scroll'
| 'scrolldragstart'
| 'scrolldragend'
| 'scrollmomentumstart'
| 'scrollmomentumend';

type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';

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 targetEventTypes = ['scroll', 'pointerdown', 'keyup'];
const rootEventTypes = ['pointermove', 'pointerup', 'pointercancel'];

function createScrollEvent(
event: ?ReactResponderEvent,
context: ReactResponderContext,
type: ScrollEventType,
target: Element | Document,
pointerType: PointerType,
): 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: '', // TODO
timeStamp: context.getTimeStamp(),
clientX,
clientY,
pageX,
pageY,
screenX,
screenY,
x: clientX,
y: clientY,
};
}

function dispatchEvent(
event: ?ReactResponderEvent,
context: ReactResponderContext,
state: ScrollState,
name: ScrollEventType,
listener: (e: Object) => void,
eventPriority: EventPriority,
): void {
const target = ((state.scrollTarget: any): Element | Document);
const pointerType = state.pointerType;
const syntheticEvent = createScrollEvent(
event,
context,
name,
target,
pointerType,
);
context.dispatchEvent(syntheticEvent, listener, eventPriority);
}

const ScrollResponder = {
targetEventTypes,
createInitialState() {
return {
pointerType: '',
scrollTarget: null,
isPointerDown: false,
};
},
allowMultipleHostChildren: true,
stopLocalPropagation: true,
onEvent(
event: ReactResponderEvent,
context: ReactResponderContext,
props: ScrollProps,
state: ScrollState,
): void {
const {target, type} = event;

if (props.disabled) {
if (state.isPointerDown) {
state.isPointerDown = false;
state.scrollTarget = null;
context.addRootEventTypes(rootEventTypes);
}
return;
}
const pointerType = context.getEventPointerType(event);

switch (type) {
case 'scroll': {
state.scrollTarget = ((target: any): Element | Document);
if (props.onScroll) {
dispatchEvent(
event,
context,
state,
'scroll',
props.onScroll,
UserBlockingEvent,
);
}
break;
}
case 'keyup': {
state.pointerType = pointerType;
break;
}
case 'pointerdown': {
state.pointerType = pointerType;
if (!state.isPointerDown) {
state.isPointerDown = true;
context.addRootEventTypes(rootEventTypes);
}
break;
}
}
},
onRootEvent(
event: ReactResponderEvent,
context: ReactResponderContext,
props: ScrollProps,
state: ScrollState,
) {
const {type} = event;
const pointerType = context.getEventPointerType(event);

switch (type) {
case 'pointercancel':
case 'pointerup': {
state.pointerType = pointerType;
if (state.isPointerDown) {
state.isPointerDown = false;
context.removeRootEventTypes(rootEventTypes);
}
break;
}
case 'pointermove': {
state.pointerType = pointerType;
}
}
},
onUnmount(
context: ReactResponderContext,
props: ScrollProps,
state: ScrollState,
) {
// TODO
},
onOwnershipChange(
context: ReactResponderContext,
props: ScrollProps,
state: ScrollState,
) {
// TODO
},
};

export default React.unstable_createEventComponent(ScrollResponder, 'Scroll');
134 changes: 134 additions & 0 deletions packages/react-events/src/__tests__/Scroll-test.internal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* 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 Scroll;

const createEvent = (type, data) => {
const event = document.createEvent('CustomEvent');
event.initCustomEvent(type, true, true);
if (data != null) {
Object.entries(data).forEach(([key, value]) => {
event[key] = value;
});
}
return event;
};

describe('Scroll event responder', () => {
let container;

beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableEventAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Scroll = require('react-events/scroll');

container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
ReactDOM.render(null, container);
document.body.removeChild(container);
container = null;
});

describe('disabled', () => {
let onScroll, ref;

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

it('prevents custom events being dispatched', () => {
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).not.toBeCalled();
});
});

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

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

describe('is called after "scroll" event', () => {
it('with a mouse pointerType', () => {
ref.current.dispatchEvent(
createEvent('pointerdown', {
pointerType: 'mouse',
}),
);
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse', type: 'scroll'}),
);
});

it('with a touch pointerType', () => {
ref.current.dispatchEvent(
createEvent('pointerdown', {
pointerType: 'touch',
}),
);
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch', type: 'scroll'}),
);
});

it('with a pen pointerType', () => {
ref.current.dispatchEvent(
createEvent('pointerdown', {
pointerType: 'pen',
}),
);
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'pen', type: 'scroll'}),
);
});

it('with a keyboard pointerType', () => {
ref.current.dispatchEvent(createEvent('keydown'));
ref.current.dispatchEvent(createEvent('keyup'));
ref.current.dispatchEvent(createEvent('scroll'));
expect(onScroll).toHaveBeenCalledTimes(1);
expect(onScroll).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'keyboard', type: 'scroll'}),
);
});
});
});
});
Loading

0 comments on commit 73c27d8

Please sign in to comment.