Skip to content

Commit 7b8c924

Browse files
authored
Merge pull request #1731 from wix/feat/TimelineEventCreation
Support timeline background press for creating new events
2 parents 3117b1a + 65faa51 commit 7b8c924

File tree

6 files changed

+213
-15
lines changed

6 files changed

+213
-15
lines changed

example/src/screens/timelineCalendar.tsx

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import XDate from 'xdate';
22
import React, {Component} from 'react';
3-
// @ts-expect-error
4-
import {ExpandableCalendar, Timeline, CalendarProvider} from 'react-native-calendars';
3+
import {Alert} from 'react-native';
4+
import {ExpandableCalendar, Timeline, CalendarProvider, TimelineProps} from 'react-native-calendars';
55
import {sameDate} from '../../../src/dateutils';
66

7-
87
const EVENTS = [
98
{
109
start: '2017-09-06 01:30:00',
@@ -81,7 +80,9 @@ const EVENTS = [
8180

8281
export default class TimelineCalendarScreen extends Component {
8382
state = {
84-
currentDate: '2017-09-10'
83+
currentDate: '2017-09-10',
84+
events: EVENTS,
85+
newEvent: undefined
8586
};
8687

8788
marked = {
@@ -91,7 +92,7 @@ export default class TimelineCalendarScreen extends Component {
9192
'2017-09-10': {marked: true}
9293
};
9394

94-
onDateChanged = date => {
95+
onDateChanged = (date: string) => {
9596
// console.warn('TimelineCalendarScreen onDateChanged: ', date, updateSource);
9697
// fetch and set data for date + week ahead
9798
this.setState({currentDate: date});
@@ -101,7 +102,47 @@ export default class TimelineCalendarScreen extends Component {
101102
// console.warn('TimelineCalendarScreen onMonthChange: ', month, updateSource);
102103
};
103104

105+
createNewEvent: TimelineProps['onBackgroundLongPress'] = (timeString, timeObject) => {
106+
const {currentDate} = this.state;
107+
const hourString = `${(timeObject.hour + 1).toString().padStart(2, '0')}`;
108+
const minutesString = `${timeObject.minutes.toString().padStart(2, '0')}`;
109+
110+
const newEvent = {
111+
start: `${currentDate} ${timeString}`,
112+
end: `${currentDate} ${hourString}:${minutesString}:00`,
113+
title: 'New Event',
114+
color: '#ffffff'
115+
};
116+
117+
this.setState({newEvent});
118+
};
119+
120+
approveNewEvent = () => {
121+
Alert.prompt('New Event', 'Enter event title', [
122+
{
123+
text: 'Cancel',
124+
onPress: () => {
125+
this.setState({
126+
newEvent: undefined
127+
});
128+
}
129+
},
130+
{
131+
text: 'Create',
132+
onPress: eventTitle => {
133+
const {newEvent = {}, events} = this.state;
134+
this.setState({
135+
newEvent: undefined,
136+
events: [...events, {...newEvent, title: eventTitle ?? 'New Event', color: '#d8ade6'}]
137+
});
138+
}
139+
}
140+
]);
141+
};
142+
104143
render() {
144+
const {events, newEvent} = this.state;
145+
const timelineEvents = newEvent ? [...events, newEvent] : events;
105146
return (
106147
<CalendarProvider
107148
date={this.state.currentDate}
@@ -119,8 +160,10 @@ export default class TimelineCalendarScreen extends Component {
119160
<Timeline
120161
format24h={true}
121162
eventTapped={e => e}
122-
events={EVENTS.filter(event => sameDate(new XDate(event.start), new XDate(this.state.currentDate)))}
163+
events={timelineEvents.filter(event => sameDate(new XDate(event.start), new XDate(this.state.currentDate)))}
123164
scrollToFirst
165+
onBackgroundLongPress={this.createNewEvent}
166+
onBackgroundLongPressOut={this.approveNewEvent}
124167
// start={0}
125168
// end={24}
126169
/>

src/timeline/Timeline.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,49 @@ import map from 'lodash/map';
66
import {Theme} from '../types';
77
import styleConstructor from './style';
88
import populateEvents, {HOUR_BLOCK_HEIGHT} from './Packer';
9-
import TimelineHours from './TimelineHours';
9+
import TimelineHours, {TimelineHoursProps} from './TimelineHours';
1010
import EventBlock, {Event, PackedEvent} from './EventBlock';
1111

1212
const LEFT_MARGIN = 60 - 1;
1313
const {width: dimensionWidth} = Dimensions.get('window');
1414

1515
export interface TimelineProps {
1616
events: Event[];
17+
/**
18+
* The timeline day start time
19+
*/
1720
start?: number;
21+
/**
22+
* The timeline day end time
23+
*/
1824
end?: number;
19-
eventTapped?: (event: Event) => void; //TODO: deprecate (prop renamed 'onEventPress', as in the other components).
25+
/**
26+
* @deprecated
27+
* Use onEventPress instead
28+
*/
29+
eventTapped?: (event: Event) => void;
30+
/**
31+
* Handle event press
32+
*/
2033
onEventPress?: (event: Event) => void;
34+
/**
35+
* Pass to handle creation of a new event by long press on the timeline background
36+
*/
37+
onBackgroundLongPress?: TimelineHoursProps['onBackgroundLongPress'];
38+
/**
39+
* Pass to handle creation of a new event by long press out on the timeline background
40+
*/
41+
onBackgroundLongPressOut?: TimelineHoursProps['onBackgroundLongPressOut'];
2142
styles?: Theme; //TODO: deprecate (prop renamed 'theme', as in the other components).
2243
theme?: Theme;
2344
scrollToFirst?: boolean;
45+
/**
46+
* Whether to use 24 hours format for the timeline hours
47+
*/
2448
format24h?: boolean;
49+
/**
50+
* Render a custom event block
51+
*/
2552
renderEvent?: (event: PackedEvent) => JSX.Element;
2653
}
2754

@@ -32,6 +59,8 @@ const Timeline = (props: TimelineProps) => {
3259
end = 24,
3360
events = [],
3461
onEventPress,
62+
onBackgroundLongPress,
63+
onBackgroundLongPressOut,
3564
renderEvent,
3665
theme,
3766
scrollToFirst,
@@ -102,7 +131,14 @@ const Timeline = (props: TimelineProps) => {
102131
return (
103132
// @ts-expect-error
104133
<ScrollView ref={scrollView} contentContainerStyle={[styles.current.contentStyle, {width: dimensionWidth}]}>
105-
<TimelineHours start={start} end={end} format24h={format24h} styles={styles.current} />
134+
<TimelineHours
135+
start={start}
136+
end={end}
137+
format24h={format24h}
138+
styles={styles.current}
139+
onBackgroundLongPress={onBackgroundLongPress}
140+
onBackgroundLongPressOut={onBackgroundLongPressOut}
141+
/>
106142
{renderEvents()}
107143
</ScrollView>
108144
);

src/timeline/TimelineHours.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1-
import React, {useMemo} from 'react';
2-
import {View, Text, ViewStyle, TextStyle, Dimensions} from 'react-native';
1+
import React, {useCallback, useMemo, useRef} from 'react';
2+
import {View, Text, TouchableWithoutFeedback, ViewStyle, TextStyle, Dimensions, StyleSheet} from 'react-native';
33
import range from 'lodash/range';
44
import {HOUR_BLOCK_HEIGHT} from './Packer';
5+
import {buildTimeString, calcTimeByPosition} from './helpers/presenter';
56

67
const {width: dimensionWidth} = Dimensions.get('window');
78

8-
interface TimelineHoursProps {
9+
interface NewEventTime {
10+
hour: number;
11+
minutes: number;
12+
}
13+
14+
export interface TimelineHoursProps {
915
start?: number;
1016
end?: number;
1117
format24h?: boolean;
18+
onBackgroundLongPress?: (timeString: string, time: {hour: number; minutes: number}) => void;
19+
onBackgroundLongPressOut?: (timeString: string, time: {hour: number; minutes: number}) => void;
1220
styles: {[key: string]: ViewStyle | TextStyle};
1321
}
1422

1523
const TimelineHours = (props: TimelineHoursProps) => {
16-
const {format24h, start = 0, end = 24, styles} = props;
24+
const {format24h, start = 0, end = 24, styles, onBackgroundLongPress, onBackgroundLongPressOut} = props;
25+
26+
const lastLongPressEventTime = useRef<NewEventTime>();
1727
// const offset = this.calendarHeight / (end - start);
1828
const offset = HOUR_BLOCK_HEIGHT;
1929
const EVENT_DIFF = 20;
@@ -37,8 +47,33 @@ const TimelineHours = (props: TimelineHoursProps) => {
3747
});
3848
}, [start, end, format24h]);
3949

50+
const handleBackgroundPress = useCallback(
51+
event => {
52+
const yPosition = event.nativeEvent.locationY;
53+
const {hour, minutes} = calcTimeByPosition(yPosition, HOUR_BLOCK_HEIGHT);
54+
55+
lastLongPressEventTime.current = {hour, minutes};
56+
57+
const timeString = buildTimeString(hour, minutes);
58+
onBackgroundLongPress?.(timeString, lastLongPressEventTime.current);
59+
},
60+
[onBackgroundLongPress]
61+
);
62+
63+
const handlePressOut = useCallback(() => {
64+
if (lastLongPressEventTime.current) {
65+
const {hour, minutes} = lastLongPressEventTime.current;
66+
const timeString = buildTimeString(hour, minutes);
67+
onBackgroundLongPressOut?.(timeString, lastLongPressEventTime.current);
68+
lastLongPressEventTime.current = undefined;
69+
}
70+
}, [onBackgroundLongPressOut]);
71+
4072
return (
4173
<>
74+
<TouchableWithoutFeedback onLongPress={handleBackgroundPress} onPressOut={handlePressOut}>
75+
<View style={StyleSheet.absoluteFillObject} />
76+
</TouchableWithoutFeedback>
4277
{hours.map(({timeText, time}, index) => {
4378
return (
4479
<React.Fragment key={time}>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as uut from '../helpers/presenter';
2+
3+
describe('timeline presenter', () => {
4+
describe('calcTimeByPosition', () => {
5+
it('should return hour/minutes by position - basic case 1', () => {
6+
const {hour, minutes} = uut.calcTimeByPosition(300, 100);
7+
expect(hour).toBe(3);
8+
expect(minutes).toBe(0);
9+
});
10+
11+
it('should return hour/minutes by position - basic case 2', () => {
12+
const {hour, minutes} = uut.calcTimeByPosition(350, 100);
13+
expect(hour).toBe(3);
14+
expect(minutes).toBe(30);
15+
});
16+
17+
it('should round down time to nearest 30 minutes block', () => {
18+
const time1 = uut.calcTimeByPosition(310, 100);
19+
expect(time1.hour).toBe(3);
20+
expect(time1.minutes).toBe(0);
21+
22+
const time2 = uut.calcTimeByPosition(280, 100);
23+
expect(time2.hour).toBe(2);
24+
expect(time2.minutes).toBe(30);
25+
26+
const time3 = uut.calcTimeByPosition(440, 100);
27+
expect(time3.hour).toBe(4);
28+
expect(time3.minutes).toBe(0);
29+
30+
const time4 = uut.calcTimeByPosition(1488, 100);
31+
expect(time4.hour).toBe(14);
32+
expect(time4.minutes).toBe(30);
33+
});
34+
35+
it('should handle different hour block heights', () => {
36+
const time1 = uut.calcTimeByPosition(350, 85);
37+
expect(time1.hour).toBe(4);
38+
expect(time1.minutes).toBe(0);
39+
40+
const time2 = uut.calcTimeByPosition(350, 145);
41+
expect(time2.hour).toBe(2);
42+
expect(time2.minutes).toBe(0);
43+
44+
const time3 = uut.calcTimeByPosition(350, 135);
45+
expect(time3.hour).toBe(2);
46+
expect(time3.minutes).toBe(30);
47+
});
48+
});
49+
50+
describe('buildTimeString', () => {
51+
it('should construct time string based on given hour/mins', () => {
52+
expect(uut.buildTimeString(3, 12)).toBe('03:12:00');
53+
expect(uut.buildTimeString(23, 55)).toBe('23:55:00');
54+
});
55+
56+
it('should padding hour/minutes with zeroes', () => {
57+
expect(uut.buildTimeString(3, 0)).toBe('03:00:00');
58+
expect(uut.buildTimeString(9, 5)).toBe('09:05:00');
59+
});
60+
61+
it('should default undefined hour/min to zeroes', () => {
62+
expect(uut.buildTimeString(undefined, 30)).toBe('00:30:00');
63+
expect(uut.buildTimeString(9, undefined)).toBe('09:00:00');
64+
});
65+
});
66+
});

src/timeline/helpers/presenter.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function calcTimeByPosition(yPosition: number, hourBlockHeight: number) {
2+
let time = yPosition / hourBlockHeight;
3+
time = Math.floor(time * 2) / 2;
4+
5+
const hour = Math.floor(time);
6+
const minutes = (time - Math.floor(time)) * 60;
7+
return {hour, minutes};
8+
}
9+
10+
export function buildTimeString(hour = 0, minutes = 0) {
11+
return `${hour.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
12+
}

tsconfig.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919
"declaration": true,
2020
"baseUrl": ".",
2121
"paths": {
22-
"react-native-calendars": ["src/index.ts"],
22+
"react-native-calendars": ["src/index.ts"]
2323
}
2424
},
2525
// "include": ["src/calendar/day/basic", "src/global.d.ts"],
26-
"include": ["src/**/*", "example/src/screens/calendars.tsx", "example/src/screens/expandableCalendar.tsx", "example/src/screens/agenda.tsx"],
26+
"include": [
27+
"src/**/*",
28+
"example/src/screens/calendars.tsx",
29+
"example/src/screens/expandableCalendar.tsx",
30+
"example/src/screens/agenda.tsx",
31+
"example/src/screens/timelineCalendar.tsx"
32+
],
2733
"exclude": ["node_modules", "testUtils"]
2834
}

0 commit comments

Comments
 (0)