Skip to content

Feat/calendar #2403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 111 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
146f1f4
Initial creation of new calendar component
ethanshar Dec 26, 2022
6eac3be
Push typings
ethanshar Dec 26, 2022
b2b7622
render initial components for example screen
ethanshar Dec 26, 2022
9c53baf
Add DateUtils with stubs
ethanshar Dec 26, 2022
e67dbf5
Add tests for DateUtils
ethanshar Dec 26, 2022
5ee5710
Add stub implementation to getDaysOfWeekNumber
ethanshar Dec 26, 2022
5cc1f05
Day - render day from date
Inbal-Tish Dec 26, 2022
a01b586
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Dec 26, 2022
f30c53b
Basic implementation of getWeekNumbersOfMonth
ethanshar Dec 26, 2022
959a0fe
Install date-fns
ethanshar Dec 26, 2022
550a03a
Basic implementation of Month
ethanshar Dec 26, 2022
9875e29
Add Week
M-i-k-e-l Dec 26, 2022
1b6cc4b
Fix Month component
ethanshar Dec 26, 2022
c3e8190
Change to millis
M-i-k-e-l Dec 26, 2022
7ecf4bf
Add key to week
ethanshar Dec 26, 2022
1b7dd9d
Fix hardcoded month value we pass to Month component
ethanshar Dec 26, 2022
d24f2a0
Add test for invalid month
ethanshar Dec 26, 2022
7108cdf
Render header in calendar item
ethanshar Dec 26, 2022
261c645
Header - render header
Inbal-Tish Dec 26, 2022
6ad966d
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Dec 26, 2022
062ba1f
fix CalendarItem render
Inbal-Tish Dec 26, 2022
e4072bc
Create getDaysOfWeekNumber and dependencies
M-i-k-e-l Jan 4, 2023
851a1f7
radability
ethanshar Jan 4, 2023
6714bc3
Add context provider and change firstDayOfWeek to enum
ethanshar Jan 4, 2023
16fc731
Add date and setDate to context
ethanshar Jan 4, 2023
3c956e6
Add getDateObject worklet
ethanshar Jan 4, 2023
286dd99
Fix calendar context types and add initialDate prop
ethanshar Jan 4, 2023
58b27f9
Basic Agenda
M-i-k-e-l Jan 4, 2023
609ddf0
Header - add months by presing arrow buttons
Inbal-Tish Jan 4, 2023
ff64571
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Jan 4, 2023
3b9702d
Add TODOs
ethanshar Jan 5, 2023
ac3e090
Move FirstDayOfWeek from enum to enum+union and fix tests
M-i-k-e-l Jan 5, 2023
e6b2508
MILLIS to MS
M-i-k-e-l Jan 5, 2023
83b0852
Add mock data (script still needs work) and link (types need work)
M-i-k-e-l Jan 5, 2023
4c870cc
Fix script and types
M-i-k-e-l Jan 6, 2023
3451f4c
addMonth - use Date's setMonth function
Inbal-Tish Jan 8, 2023
c9e6092
Moving 'firstDayOfWeek' to be enum
Inbal-Tish Jan 9, 2023
ac47a51
WeekDaysNames - adding new component to Header
Inbal-Tish Jan 9, 2023
19f9a81
add ts notes
Inbal-Tish Jan 9, 2023
e7f806b
update comment for ts version ^4.3.2
Inbal-Tish Jan 9, 2023
a121b48
Set WeekDaysNames format in Header
Inbal-Tish Jan 9, 2023
84331ac
use FirstDayOfWeek enum
Inbal-Tish Jan 9, 2023
cffcdb6
use 'DayNamesFormat' enum
Inbal-Tish Jan 9, 2023
bef8229
remove console
Inbal-Tish Jan 10, 2023
b76ebac
Day - adding onPress
Inbal-Tish Jan 10, 2023
a911820
add todo
Inbal-Tish Jan 10, 2023
1fe9ba4
Minor fixes and remove todos
ethanshar Jan 10, 2023
bec1f95
Change context's date to selectedDate
ethanshar Jan 10, 2023
b35f196
remove style file
Inbal-Tish Jan 10, 2023
fdb53d2
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Jan 10, 2023
798c7f8
valueOf to getTime
M-i-k-e-l Jan 10, 2023
294e33b
export Event
M-i-k-e-l Jan 10, 2023
2543a10
Day - adding selection
Inbal-Tish Jan 10, 2023
e4e073d
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Jan 10, 2023
1fe9f80
Wrap CalendarItem with FlashList
ethanshar Jan 10, 2023
08f4a04
Agenda - add headers + scroll on date change
M-i-k-e-l Jan 11, 2023
3d0361e
Agenda - set date on scroll
M-i-k-e-l Jan 11, 2023
428b4ca
Fix mock and script
M-i-k-e-l Jan 11, 2023
9d2261f
UI touchups
Inbal-Tish Jan 11, 2023
cc4c2fc
sort imports and add test for 'getMonthForIndex' and adding tests to …
Inbal-Tish Jan 11, 2023
51c5656
Header - small ui changes
Inbal-Tish Jan 12, 2023
1b309ce
Context - adding 'showWeeksNumbers'
Inbal-Tish Jan 12, 2023
aa28da2
fix indentation
Inbal-Tish Jan 12, 2023
c189293
margin
Inbal-Tish Jan 12, 2023
a502404
Add TODOs
M-i-k-e-l Jan 12, 2023
6ef0603
Add flex to day
ethanshar Jan 12, 2023
fa0d695
Day - mark today
Inbal-Tish Jan 17, 2023
eb607f1
Fix FirstDayOfWeek enum and add update sources
ethanshar Jan 17, 2023
f352a54
Adding UpdateSource to setDate calls
Inbal-Tish Jan 17, 2023
32723de
Implement setDate to store lastUpdateSource
ethanshar Jan 17, 2023
7c7508f
dateutils - adding 'isSameMonth'
Inbal-Tish Jan 17, 2023
ca4337d
Add estimatedItemSize prop to FlashLists
ethanshar Jan 17, 2023
20a7515
Update mock data
ethanshar Jan 17, 2023
44d8c29
Fix TS errors
ethanshar Jan 17, 2023
562371d
Context - add updateSource
Inbal-Tish Jan 17, 2023
9554352
fix screen errors
Inbal-Tish Jan 17, 2023
5552cf9
fix updateSource on Context
Inbal-Tish Jan 17, 2023
84ef5fb
fix type
Inbal-Tish Jan 17, 2023
400a014
Calendar - add scrollToIndex on selectedDate change
Inbal-Tish Jan 19, 2023
c9dd43e
Agenda - verify update source and use isSameDay
M-i-k-e-l Jan 25, 2023
1504dd2
Calendar - fix scrollToIndex
Inbal-Tish Jan 25, 2023
2d7e10a
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Jan 25, 2023
76880c4
Agenda initial index
M-i-k-e-l Jan 25, 2023
a509989
isSameMonth - support MonthProps type for params
Inbal-Tish Jan 25, 2023
c862a65
remove synchronous calls to isNumber from a worklet
Inbal-Tish Jan 25, 2023
0f61874
Calendar - add setDate on calendar scroll (by user) to the first of t…
Inbal-Tish Jan 25, 2023
ff81069
Fix getWeekNumbersOfMonth and add missing tests
ethanshar Jan 26, 2023
ea4d07c
Fix typing issues
ethanshar Jan 26, 2023
c9a30e8
fix scrolledByUser.value
Inbal-Tish Jan 26, 2023
c69e9a1
Merge branch 'feat/Calendar' of github.com:wix/react-native-ui-lib in…
Inbal-Tish Jan 26, 2023
cf1c1d8
Calendar - fix scrolledByUser
Inbal-Tish Jan 26, 2023
87e1997
merge fix
Inbal-Tish Jan 26, 2023
1e54efa
shush ts wrannings
Inbal-Tish Jan 26, 2023
3717e13
Add todos
ethanshar Jan 26, 2023
fdfadcc
DateUtils - add 'getNormalizedDate' function
Inbal-Tish Jan 29, 2023
4d33cb4
Change week numbering from ISO to default
M-i-k-e-l Jan 29, 2023
771980c
DateUtils - adding 'isToday' and 'isPastDate'
Inbal-Tish Jan 30, 2023
6c67153
adding assets
Inbal-Tish Jan 31, 2023
7ea4b0a
Agenda - fixed height for items and initialScrollIndex
M-i-k-e-l Jan 31, 2023
a451e3e
TodayButton
Inbal-Tish Jan 31, 2023
6378a60
Update calendars mock data
ethanshar Feb 2, 2023
f872273
Adding 'staticHeader' prop to calendar and passing it on context
Inbal-Tish Feb 2, 2023
f5afe2b
Merge branch 'master' into feat/Calendar
ethanshar Feb 3, 2023
235d608
Fix lint
ethanshar Feb 3, 2023
cd3d345
HeaderHeight to context
Inbal-Tish Feb 5, 2023
1997a69
Utils - use JS Date only inside utils (not as a parameter nor return …
Inbal-Tish Feb 5, 2023
aee98ea
Fix tests and remove getNormalizedDate
ethanshar Feb 7, 2023
927d111
fix lint
Inbal-Tish Feb 7, 2023
5bf37fe
Calendar - setDate when 'initialDate' prop changes
Inbal-Tish Feb 7, 2023
9af9a63
Header - fix arrows
Inbal-Tish Feb 7, 2023
7b17447
fix margins
Inbal-Tish Feb 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export const navigationData = {
Incubator: {
title: 'Incubator (Experimental)',
screens: [
{title: 'Calendar', tags: 'calendar', screen: 'unicorn.components.IncubatorCalendarScreen'},
{title: 'ChipsInput (New)', tags: 'chips input', screen: 'unicorn.components.IncubatorChipsInputScreen'},
{title: 'Native TouchableOpacity', tags: 'touchable native', screen: 'unicorn.incubator.TouchableOpacityScreen'},
{title: 'Dialog (New)', tags: 'dialog modal popup alert', screen: 'unicorn.incubator.IncubatorDialogScreen'},
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, {Component} from 'react';
import {View, Incubator} from 'react-native-ui-lib';
import {data} from './MockData';

export default class CalendarScreen extends Component {
// constructor(props) {
// super(props);

// setTimeout(() => {
// this.setState({date: 1676026748000});
// }, 2000);
// }

state = {
date: undefined
};

render() {
return (
<View flex>
<Incubator.Calendar data={data} staticHeader initialDate={this.state.date}>
<Incubator.Calendar.Agenda/>
</Incubator.Calendar>
</View>
);
}
}
1 change: 1 addition & 0 deletions demo/src/screens/incubatorScreens/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';

export function registerScreens(registrar) {
registrar('unicorn.components.IncubatorCalendarScreen', () => require('./IncubatorCalendarScreen').default);
registrar('unicorn.components.IncubatorChipsInputScreen', () => require('./IncubatorChipsInputScreen').default);
registrar('unicorn.incubator.TouchableOpacityScreen', () =>
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default));
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"pre-push": "npm run build:dev && npm run test",
"docs:deploy": "./scripts/deployDocs.sh",
"docs:build": "node scripts/buildDocs.js",
"calendar:createMocks": "node scripts/createCalendarMockData.js",
"snippets:build": "node scripts/generateSnippets.js",
"demo": "./scripts/demo.sh",
"release": "node ./scripts/release.js"
Expand All @@ -40,6 +41,7 @@
"babel-plugin-transform-inline-environment-variables": "^0.0.2",
"color": "^3.1.0",
"commons-validator-js": "^1.0.237",
"date-fns": "^2.29.3",
"deprecated-react-native-prop-types": "^2.3.0",
"hoist-non-react-statics": "^3.0.0",
"lodash": "^4.17.21",
Expand Down
37 changes: 37 additions & 0 deletions scripts/createCalendarMockData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const fs = require('fs');
const HOUR_IN_MS = 60 * 60 * 1000;
const ID_LENGTH = 10;

function generateId() {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < ID_LENGTH; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

const data = [];

for (let year = 2021; year <= 2023; ++year) {
for (let month = 0; month <= 11; ++month) {
for (let day = 1; day <= 31; day += Math.random() > 0.5 ? 2 : 1) {
for (let hour = 9; hour <= 19; hour += Math.random() > 0.5 ? 4 : 3) {
const startDate = new Date(year, month, day, hour, 0);
if (startDate.getDay() >= 2 && startDate.getDay() <= 5) {
const start = startDate.getTime();
const end = start + HOUR_IN_MS * (Math.random() > 0.5 ? 0.5 : 1);
const id = generateId();
data.push({id, start, end});
}
}
}
}
}

console.log(`${data.length} events were created`);

fs.writeFileSync('demo/src/screens/incubatorScreens/IncubatorCalendarScreen/MockData.ts',
`// Note: to generate new data run calendar:createMocks and update createCalendarMockData script \n` +
`export const data = ${JSON.stringify(data)};`);
158 changes: 158 additions & 0 deletions src/incubator/Calendar/Agenda.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, {useContext, useCallback, useRef} from 'react';
import {runOnJS, useAnimatedReaction, useSharedValue} from 'react-native-reanimated';
import {FlashList, ViewToken} from '@shopify/flash-list';
import {BorderRadiuses} from 'style';
import View from '../../components/view';
import Text from '../../components/text';
import {isSameDay, isSameMonth} from './helpers/DateUtils';
import {InternalEvent, Event, DateSectionHeader, UpdateSource} from './types';
import CalendarContext from './CalendarContext';

// TODO: Fix initial scrolling
function Agenda() {
const {data, selectedDate, setDate, updateSource} = useContext(CalendarContext);
const flashList = useRef<FlashList<InternalEvent>>(null);
const closestSectionHeader = useSharedValue<DateSectionHeader | null>(null);
const scrolledByUser = useSharedValue<boolean>(false);

const keyExtractor = useCallback((item: InternalEvent) => {
return item.type === 'Event' ? item.id : item.header;
}, []);

const renderEvent = useCallback((item: Event) => {
return (
<View
marginV-1
marginH-10
paddingH-10
height={50}
style={{borderWidth: 1, borderRadius: BorderRadiuses.br20, justifyContent: 'center'}}
>
<Text style={{}}>
Item for{' '}
{new Date(item.start).toLocaleString('en-GB', {
month: 'short',
day: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit'
})}
-{new Date(item.end).toLocaleString('en-GB', {hour12: false, hour: '2-digit', minute: '2-digit'})}
</Text>
</View>
);
}, []);

const renderHeader = useCallback((item: DateSectionHeader) => {
return (
<View
marginB-1
paddingB-4
marginH-10
paddingH-10
height={50}
bottom
>
<Text>{item.header}</Text>
</View>
);
}, []);

const renderItem = useCallback(({item}: {item: InternalEvent; index: number}) => {
switch (item.type) {
case 'Event':
return renderEvent(item);
case 'Header':
return renderHeader(item);
}
},
[renderEvent, renderHeader]);

const getItemType = useCallback(item => item.type, []);

const findClosestDateAfter = useCallback((selected: number) => {
'worklet';
for (let index = 0; index < data.length; ++index) {
const item = data[index];
if (item.type === 'Header') {
if (item.date >= selected) {
return {dateSectionHeader: item, index};
}
}
}

return null;
},
[data]);

const scrollToIndex = useCallback((index: number, animated: boolean) => {
flashList.current?.scrollToIndex({index, animated});
}, []);

useAnimatedReaction(() => {
return selectedDate.value;
},
(selected, previous) => {
if (updateSource?.value !== UpdateSource.AGENDA_SCROLL) {
if (
selected !== previous &&
(closestSectionHeader.value?.date === undefined || !isSameDay(selected, closestSectionHeader.value?.date))
) {
const result = findClosestDateAfter(selected);
if (result !== null) {
const {dateSectionHeader, index} = result;
closestSectionHeader.value = dateSectionHeader;
scrolledByUser.value = false;
// TODO: Can the animation be improved (not in JS)?
if (previous) {
const _isSameMonth = isSameMonth(selected, previous);
runOnJS(scrollToIndex)(index, _isSameMonth);
}
}
}
}
},
[findClosestDateAfter]);

// TODO: look at https://docs.swmansion.com/react-native-reanimated/docs/api/hooks/useAnimatedScrollHandler
const onViewableItemsChanged = useCallback(({viewableItems}: {viewableItems: ViewToken[]}) => {
if (scrolledByUser.value) {
const result = viewableItems.find(item => item.item.type === 'Header');
if (result) {
const {item}: {item: DateSectionHeader} = result;
if (closestSectionHeader.value?.date !== item.date) {
closestSectionHeader.value = item;
setDate(item.date, UpdateSource.AGENDA_SCROLL);
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onMomentumScrollBegin = useCallback(() => {
scrolledByUser.value = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const onScrollBeginDrag = useCallback(() => {
scrolledByUser.value = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<FlashList
ref={flashList}
estimatedItemSize={52}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
getItemType={getItemType}
onViewableItemsChanged={onViewableItemsChanged}
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollBeginDrag={onScrollBeginDrag}
initialScrollIndex={findClosestDateAfter(selectedDate.value)?.index ?? 0}
/>
);
}

export default Agenda;
9 changes: 9 additions & 0 deletions src/incubator/Calendar/CalendarContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {createContext} from 'react';
import {CalendarContextProps, FirstDayOfWeek} from './types';

// @ts-ignore
const CalendarContext = createContext<CalendarContextProps>({
firstDayOfWeek: FirstDayOfWeek.MONDAY
});

export default CalendarContext;
44 changes: 44 additions & 0 deletions src/incubator/Calendar/CalendarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {useContext, useMemo} from 'react';
import {StyleSheet} from 'react-native';
import {Constants} from '../../commons/new';
import View from '../../components/view';
import {CalendarItemProps} from './types';
import CalendarContext from './CalendarContext';
import Month from './Month';
import Header from './Header';

const CALENDAR_HEIGHT = 250;

function CalendarItem(props: CalendarItemProps) {
const {year, month} = props;
const {staticHeader, headerHeight} = useContext(CalendarContext);

const calendarStyle = useMemo(() => {
// TODO: dynamic height: calc calendar height with month's number of weeks
return [
styles.container,
{
height: CALENDAR_HEIGHT - (staticHeader ? headerHeight.value : 0)
}
];
}, [staticHeader]);

if (month !== undefined) {
return (
<View style={calendarStyle}>
{!staticHeader && <Header month={month} year={year}/>}
<Month month={month} year={year}/>
</View>
);
}
return null;
}

export default CalendarItem;

const styles = StyleSheet.create({
container: {
width: Constants.screenWidth,
borderBottomWidth: 1
}
});
83 changes: 83 additions & 0 deletions src/incubator/Calendar/Day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import isNull from 'lodash/isNull';
import React, {useContext, useCallback} from 'react';
import {StyleSheet} from 'react-native';
import Reanimated, {useSharedValue, useAnimatedStyle, useAnimatedReaction} from 'react-native-reanimated';
import {Colors} from 'style';
import View from '../../components/view';
import TouchableOpacity from '../../components/touchableOpacity';
import Text from '../../components/text';
import {getDayOfDate, isSameDay, isToday} from './helpers/DateUtils';
import {DayProps, UpdateSource} from './types';
import CalendarContext from './CalendarContext';


const AnimatedText = Reanimated.createAnimatedComponent(Text);

const Day = (props: DayProps) => {
const {date, onPress} = props;
const {selectedDate, setDate} = useContext(CalendarContext);

const shouldMarkSelected = !isNull(date) ? isSameDay(selectedDate.value, date) : false;
const isSelected = useSharedValue(shouldMarkSelected);

const backgroundColor = isToday(date) ? Colors.$backgroundSuccessHeavy : Colors.transparent;
const textColor = isToday(date) ? Colors.$textDefaultLight : Colors.$backgroundPrimaryHeavy;

const animatedStyles = useAnimatedStyle(() => {
return {
backgroundColor: isSelected.value ? Colors.$backgroundPrimaryHeavy : backgroundColor,
color: isSelected.value ? Colors.$textDefaultLight : textColor
};
});

const animatedTextStyles = useAnimatedStyle(() => {
return {
color: isSelected.value ? Colors.$textDefaultLight : textColor
};
});

useAnimatedReaction(() => {
return selectedDate.value;
}, (selected) => {
isSelected.value = isSameDay(selected, date!);
}, []);

const _onPress = useCallback(() => {
if (date !== null) {
isSelected.value = true;
setDate(date, UpdateSource.DAY_SELECT);
onPress?.(date);
}
}, [date, setDate, onPress]);

const renderDay = () => {
const day = !isNull(date) ? getDayOfDate(date) : '';
return (
<View center>
<View reanimated style={[styles.selection, animatedStyles]}/>
<AnimatedText style={animatedTextStyles}>{day}</AnimatedText>
</View>
);
};

return (
<TouchableOpacity flex center style={styles.dayContainer} onPress={_onPress} activeOpacity={1}>
{renderDay()}
</TouchableOpacity>
);
};

export default Day;

const styles = StyleSheet.create({
dayContainer: {
width: 32,
height: 32
},
selection: {
position: 'absolute',
width: 24,
height: 24,
borderRadius: 12
}
});
Loading