diff --git a/packages/react-events/src/dom/Scroll.js b/packages/react-events/src/dom/Scroll.js index c17275c1366b..4253a39d3a5d 100644 --- a/packages/react-events/src/dom/Scroll.js +++ b/packages/react-events/src/dom/Scroll.js @@ -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 = @@ -58,8 +60,14 @@ 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, @@ -67,6 +75,7 @@ function createScrollEvent( type: ScrollEventType, target: Element | Document, pointerType: PointerType, + direction: ScrollDirection, ): ScrollEvent { let clientX = null; let clientY = null; @@ -84,7 +93,7 @@ function createScrollEvent( target, type, pointerType, - direction: '', // TODO + direction, timeStamp: context.getTimeStamp(), clientX, clientY, @@ -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); } @@ -122,9 +133,12 @@ const ScrollResponder: ReactDOMEventResponder = { targetEventTypes, createInitialState() { return { + direction: '', + isTouching: false, pointerType: '', + prevScrollTop: 0, + prevScrollLeft: 0, scrollTarget: null, - isPointerDown: false, }; }, allowMultipleHostChildren: true, @@ -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, @@ -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; } } }, @@ -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; } } }, diff --git a/packages/react-events/src/dom/__tests__/Scroll-test.internal.js b/packages/react-events/src/dom/__tests__/Scroll-test.internal.js index 88244e9654a4..d4ce6ef415bc 100644 --- a/packages/react-events/src/dom/__tests__/Scroll-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Scroll-test.internal.js @@ -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', + }), ); }); @@ -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', + }), ); }); @@ -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', + }), ); }); @@ -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 = ( + +
+ + ); + 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 = ( + +
+ + ); + 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: '', + }), + ); + }); + }); });