diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6aaa510..8eb7bf94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres (more or less) to [Semantic Versioning](http://semver.o * Add unit argument to onZoom and onTimeChange callbacks * Add `className` prop to Timeline component to override `react-calendar-timeline` class #682 * Fix injecting custom vertical line's class names for time periods longer than day +* Add onEndTimeChange event which is called once when scrolling is finished ## 0.26.7 diff --git a/README.md b/README.md index 901939b7a..903ac61a9 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,10 @@ function (visibleTimeStart, visibleTimeEnd, updateScrollCanvas) { } ``` +## onEndTimeChange(visibleTimeStart, visibleTimeEnd) + +A function that's called when the user has finished scrolling. + ## onBoundsChange(canvasTimeStart, canvasTimeEnd) Called when the bounds in the calendar's canvas change. Use it for example to load new data to display. (see "Behind the scenes" below). `canvasTimeStart` and `canvasTimeEnd` are unix timestamps in milliseconds. diff --git a/__tests__/components/ScrollElement/ScrollElement.test.js b/__tests__/components/ScrollElement/ScrollElement.test.js index 0ba786d82..b50c293cd 100644 --- a/__tests__/components/ScrollElement/ScrollElement.test.js +++ b/__tests__/components/ScrollElement/ScrollElement.test.js @@ -9,6 +9,7 @@ const defaultProps = { onZoom: noop, onWheelZoom: noop, onScroll: noop, + onEndScroll: noop, traditionalZoom: false, scrollRef: noop, isInteractingWithItem: false, @@ -26,8 +27,8 @@ const createMouseEvent = pageX => ({ const scrollElementSelector = sel('scroll-element') -xdescribe('ScrollElement', () => { - describe('mouse event delegates', () => { +describe('ScrollElement', () => { + describe.skip('mouse event delegates', () => { let onDoubleClickMock, onMouseLeaveMock, onMouseMoveMock, @@ -75,7 +76,7 @@ xdescribe('ScrollElement', () => { expect(onContextMenuMock).toHaveBeenCalledTimes(1) }) }) - describe('mouse drag', () => { + describe.skip('mouse drag', () => { let wrapper beforeEach(() => { @@ -175,7 +176,7 @@ xdescribe('ScrollElement', () => { expect(onScrollMock).toHaveBeenCalledTimes(1) }) - it('adds width to scrollLeft if scrollLeft is less than half of width', () => { + it.skip('adds width to scrollLeft if scrollLeft is less than half of width', () => { const width = 800 const props = { ...defaultProps, @@ -197,7 +198,7 @@ xdescribe('ScrollElement', () => { currentScrollLeft + width ) }) - it('subtracts width from scrollLeft if scrollLeft is greater than one and a half of width', () => { + it.skip('subtracts width from scrollLeft if scrollLeft is greater than one and a half of width', () => { const width = 800 const props = { ...defaultProps, @@ -244,5 +245,25 @@ xdescribe('ScrollElement', () => { expect(wrapper.instance().scrollComponent.scrollLeft).toBe(scroll) }) }) + + it('calls onEndScroll on mouse up with current scrollLeft', () => { + const onEndScrollMock = jest.fn() + const props = { + ...defaultProps, + onEndScroll: onEndScrollMock + } + + const wrapper = mount( + +
+ + ) + const scrollLeft = 200 + wrapper.instance().scrollComponent.scrollLeft = scrollLeft + + wrapper.find(scrollElementSelector).simulate('mouseUp') + + expect(onEndScrollMock).toHaveBeenCalledWith(scrollLeft) + }) }) }) diff --git a/__tests__/components/Timeline/Timeline.test.js b/__tests__/components/Timeline/Timeline.test.js index 820e06f42..b130df546 100644 --- a/__tests__/components/Timeline/Timeline.test.js +++ b/__tests__/components/Timeline/Timeline.test.js @@ -2,7 +2,7 @@ import React from 'react' import moment from 'moment' import { mount } from 'enzyme' import Timeline from 'lib/Timeline' -import { noop } from 'test-utility' +import { noop, sel } from 'test-utility' const defaultProps = { ...Timeline.defaultProps, @@ -10,8 +10,8 @@ const defaultProps = { groups: [] } -xdescribe('Timeline', () => { - describe('initialiation', () => { +describe('Timeline', () => { + describe('initialization', () => { it('sets the visibleTime properties to defaultTime props', () => { const defaultTimeStart = moment('2018-01-01') const defaultTimeEnd = moment('2018-03-01') @@ -61,4 +61,47 @@ xdescribe('Timeline', () => { jest.restoreAllMocks() }) }) + + describe('scrolling', () => { + const visibleTimeStart = moment('2018-01-01').valueOf() + const visibleTimeEnd = moment('2018-03-01').valueOf() + const mockOnEndTimeChange = jest.fn(); + + afterEach(() => { + mockOnEndTimeChange.mockReset() + }) + + it('calls onEndScroll', () => { + const props = { + ...defaultProps, + visibleTimeStart, + visibleTimeEnd, + onEndTimeChange: mockOnEndTimeChange + } + const wrapper = mount() + + const scrollElement = wrapper.find(sel('scroll-element')) + wrapper.instance().scrollComponent.scrollLeft = 200 + scrollElement.simulate('scroll'); + scrollElement.simulate('mouseUp'); + + expect(mockOnEndTimeChange).toHaveBeenCalledTimes(1); + }) + + it('does not call onEndScroll if not scrolled', () => { + const props = { + ...defaultProps, + visibleTimeStart, + visibleTimeEnd, + onEndTimeChange: mockOnEndTimeChange + } + const wrapper = mount() + + const scrollElement = wrapper.find(sel('scroll-element')) + scrollElement.simulate('scroll'); + scrollElement.simulate('mouseUp'); + + expect(mockOnEndTimeChange).not.toHaveBeenCalled(); + }) + }) }) diff --git a/demo/app/demo-main/index.js b/demo/app/demo-main/index.js index 66deb97d2..a2def441f 100644 --- a/demo/app/demo-main/index.js +++ b/demo/app/demo-main/index.js @@ -135,6 +135,10 @@ export default class App extends Component { } } + handleEndTimeChange = (visibleTimeStart, visibleTimeEnd) => { + console.log("EndTimeChange", visibleTimeStart, visibleTimeEnd); + } + handleZoom = (timelineContext, unit) => { console.log('Zoomed', timelineContext, unit) } @@ -178,6 +182,7 @@ export default class App extends Component { onItemResize={this.handleItemResize} onItemDoubleClick={this.handleItemDoubleClick} onTimeChange={this.handleTimeChange} + onEndTimeChange={this.handleEndTimeChange} onZoom={this.handleZoom} moveResizeValidator={this.moveResizeValidator} > diff --git a/src/lib/Timeline.js b/src/lib/Timeline.js index 1355f110a..7a3ea0953 100644 --- a/src/lib/Timeline.js +++ b/src/lib/Timeline.js @@ -115,6 +115,7 @@ export default class ReactCalendarTimeline extends Component { visibleTimeStart: PropTypes.number, visibleTimeEnd: PropTypes.number, onTimeChange: PropTypes.func, + onEndTimeChange: PropTypes.func, onBoundsChange: PropTypes.func, selected: PropTypes.array, @@ -226,6 +227,7 @@ export default class ReactCalendarTimeline extends Component { ) { updateScrollCanvas(visibleTimeStart, visibleTimeEnd) }, + onEndTimeChange: null, // called when the canvas area of the calendar changes onBoundsChange: null, children: null, @@ -284,6 +286,8 @@ export default class ReactCalendarTimeline extends Component { constructor(props) { super(props) + this.scrolled = false + this.getSelected = this.getSelected.bind(this) this.hasSelectedItem = this.hasSelectedItem.bind(this) this.isItemSelected= this.isItemSelected.bind(this) @@ -511,27 +515,45 @@ export default class ReactCalendarTimeline extends Component { } onScroll = scrollX => { - const width = this.state.width - - const canvasTimeStart = this.state.canvasTimeStart - - const zoom = this.state.visibleTimeEnd - this.state.visibleTimeStart - - const visibleTimeStart = canvasTimeStart + zoom * scrollX / width + const [visibleTimeStart, visibleTimeEnd] = this.getVisibleTimeRange(scrollX) if ( this.state.visibleTimeStart !== visibleTimeStart || - this.state.visibleTimeEnd !== visibleTimeStart + zoom + this.state.visibleTimeEnd !== visibleTimeEnd ) { + this.scrolled = true + this.props.onTimeChange( visibleTimeStart, - visibleTimeStart + zoom, + visibleTimeEnd, this.updateScrollCanvas, this.getTimelineUnit() ) } } + onEndScroll = (scrollX) => { + if (this.scrolled) { + this.scrolled = false + + if (this.props.onEndTimeChange) { + this.props.onEndTimeChange(...this.getVisibleTimeRange(scrollX)) + } + } + } + + getVisibleTimeRange = (scrollX) => { + const width = this.state.width + + const canvasTimeStart = this.state.canvasTimeStart + + const zoom = this.state.visibleTimeEnd - this.state.visibleTimeStart + + const visibleTimeStart = canvasTimeStart + zoom * scrollX / width + + return [visibleTimeStart, visibleTimeStart + zoom] + } + // called when the visible time changes updateScrollCanvas = ( visibleTimeStart, @@ -1073,6 +1095,7 @@ export default class ReactCalendarTimeline extends Component { onWheelZoom={this.handleWheelZoom} traditionalZoom={traditionalZoom} onScroll={this.onScroll} + onEndScroll={this.onEndScroll} isInteractingWithItem={isInteractingWithItem} > diff --git a/src/lib/scroll/ScrollElement.js b/src/lib/scroll/ScrollElement.js index 5f79ae6cd..a36cecce4 100644 --- a/src/lib/scroll/ScrollElement.js +++ b/src/lib/scroll/ScrollElement.js @@ -12,7 +12,8 @@ class ScrollElement extends Component { isInteractingWithItem: PropTypes.bool.isRequired, onZoom: PropTypes.func.isRequired, onWheelZoom: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + onScroll: PropTypes.func.isRequired, + onEndScroll : PropTypes.func.isRequired } constructor() { @@ -37,12 +38,12 @@ class ScrollElement extends Component { el.addEventListener('wheel', this.handleWheel, {passive: false}); } } - + handleWheel = e => { const { traditionalZoom } = this.props - + // zoom in the time dimension if (e.ctrlKey || e.metaKey || e.altKey) { @@ -82,6 +83,8 @@ class ScrollElement extends Component { } handleMouseUp = () => { + this.props.onEndScroll(this.scrollComponent.scrollLeft) + this.dragStartPosition = null this.dragLastPosition = null @@ -161,6 +164,7 @@ class ScrollElement extends Component { handleTouchEnd = () => { if (this.lastTouchDistance) { this.lastTouchDistance = null + this.props.onEndScroll(this.scrollComponent.scrollLeft) } if (this.lastSingleTouch) { this.lastSingleTouch = null