diff --git a/example/src/screens/expandableCalendar.js b/example/src/screens/expandableCalendar.tsx similarity index 96% rename from example/src/screens/expandableCalendar.js rename to example/src/screens/expandableCalendar.tsx index 485f334f32..c2e95b2fa3 100644 --- a/example/src/screens/expandableCalendar.js +++ b/example/src/screens/expandableCalendar.tsx @@ -1,8 +1,10 @@ import _ from 'lodash'; import React, {Component, useCallback} from 'react'; import {Platform, StyleSheet, Alert, View, Text, TouchableOpacity, Button} from 'react-native'; +// @ts-expect-error import {ExpandableCalendar, AgendaList, CalendarProvider, WeekCalendar} from 'react-native-calendars'; + const testIDs = require('../testIDs'); const today = new Date().toISOString().split('T')[0]; @@ -128,7 +130,11 @@ function getTheme() { const leftArrowIcon = require('../img/previous.png'); const rightArrowIcon = require('../img/next.png'); -export default class ExpandableCalendarScreen extends Component { +interface Props { + weekView?: boolean +} + +export default class ExpandableCalendarScreen extends Component { marked = getMarkedDates(ITEMS); theme = getTheme(); todayBtnTheme = { @@ -144,7 +150,7 @@ export default class ExpandableCalendarScreen extends Component { // console.warn('ExpandableCalendarScreen onMonthChange: ', month, updateSource); }; - renderItem = ({item}) => { + renderItem = ({item}: any) => { return ; }; @@ -191,7 +197,11 @@ export default class ExpandableCalendarScreen extends Component { } } -const AgendaItem = React.memo(function AgendaItem(props) { +interface ItemProps { + item: any +} + +const AgendaItem = React.memo(function AgendaItem(props: ItemProps) { // console.warn('item rendered', Date.now()); const {item} = props; diff --git a/src/expandableCalendar/Context/Presenter.spec.js b/src/expandableCalendar/Context/Presenter.spec.js index b2f7bf5d73..555a9910f6 100644 --- a/src/expandableCalendar/Context/Presenter.spec.js +++ b/src/expandableCalendar/Context/Presenter.spec.js @@ -3,22 +3,18 @@ import XDate from 'xdate'; import {UPDATE_SOURCES} from '../commons'; import {toMarkingFormat} from '../../interface'; + describe('Context provider tests', () => { const makeUUT = () => { return new Presenter(); }; const pastDate = '2021-04-04'; - const futureDate = '2050-04-04'; - const today1 = XDate(); - const today2 = new Date(); - const todayDate = toMarkingFormat(XDate()); - - const updateSource = UPDATE_SOURCES.CALENDAR_INIT; + const updateSources = UPDATE_SOURCES.CALENDAR_INIT; describe('isPastDate function tests', () => { it('Expect to get true while passing a past date', () => { @@ -35,7 +31,6 @@ describe('Context provider tests', () => { describe('Button Icon test', () => { it('Expect to get down button on past date', () => { const {getButtonIcon} = makeUUT(); - const imageUp = '../../../src/img/up.png'; const imageDown = '../../../src/img/down.png'; @@ -62,12 +57,12 @@ describe('Context provider tests', () => { const {setDate} = makeUUT(); const date = '2021-01-01'; const sameMonthDate = '2021-01-20'; - const props = {onDateChanged, onMonthChange, showTodayButton: false}; - setDate(props, date, sameMonthDate, updateState, updateSource); + + setDate(props, date, sameMonthDate, updateState, updateSources); expect(updateState).toBeCalled(); - expect(onDateChanged).toBeCalledWith(date, updateSource); + expect(onDateChanged).toBeCalledWith(date, updateSources); expect(onMonthChange).not.toBeCalled(); }); @@ -75,12 +70,12 @@ describe('Context provider tests', () => { const {setDate} = makeUUT(); const date = '2021-01-01'; const differentMonth = '2021-02-20'; - const props = {onDateChanged, onMonthChange, showTodayButton: false}; - setDate(props, date, differentMonth, updateState, updateSource); + + setDate(props, date, differentMonth, updateState, updateSources); expect(updateState).toBeCalled(); - expect(onDateChanged).toBeCalledWith(date, updateSource); + expect(onDateChanged).toBeCalledWith(date, updateSources); expect(onMonthChange).toBeCalled(); }); }); @@ -138,8 +133,8 @@ describe('Context provider tests', () => { it("Expect animation value to be top position when today's date passed", () => { const {getPositionAnimation} = makeUUT(); const TOP_POSITION = 65; - const {tension, friction, useNativeDriver} = getPositionAnimation(todayDate, 10); + expect(tension).toEqual(30); expect(friction).toEqual(8); expect(useNativeDriver).toBe(true); @@ -159,8 +154,8 @@ describe('Context provider tests', () => { it('Expect opacity animation value', () => { const {getOpacityAnimation} = makeUUT(); const disabledOpacity = 0.5; - let data = getOpacityAnimation({disabledOpacity}, true); + expect(data.toValue).toBe(0.5); data = getOpacityAnimation({disabledOpacity}, false); @@ -174,7 +169,6 @@ describe('Context provider tests', () => { describe('onTodayPressed tests', () => { it("Expect return value to be XDate today's date", () => { const {getTodayDate} = makeUUT(); - expect(getTodayDate()).toEqual(todayDate); }); }); diff --git a/src/expandableCalendar/Context/Presenter.js b/src/expandableCalendar/Context/Presenter.ts similarity index 58% rename from src/expandableCalendar/Context/Presenter.js rename to src/expandableCalendar/Context/Presenter.ts index 2a09f0977b..723ebe762b 100644 --- a/src/expandableCalendar/Context/Presenter.js +++ b/src/expandableCalendar/Context/Presenter.ts @@ -1,15 +1,21 @@ import _ from 'lodash'; import XDate from 'xdate'; -import {sameMonth as dateutils_sameMonth} from '../../dateutils'; + +// @ts-expect-error +import {sameMonth} from '../../dateutils'; +// @ts-expect-error import {xdateToData, toMarkingFormat} from '../../interface'; +import {CalendarContextProviderProps} from './Provider'; +import {UpdateSource} from '../../types'; + const commons = require('../commons'); const TOP_POSITION = 65; class Presenter { - _isPastDate(date) { - const today = XDate(); - const d = XDate(date); + _isPastDate(date: Date) { + const today = new XDate(); + const d = new XDate(date); if (today.getFullYear() > d.getFullYear()) { return true; @@ -35,7 +41,7 @@ class Presenter { return require('../../img/up.png'); }; - getButtonIcon = (date, showTodayButton = true) => { + getButtonIcon = (date: Date, showTodayButton = true) => { if (!showTodayButton) { return undefined; } @@ -43,40 +49,40 @@ class Presenter { return icon; }; - setDate = (props, date, newDate, updateState, updateSource) => { - const sameMonth = dateutils_sameMonth(XDate(date), XDate(newDate)); + setDate = (props: CalendarContextProviderProps, date: Date, newDate: Date, updateState: (buttonIcon: any) => any, updateSource: UpdateSource) => { + const isSameMonth = sameMonth(new XDate(date), new XDate(newDate)); const buttonIcon = this.getButtonIcon(date, props.showTodayButton); updateState(buttonIcon); _.invoke(props, 'onDateChanged', date, updateSource); - if (!sameMonth) { - _.invoke(props, 'onMonthChange', xdateToData(XDate(date)), updateSource); + if (!isSameMonth) { + _.invoke(props, 'onMonthChange', xdateToData(new XDate(date)), updateSource); } }; - setDisabled = (showTodayButton, newDisabledValue, oldDisabledValue, updateState) => { + setDisabled = (showTodayButton: boolean, newDisabledValue: boolean, oldDisabledValue: boolean, updateState: (disabled: boolean) => void) => { if (!showTodayButton || newDisabledValue === oldDisabledValue) { return; } updateState(newDisabledValue); }; - shouldAnimateTodayButton = props => { + shouldAnimateTodayButton = (props: CalendarContextProviderProps) => { return props.showTodayButton; }; - _isToday = date => { - const today = toMarkingFormat(XDate()); + _isToday = (date: Date) => { + const today = toMarkingFormat(new XDate()); return today === date; }; getTodayDate = () => { - return toMarkingFormat(XDate()); + return toMarkingFormat(new XDate()); }; - getPositionAnimation = (date, todayBottomMargin) => { + getPositionAnimation = (date: Date, todayBottomMargin = 0) => { const toValue = this._isToday(date) ? TOP_POSITION : -todayBottomMargin || -TOP_POSITION; return { toValue, @@ -86,19 +92,20 @@ class Presenter { }; }; - shouldAnimateOpacity = props => { + shouldAnimateOpacity = (props: CalendarContextProviderProps) => { return props.disabledOpacity; }; - getOpacityAnimation = (props, disabled) => { + getOpacityAnimation = (props: CalendarContextProviderProps, disabled: boolean) => { return { - toValue: disabled ? props.disabledOpacity : 1, + toValue: disabled ? props.disabledOpacity || 0 : 1, duration: 500, useNativeDriver: true }; }; getTodayFormatted = () => { + // @ts-expect-error const todayString = XDate.locales[XDate.defaultLocale].today || commons.todayString; const today = todayString.charAt(0).toUpperCase() + todayString.slice(1); return today; diff --git a/src/expandableCalendar/Context/Provider.js b/src/expandableCalendar/Context/Provider.tsx similarity index 66% rename from src/expandableCalendar/Context/Provider.js rename to src/expandableCalendar/Context/Provider.tsx index 31512a42a9..0dcce9bf2f 100644 --- a/src/expandableCalendar/Context/Provider.js +++ b/src/expandableCalendar/Context/Provider.tsx @@ -1,21 +1,46 @@ -import React, {Component} from 'react'; -import {StyleSheet, Animated, TouchableOpacity, View} from 'react-native'; import PropTypes from 'prop-types'; import XDate from 'xdate'; + +import React, {Component} from 'react'; +import {StyleSheet, Animated, TouchableOpacity, View, StyleProp, ViewStyle} from 'react-native'; + +// @ts-expect-error +import {toMarkingFormat} from '../../interface'; +import {Theme, UpdateSource, DateData} from '../../types'; import styleConstructor from '../style'; import CalendarContext from '.'; import Presenter from './Presenter'; -import {toMarkingFormat} from '../../interface'; + const commons = require('../commons'); -const UPDATE_SOURCES = commons.UPDATE_SOURCES; +const updateSources = commons.UPDATE_SOURCES; const TOP_POSITION = 65; +interface Props { + /** Initial date in 'yyyy-MM-dd' format. Default = Date() */ + date: Date; + /** Callback for date change event */ + onDateChanged?: () => Date, + /** Callback for month change event */ + onMonthChange?: () => DateData, + /** Whether to show the today button */ + showTodayButton?: boolean; + /** Today button's top position */ + todayBottomMargin?: number; + /** Today button's style */ + todayButtonStyle?: ViewStyle; + /** The opacity for the disabled today button (0-1) */ + disabledOpacity?: number; + style?: StyleProp; + theme?: Theme; +} +export type CalendarContextProviderProps = Props; + /** * @description: Calendar context provider component * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js */ -class CalendarProvider extends Component { +class CalendarProvider extends Component { static displayName = 'CalendarProvider'; static propTypes = { @@ -35,26 +60,22 @@ class CalendarProvider extends Component { disabledOpacity: PropTypes.number }; - constructor(props) { - super(props); - this.style = styleConstructor(props.theme); - this.presenter = new Presenter(); - const {showTodayButton} = props; - - this.state = { - prevDate: this.props.date || toMarkingFormat(XDate()), - date: this.props.date || toMarkingFormat(XDate()), - updateSource: UPDATE_SOURCES.CALENDAR_INIT, - buttonY: new Animated.Value(-props.todayBottomMargin || -TOP_POSITION), - buttonIcon: this.presenter.getButtonIcon(props.date, showTodayButton), - disabled: false, - opacity: new Animated.Value(1) - }; - } + style = styleConstructor(this.props.theme); + presenter = new Presenter(); + + state = { + prevDate: this.props.date || toMarkingFormat(new XDate()), + date: this.props.date || toMarkingFormat(new XDate()), + updateSource: updateSources.CALENDAR_INIT, + buttonY: new Animated.Value(this.props.todayBottomMargin ? -this.props.todayBottomMargin : -TOP_POSITION), + buttonIcon: this.presenter.getButtonIcon(this.props.date, this.props.showTodayButton), + disabled: false, + opacity: new Animated.Value(1) + }; - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (prevProps.date !== this.props.date) { - this.setDate(this.props.date, UPDATE_SOURCES.PROP_UPDATE); + this.setDate(this.props.date, updateSources.PROP_UPDATE); } } @@ -68,10 +89,10 @@ class CalendarProvider extends Component { }; }; - setDate = (date, updateSource) => { + setDate = (date: Date, updateSource: UpdateSource) => { const {setDate} = this.presenter; - const updateState = buttonIcon => { + const updateState = (buttonIcon: any) => { this.setState({date, prevDate: this.state.date, updateSource, buttonIcon}, () => { this.animateTodayButton(date); }); @@ -80,11 +101,11 @@ class CalendarProvider extends Component { setDate(this.props, date, this.state.date, updateState, updateSource); }; - setDisabled = disabled => { + setDisabled = (disabled: boolean) => { const {setDisabled} = this.presenter; - const {showTodayButton} = this.props; + const {showTodayButton = false} = this.props; - const updateState = disabled => { + const updateState = (disabled: boolean) => { this.setState({disabled}); this.animateOpacity(disabled); }; @@ -92,7 +113,7 @@ class CalendarProvider extends Component { setDisabled(showTodayButton, disabled, this.state.disabled, updateState); }; - animateTodayButton(date) { + animateTodayButton(date: Date) { const {shouldAnimateTodayButton, getPositionAnimation} = this.presenter; if (shouldAnimateTodayButton(this.props)) { @@ -104,7 +125,7 @@ class CalendarProvider extends Component { } } - animateOpacity(disabled) { + animateOpacity(disabled: boolean) { const {shouldAnimateOpacity, getOpacityAnimation} = this.presenter; if (shouldAnimateOpacity(this.props)) { @@ -118,7 +139,7 @@ class CalendarProvider extends Component { onTodayPress = () => { const today = this.presenter.getTodayDate(); - this.setDate(today, UPDATE_SOURCES.TODAY_PRESS); + this.setDate(today, updateSources.TODAY_PRESS); }; renderTodayButton() { diff --git a/src/expandableCalendar/Context/index.js b/src/expandableCalendar/Context/index.ts similarity index 100% rename from src/expandableCalendar/Context/index.js rename to src/expandableCalendar/Context/index.ts diff --git a/src/expandableCalendar/WeekCalendar/index.js b/src/expandableCalendar/WeekCalendar/index.tsx similarity index 76% rename from src/expandableCalendar/WeekCalendar/index.js rename to src/expandableCalendar/WeekCalendar/index.tsx index 23a8a4416d..f56b48db4f 100644 --- a/src/expandableCalendar/WeekCalendar/index.js +++ b/src/expandableCalendar/WeekCalendar/index.tsx @@ -1,30 +1,48 @@ import memoize from 'memoize-one'; import PropTypes from 'prop-types'; import XDate from 'xdate'; +import {Map} from 'immutable'; import React, {Component} from 'react'; -import {FlatList, View, Text} from 'react-native'; -import {Map} from 'immutable'; +import {FlatList, View, Text, NativeSyntheticEvent, NativeScrollEvent} from 'react-native'; +// @ts-expect-error import {extractComponentProps} from '../../component-updater'; +// @ts-expect-error import {weekDayNames} from '../../dateutils'; +// @ts-expect-error import {toMarkingFormat} from '../../interface'; +import {DateData} from '../../types'; import styleConstructor from '../style'; import asCalendarConsumer from '../asCalendarConsumer'; -import CalendarList from '../../calendar-list'; +import CalendarList, {CalendarListProps} from '../../calendar-list'; import Week from '../week'; import Presenter from './presenter'; + const commons = require('../commons'); const NUMBER_OF_PAGES = 2; // must be a positive number const applyAndroidRtlFix = commons.isAndroid && commons.isRTL; +interface Props extends CalendarListProps { + /** whether to have shadow/elevation for the calendar */ + allowShadow?: boolean; + /** whether to hide the names of the week days */ + hideDayNames?: boolean; + context?: any; +} +export type WeekCalendarProps = Props; + +interface State { + items: Date[] +} + /** * @description: Week calendar component * @note: Should be wrapped with 'CalendarProvider' * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js */ -class WeekCalendar extends Component { +class WeekCalendar extends Component { static displayName = 'WeekCalendar'; static propTypes = { @@ -42,23 +60,17 @@ class WeekCalendar extends Component { allowShadow: true }; - constructor(props) { - super(props); + style = styleConstructor(this.props.theme); + presenter = new Presenter(); + page = NUMBER_OF_PAGES; + // On Android+RTL there's an initial scroll that cause issues + firstAndroidRTLScrollIgnored = !applyAndroidRtlFix; - this.style = styleConstructor(props.theme); - - this.presenter = new Presenter(props); - this.list = React.createRef(); - this.page = NUMBER_OF_PAGES; - // On Android+RTL there's an initial scroll that cause issues - this.firstAndroidRTLScrollIgnored = !applyAndroidRtlFix; - - this.state = { - items: this.presenter.getDatesArray(this.props) - }; - } + state: State = { + items: this.presenter.getDatesArray(this.props) + }; - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { const {context} = this.props; const {shouldComponentUpdate, getDatesArray, scrollToIndex} = this.presenter; @@ -81,9 +93,9 @@ class WeekCalendar extends Component { return array; } - getDate(weekIndex) { - const {current, context, firstDay} = this.props; - const d = XDate(current || context.date); + getDate(weekIndex: number) { + const {current, context, firstDay = 0} = this.props; + const d = new XDate(current || context.date); // get the first day of the week as date (for the on scroll mark) let dayOfTheWeek = d.getDay(); if (dayOfTheWeek < firstDay && firstDay > 0) { @@ -100,21 +112,18 @@ class WeekCalendar extends Component { return [{width}, style]; }); - onDayPress = value => { - this.presenter.onDayPressed(this.props.context, value); + onDayPress = (value: DateData) => { + this.presenter.onDayPress(this.props.context, value); }; - onScroll = ({ - nativeEvent: { - contentOffset: {x} - } - }) => { + onScroll = (event: NativeSyntheticEvent) => { + const x = event.nativeEvent.contentOffset.x; const {onScroll} = this.presenter; const {context} = this.props; const {items} = this.state; const {containerWidth, page} = this; - const updateState = (newData, newPage) => { + const updateState = (newData: Date[], newPage: number) => { this.page = newPage; this.setState({items: [...newData]}); }; @@ -126,7 +135,7 @@ class WeekCalendar extends Component { const {items} = this.state; const {onMomentumScrollEnd} = this.presenter; - const updateItems = items => { + const updateItems = (items: Date[]) => { setTimeout(() => { this.setState({items: [...items]}); }, 100); @@ -135,7 +144,7 @@ class WeekCalendar extends Component { onMomentumScrollEnd({items, props: this.props, page: this.page, updateItems}); }; - renderItem = ({item}) => { + renderItem = ({item}: any) => { const {style, onDayPress, markedDates, firstDay, ...others} = extractComponentProps(Week, this.props); const {context} = this.props; @@ -156,7 +165,7 @@ class WeekCalendar extends Component { ); }; - getItemLayout = (data, index) => { + getItemLayout = (_: any, index: number) => { return { length: this.containerWidth, offset: this.containerWidth * index, @@ -164,13 +173,13 @@ class WeekCalendar extends Component { }; }; - keyExtractor = (item, index) => index.toString(); + keyExtractor = (_: Date, index: number) => index.toString(); renderWeekDaysNames = memoize(weekDaysNames => { - return weekDaysNames.map((day, idx) => ( + return weekDaysNames.map((day: Date, index: number) => ( { }); describe('Event - onDatePressed', () => { - it('onDayPressed', () => { + it('onDayPress', () => { const setDate = jest.fn(); const context = {date: '2021-02-02', setDate}; - const {onDayPressed} = makeUUT({context}); + const {onDayPress} = makeUUT({context}); - onDayPressed(context, {dateString: '2021-01-22'}); + onDayPress(context, {dateString: '2021-01-22'}); expect(setDate).toBeCalledWith('2021-01-22', UPDATE_SOURCES.DAY_PRESS); }); }); diff --git a/src/expandableCalendar/WeekCalendar/presenter.js b/src/expandableCalendar/WeekCalendar/presenter.ts similarity index 61% rename from src/expandableCalendar/WeekCalendar/presenter.js rename to src/expandableCalendar/WeekCalendar/presenter.ts index a60930424b..a07d929d96 100644 --- a/src/expandableCalendar/WeekCalendar/presenter.js +++ b/src/expandableCalendar/WeekCalendar/presenter.ts @@ -1,37 +1,42 @@ import _ from 'lodash'; +import XDate from 'xdate'; + import React from 'react'; + +// @ts-expect-error import {sameWeek} from '../../dateutils'; -const commons = require('../commons'); -import XDate from 'xdate'; +// @ts-expect-error import {toMarkingFormat} from '../../interface'; +import {DateData} from '../../types'; +import {WeekCalendarProps} from './index'; -const UPDATE_SOURCES = commons.UPDATE_SOURCES; + +const commons = require('../commons'); +const updateSources = commons.UPDATE_SOURCES; // must be a positive number const NUMBER_OF_PAGES = 2; class Presenter { - constructor() { - this.list = React.createRef(); - this._applyAndroidRtlFix = commons.isAndroid && commons.isRTL; - // On Android+RTL there's an initial scroll that cause issues - this._firstAndroidRTLScrollIgnored = !this._applyAndroidRtlFix; - } - scrollToIndex = animated => { - this.list.current.scrollToIndex({animated, index: NUMBER_OF_PAGES}); + private _applyAndroidRtlFix = commons.isAndroid && commons.isRTL; + // On Android+RTL there's an initial scroll that cause issues + private _firstAndroidRTLScrollIgnored = !this._applyAndroidRtlFix; + public list: React.RefObject = React.createRef(); + + scrollToIndex = (animated: boolean) => { + this.list?.current?.scrollToIndex({animated, index: NUMBER_OF_PAGES}); }; - isSameWeek = (date, prevDate, firstDay) => { + isSameWeek = (date: Date, prevDate: Date, firstDay: number) => { return sameWeek(date, prevDate, firstDay); }; // Events - - onDayPressed = (context, value) => { - _.invoke(context, 'setDate', value.dateString, UPDATE_SOURCES.DAY_PRESS); + onDayPress = (context: any, value: DateData) => { + _.invoke(context, 'setDate', value.dateString, updateSources.DAY_PRESS); }; - onScroll = ({context, updateState, x, page, items, width}) => { + onScroll = ({context, updateState, x, page, items, width}: any) => { if (!this._firstAndroidRTLScrollIgnored) { this._firstAndroidRTLScrollIgnored = true; return; @@ -40,14 +45,14 @@ class Presenter { x = this._getX(x, items?.length, width); const newPage = this._getNewPage(x, width); - if (this._shouldUpdateState(page)) { - _.invoke(context, 'setDate', items[newPage], UPDATE_SOURCES.WEEK_SCROLL); + if (this._shouldUpdateState(page, newPage)) { + _.invoke(context, 'setDate', items[newPage], updateSources.WEEK_SCROLL); const data = this._getItemsForPage(page, items); updateState(data, newPage); } }; - onMomentumScrollEnd = ({items, props, page, updateItems}) => { + onMomentumScrollEnd = ({items, props, page, updateItems}: any) => { if (this._isFirstPage(page) || this._isLastPage(page, items)) { this.scrollToIndex(false); @@ -63,16 +68,16 @@ class Presenter { } }; - shouldComponentUpdate = (context, prevContext) => { + shouldComponentUpdate = (context: any, prevContext: any) => { const {date, updateSource} = context; return ( date !== prevContext.date && - updateSource !== UPDATE_SOURCES.WEEK_SCROLL + updateSource !== updateSources.WEEK_SCROLL ); }; - getDate({current, context, firstDay}, weekIndex) { - const d = XDate(current || context.date); + getDate({current, context, firstDay = 0}: WeekCalendarProps, weekIndex: number) { + const d = new XDate(current || context.date); // get the first day of the week as date (for the on scroll mark) let dayOfTheWeek = d.getDay(); if (dayOfTheWeek < firstDay && firstDay > 0) { @@ -85,7 +90,7 @@ class Presenter { return toMarkingFormat(newDate); } - getDatesArray = args => { + getDatesArray = (args: WeekCalendarProps) => { const array = []; for (let index = -NUMBER_OF_PAGES; index <= NUMBER_OF_PAGES; index++) { const d = this.getDate(args, index); @@ -94,11 +99,11 @@ class Presenter { return array; }; - _shouldUpdateState = (page, newPage) => { + _shouldUpdateState = (page: number, newPage: number) => { return page !== newPage; }; - _getX = (x, itemsCount, containerWidth) => { + _getX = (x: number, itemsCount: number, containerWidth: number) => { if (this._applyAndroidRtlFix) { const numberOfPages = itemsCount - 1; const overallWidth = numberOfPages * containerWidth; @@ -107,47 +112,47 @@ class Presenter { return x; }; - _getNewPage = (x, containerWidth) => { + _getNewPage = (x: number, containerWidth: number) => { return Math.round(x / containerWidth); }; - _isFirstPage = page => { + _isFirstPage = (page: number) => { return page === 0; }; - _isLastPage = (page, items) => { + _isLastPage = (page: number, items: Date[]) => { return page === items.length - 1; }; - _getNexPageItems = items => { + _getNexPageItems = (items: Date[]) => { return items.map((_, i) => { const index = i <= NUMBER_OF_PAGES ? i + NUMBER_OF_PAGES : i; return items[index]; }); }; - _getFirstPageItems = items => { + _getFirstPageItems = (items: Date[]) => { return items.map((_, i) => { const index = i >= NUMBER_OF_PAGES ? i - NUMBER_OF_PAGES : i; return items[index]; }); }; - _mergeArraysFromEnd = (items, newArray) => { + _mergeArraysFromEnd = (items: Date[], newArray: Date[]) => { for (let i = NUMBER_OF_PAGES + 1; i < items.length; i++) { items[i] = newArray[i]; } return items; }; - _mergeArraysFromTop = (items, newArray) => { + _mergeArraysFromTop = (items: Date[], newArray: Date[]) => { for (let i = 0; i < NUMBER_OF_PAGES; i++) { items[i] = newArray[i]; } return items; }; - _getItemsForPage = (page, items) => { + _getItemsForPage = (page: number, items: Date[]) => { if (this._isLastPage(page, items)) { return this._getNexPageItems(items); } else if (this._isFirstPage(page)) { diff --git a/src/expandableCalendar/agendaList.js b/src/expandableCalendar/agendaList.tsx similarity index 60% rename from src/expandableCalendar/agendaList.js rename to src/expandableCalendar/agendaList.tsx index d09a050ba1..7236f32f04 100644 --- a/src/expandableCalendar/agendaList.js +++ b/src/expandableCalendar/agendaList.tsx @@ -1,15 +1,44 @@ import _ from 'lodash'; -import React, {Component} from 'react'; -import {SectionList, Text} from 'react-native'; import PropTypes from 'prop-types'; import XDate from 'xdate'; -import {isToday as dateutils_isToday} from '../dateutils'; + +import React, {Component} from 'react'; +import {Text, SectionList, SectionListProps, DefaultSectionT, SectionListData, ViewStyle, NativeSyntheticEvent, NativeScrollEvent, LayoutChangeEvent, ViewToken} from 'react-native'; + +// @ts-expect-error +import {isToday} from '../dateutils'; +// @ts-expect-error +import {getMoment} from '../momentResolver'; +import {Theme} from '../types'; import styleConstructor from './style'; import asCalendarConsumer from './asCalendarConsumer'; -import {getMoment} from '../momentResolver'; + const commons = require('./commons'); -const UPDATE_SOURCES = commons.UPDATE_SOURCES; +const updateSources = commons.UPDATE_SOURCES; + +interface Props extends SectionListProps { + /** day format in section title. Formatting values: http://arshaw.com/xdate/#Formatting */ + dayFormat?: string; + /** a function to custom format the section header's title */ + dayFormatter?: (arg0: string) => string; + /** whether to use moment.js for date string formatting + * (remember to pass 'dayFormat' with appropriate format, like 'dddd, MMM D') */ + useMoment?: boolean; + /** whether to mark today's title with the "Today, ..." string. Default = true */ + markToday?: boolean; + /** style passed to the section view */ + sectionStyle?: ViewStyle; + /** whether to block the date change in calendar (and calendar context provider) when agenda scrolls */ + avoidDateUpdates?: boolean; + /** offset scroll to section */ + viewOffset?: number; + theme?: Theme; + context?: any; +} +export type AgendaListProps = Props; + +interface State {} /** * @description: AgendaList component @@ -17,11 +46,11 @@ const UPDATE_SOURCES = commons.UPDATE_SOURCES; * @extends: SectionList * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js */ -class AgendaList extends Component { +class AgendaList extends Component { static displayName = 'AgendaList'; static propTypes = { - ...SectionList.propTypes, + // ...SectionList.propTypes, /** day format in section title. Formatting values: http://arshaw.com/xdate/#Formatting */ dayFormat: PropTypes.string, /** a function to custom format the section header's title */ @@ -43,54 +72,54 @@ class AgendaList extends Component { markToday: true }; - constructor(props) { - super(props); - this.style = styleConstructor(props.theme); - - this._topSection = _.get(props, 'sections[0].title'); - this.didScroll = false; - this.sectionScroll = false; - - this.viewabilityConfig = { - itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible - }; - this.list = React.createRef(); - } - - getSectionIndex(date) { - let i; - _.map(this.props.sections, (section, index) => { - // NOTE: sections titles should match current date format!!! - if (section.title === date) { - i = index; - return; - } - }); - return i; - } + style = styleConstructor(this.props.theme); + _topSection = _.get(this.props, 'sections[0].title'); + didScroll = false; + sectionScroll = false; + viewabilityConfig = { + itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible + }; + list: React.RefObject = React.createRef(); + sectionHeight = 0; componentDidMount() { const {date} = this.props.context; if (date !== this._topSection) { setTimeout(() => { const sectionIndex = this.getSectionIndex(date); - this.scrollToSection(sectionIndex); + if (sectionIndex) { + this.scrollToSection(sectionIndex); + } }, 500); } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { const {updateSource, date} = this.props.context; if (date !== prevProps.context.date) { // NOTE: on first init data should set first section to the current date!!! - if (updateSource !== UPDATE_SOURCES.LIST_DRAG && updateSource !== UPDATE_SOURCES.CALENDAR_INIT) { + if (updateSource !== updateSources.LIST_DRAG && updateSource !== updateSources.CALENDAR_INIT) { const sectionIndex = this.getSectionIndex(date); - this.scrollToSection(sectionIndex); + if (sectionIndex) { + this.scrollToSection(sectionIndex); + } } } } - getSectionTitle(title) { + getSectionIndex(date: Date) { + let i; + _.map(this.props.sections, (section, index) => { + // NOTE: sections titles should match current date format!!! + if (section.title === date) { + i = index; + return; + } + }); + return i; + } + + getSectionTitle(title: string) { if (!title) return; const {dayFormatter, dayFormat, useMoment, markToday} = this.props; @@ -103,81 +132,84 @@ class AgendaList extends Component { const moment = getMoment(); sectionTitle = moment(title).format(dayFormat); } else { - sectionTitle = XDate(title).toString(dayFormat); + sectionTitle = new XDate(title).toString(dayFormat); } } if (markToday) { + // @ts-expect-error const todayString = XDate.locales[XDate.defaultLocale].today || commons.todayString; - const isToday = dateutils_isToday(XDate(title)); - sectionTitle = isToday ? `${todayString}, ${sectionTitle}` : sectionTitle; + const today = isToday(new XDate(title)); + sectionTitle = today ? `${todayString}, ${sectionTitle}` : sectionTitle; } return sectionTitle; } - scrollToSection(sectionIndex) { - if (this.list.current && sectionIndex !== undefined) { + scrollToSection(sectionIndex: number) { + if (this.list?.current && sectionIndex !== undefined) { + const {sections, viewOffset = 0} = this.props; this.sectionScroll = true; // to avoid setDate() in onViewableItemsChanged - this._topSection = this.props.sections[sectionIndex].title; + this._topSection = sections[sectionIndex].title; this.list.current.scrollToLocation({ animated: true, sectionIndex: sectionIndex, itemIndex: 0, viewPosition: 0, // position at the top - viewOffset: (commons.isAndroid ? this.sectionHeight : 0) + this.props.viewOffset + viewOffset: (commons.isAndroid ? this.sectionHeight : 0) + viewOffset }); } } - onViewableItemsChanged = ({viewableItems}) => { - if (viewableItems && !this.sectionScroll) { - const topSection = _.get(viewableItems[0], 'section.title'); + onViewableItemsChanged = ((info: {viewableItems: Array; changed: Array}) => { + if (info?.viewableItems && !this.sectionScroll) { + const topSection = _.get(info?.viewableItems[0], 'section.title'); if (topSection && topSection !== this._topSection) { this._topSection = topSection; if (this.didScroll && !this.props.avoidDateUpdates) { // to avoid setDate() on first load (while setting the initial context.date value) - _.invoke(this.props.context, 'setDate', this._topSection, UPDATE_SOURCES.LIST_DRAG); + _.invoke(this.props.context, 'setDate', this._topSection, updateSources.LIST_DRAG); } } } - }; + }); - onScroll = event => { + onScroll = (event: NativeSyntheticEvent) => { if (!this.didScroll) { this.didScroll = true; } _.invoke(this.props, 'onScroll', event); }; - onMomentumScrollBegin = event => { + onMomentumScrollBegin = (event: NativeSyntheticEvent) => { _.invoke(this.props.context, 'setDisabled', true); _.invoke(this.props, 'onMomentumScrollBegin', event); }; - onMomentumScrollEnd = event => { + onMomentumScrollEnd = (event: NativeSyntheticEvent) => { // when list momentum ends AND when scrollToSection scroll ends this.sectionScroll = false; _.invoke(this.props.context, 'setDisabled', false); _.invoke(this.props, 'onMomentumScrollEnd', event); }; - onScrollToIndexFailed = info => { - if (this.props.onScrollToIndexFailed) { - this.props.onScrollToIndexFailed(info); - } else { - console.warn('onScrollToIndexFailed info: ', info); - } + onScrollToIndexFailed = (info: { + index: number; + highestMeasuredFrameIndex: number; + averageItemLength: number; +}) => { + console.warn('onScrollToIndexFailed info: ', info); }; - onHeaderLayout = ({nativeEvent}) => { - this.sectionHeight = nativeEvent.layout.height; + onHeaderLayout = (event: LayoutChangeEvent) => { + this.sectionHeight = event.nativeEvent.layout.height; }; - renderSectionHeader = ({section: {title}}) => { + renderSectionHeader = (info: {section: SectionListData}) => { const {renderSectionHeader, sectionStyle} = this.props; - + const title = info?.section?.title; + if (renderSectionHeader) { return renderSectionHeader(title); } @@ -189,7 +221,7 @@ class AgendaList extends Component { ); }; - keyExtractor = (item, index) => { + keyExtractor = (item: any, index: number) => { const {keyExtractor} = this.props; return _.isFunction(keyExtractor) ? keyExtractor(item, index) : String(index); }; diff --git a/src/expandableCalendar/asCalendarConsumer.js b/src/expandableCalendar/asCalendarConsumer.tsx similarity index 51% rename from src/expandableCalendar/asCalendarConsumer.js rename to src/expandableCalendar/asCalendarConsumer.tsx index 3daaad2335..2d21ab9544 100644 --- a/src/expandableCalendar/asCalendarConsumer.js +++ b/src/expandableCalendar/asCalendarConsumer.tsx @@ -1,13 +1,22 @@ -import React, {Component} from 'react'; +import React, {Component, Ref} from 'react'; +// @ts-expect-error import hoistNonReactStatic from 'hoist-non-react-statics'; import CalendarContext from './Context'; -function asCalendarConsumer(WrappedComponent) { + +function asCalendarConsumer(WrappedComponent: React.ComponentType): React.ComponentClass { + class CalendarConsumer extends Component { + contentRef: any; + + saveRef = (r: Ref>) => { + this.contentRef = r; + }; + render() { return ( - {context => (this.contentRef = r)} context={context} {...this.props} />} + {context => } ); } diff --git a/src/expandableCalendar/commons.js b/src/expandableCalendar/commons.ts similarity index 69% rename from src/expandableCalendar/commons.js rename to src/expandableCalendar/commons.ts index 438bab2fdf..606fc6718e 100644 --- a/src/expandableCalendar/commons.js +++ b/src/expandableCalendar/commons.ts @@ -8,15 +8,16 @@ export const isIos = Platform.OS === 'ios'; export const screenWidth = width; export const screenHeight = height; export const screenAspectRatio = screenWidth < screenHeight ? screenHeight / screenWidth : screenWidth / screenHeight; +// @ts-expect-error export const isTablet = Platform.isPad || (screenAspectRatio < 1.6 && Math.max(screenWidth, screenHeight) >= 900); export const todayString = 'today'; -export const UPDATE_SOURCES = { - CALENDAR_INIT: 'calendarInit', - TODAY_PRESS: 'todayPress', - LIST_DRAG: 'listDrag', - DAY_PRESS: 'dayPress', - PAGE_SCROLL: 'pageScroll', - WEEK_SCROLL: 'weekScroll', - PROP_UPDATE: 'propUpdate' -}; +export enum UPDATE_SOURCES { + CALENDAR_INIT = 'calendarInit', + TODAY_PRESS = 'todayPress', + LIST_DRAG = 'listDrag', + DAY_PRESS = 'dayPress', + PAGE_SCROLL = 'pageScroll', + WEEK_SCROLL = 'weekScroll', + PROP_UPDATE = 'propUpdate' +} diff --git a/src/expandableCalendar/index.js b/src/expandableCalendar/index.tsx similarity index 64% rename from src/expandableCalendar/index.js rename to src/expandableCalendar/index.tsx index 7f9eaf23d1..ebb2cef719 100644 --- a/src/expandableCalendar/index.js +++ b/src/expandableCalendar/index.tsx @@ -4,24 +4,29 @@ import memoize from 'memoize-one'; import XDate from 'xdate'; import React, {Component} from 'react'; -import {AccessibilityInfo, PanResponder, Animated, View, Text, Image} from 'react-native'; +import {AccessibilityInfo, PanResponder, Animated, View, ViewStyle, Text, Image, ImageSourcePropType, PanResponderInstance, GestureResponderEvent, PanResponderGestureState} from 'react-native'; +// @ts-expect-error import {CALENDAR_KNOB} from '../testIDs'; +// @ts-expect-error import {page, weekDayNames} from '../dateutils'; +// @ts-expect-error import {parseDate, toMarkingFormat} from '../interface'; +import {Theme, DateData, Direction} from 'types'; import styleConstructor, {HEADER_HEIGHT} from './style'; -import CalendarList from '../calendar-list'; +import CalendarList, {CalendarListProps} from '../calendar-list'; import Calendar from '../calendar'; import asCalendarConsumer from './asCalendarConsumer'; import WeekCalendar from './WeekCalendar'; import Week from './week'; + const commons = require('./commons'); -const UPDATE_SOURCES = commons.UPDATE_SOURCES; -const POSITIONS = { - CLOSED: 'closed', - OPEN: 'open' -}; +const updateSources = commons.UPDATE_SOURCES; +enum Positions { + CLOSED = 'closed', + OPEN = 'open' +} const SPEED = 20; const BOUNCINESS = 6; const CLOSED_HEIGHT = 120; // header + 1 week @@ -29,6 +34,41 @@ const WEEK_HEIGHT = 46; const KNOB_CONTAINER_HEIGHT = 20; const DAY_NAMES_PADDING = 24; const PAN_GESTURE_THRESHOLD = 30; +const LEFT_ARROW = require('../calendar/img/previous.png'); +const RIGHT_ARROW = require('../calendar/img/next.png'); + + +export interface Props extends CalendarListProps { + /** the initial position of the calendar ('open' or 'closed') */ + initialPosition?: Positions; + /** callback that fires when the calendar is opened or closed */ + onCalendarToggled?: () => boolean; + /** an option to disable the pan gesture and disable the opening and closing of the calendar (initialPosition will persist)*/ + disablePan?: boolean; + /** whether to hide the knob */ + hideKnob?: boolean; + /** source for the left arrow image */ + leftArrowImageSource?: ImageSourcePropType; + /** source for the right arrow image */ + rightArrowImageSource?: ImageSourcePropType; + /** whether to have shadow/elevation for the calendar */ + allowShadow?: boolean; + /** whether to disable the week scroll in closed position */ + disableWeekScroll?: boolean; + /** a threshold for opening the calendar with the pan gesture */ + openThreshold?: number; + /** a threshold for closing the calendar with the pan gesture */ + closeThreshold?: number; + context?: any; +} +export type ExpandableCalendarProps = Props; + +interface State { + deltaY: Animated.Value; + headerDeltaY: Animated.Value; + position: Positions; + screenReaderEnabled: boolean; +} /** * @description: Expandable calendar component @@ -37,13 +77,13 @@ const PAN_GESTURE_THRESHOLD = 30; * @extendslink: docs/CalendarList * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js */ -class ExpandableCalendar extends Component { +class ExpandableCalendar extends Component { static displayName = 'ExpandableCalendar'; static propTypes = { ...CalendarList.propTypes, /** the initial position of the calendar ('open' or 'closed') */ - initialPosition: PropTypes.oneOf(_.values(POSITIONS)), + initialPosition: PropTypes.oneOf(_.values(Positions)), /** callback that fires when the calendar is opened or closed */ onCalendarToggled: PropTypes.func, /** an option to disable the pan gesture and disable the opening and closing of the calendar (initialPosition will persist)*/ @@ -66,50 +106,74 @@ class ExpandableCalendar extends Component { static defaultProps = { horizontal: true, - initialPosition: POSITIONS.CLOSED, + initialPosition: Positions.CLOSED, firstDay: 0, - leftArrowImageSource: require('../calendar/img/previous.png'), - rightArrowImageSource: require('../calendar/img/next.png'), + leftArrowImageSource: LEFT_ARROW, + rightArrowImageSource: RIGHT_ARROW, allowShadow: true, openThreshold: PAN_GESTURE_THRESHOLD, closeThreshold: PAN_GESTURE_THRESHOLD }; - static positions = POSITIONS; + static positions = Positions; - constructor(props) { + style = styleConstructor(this.props.theme); + panResponder: PanResponderInstance; + closedHeight: number; + numberOfWeeks: number; + openHeight: number; + _height: number; + _wrapperStyles: { + style: ViewStyle; + } + _headerStyles: { + style: ViewStyle; + }; + _weekCalendarStyles: { + style: ViewStyle; + }; + visibleMonth: any; + initialDate: XDate; + headerStyleOverride: Theme; + header: React.RefObject = React.createRef(); + wrapper: React.RefObject = React.createRef(); + calendar: React.RefObject = React.createRef(); + weekCalendar: React.RefObject = React.createRef(); + + constructor(props: Props) { super(props); - - this.style = styleConstructor(props.theme); + this.closedHeight = CLOSED_HEIGHT + (props.hideKnob ? 0 : KNOB_CONTAINER_HEIGHT); - this.numberOfWeeks = this.getNumberOfWeeksInMonth(XDate(this.props.context.date)); + this.numberOfWeeks = this.getNumberOfWeeksInMonth(new XDate(this.props.context.date)); this.openHeight = this.getOpenHeight(); - const startHeight = props.initialPosition === POSITIONS.CLOSED ? this.closedHeight : this.openHeight; + const startHeight = props.initialPosition === Positions.CLOSED ? this.closedHeight : this.openHeight; this._height = startHeight; this._wrapperStyles = {style: {height: startHeight}}; - this._headerStyles = {style: {top: props.initialPosition === POSITIONS.CLOSED ? 0 : -HEADER_HEIGHT}}; + this._headerStyles = {style: {top: props.initialPosition === Positions.CLOSED ? 0 : -HEADER_HEIGHT}}; this._weekCalendarStyles = {style: {}}; - this.wrapper = undefined; - this.calendar = undefined; this.visibleMonth = this.getMonth(this.props.context.date); - this.initialDate = props.context.date; // should be set only once!!! + this.initialDate = new XDate(props.context.date); // should be set only once!!! this.headerStyleOverride = { - 'stylesheet.calendar.header': { - week: { - marginTop: 7, - marginBottom: -4, // reduce space between dayNames and first line of dates - flexDirection: 'row', - justifyContent: 'space-around' + stylesheet: { + calendar: { + header: { + week: { + marginTop: 7, + marginBottom: -4, // reduce space between dayNames and first line of dates + flexDirection: 'row', + justifyContent: 'space-around' + } + } } } }; this.state = { deltaY: new Animated.Value(startHeight), - headerDeltaY: new Animated.Value(props.initialPosition === POSITIONS.CLOSED ? 0 : -HEADER_HEIGHT), - position: props.initialPosition, + headerDeltaY: new Animated.Value(props.initialPosition === Positions.CLOSED ? 0 : -HEADER_HEIGHT), + position: props.initialPosition || Positions.CLOSED, screenReaderEnabled: false }; @@ -133,7 +197,7 @@ class ExpandableCalendar extends Component { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { const {date} = this.props.context; if (date !== prevProps.context.date) { // date was changed from AgendaList, arrows or scroll @@ -141,41 +205,40 @@ class ExpandableCalendar extends Component { } } - handleScreenReaderStatus = screenReaderEnabled => { + handleScreenReaderStatus = (screenReaderEnabled: any) => { this.setState({screenReaderEnabled}); }; updateNativeStyles() { - this.wrapper?.setNativeProps(this._wrapperStyles); + this.wrapper?.current?.setNativeProps(this._wrapperStyles); + if (!this.props.horizontal) { - this.header && this.header.setNativeProps(this._headerStyles); + this.header?.current?.setNativeProps(this._headerStyles); } else { - this.weekCalendar && this.weekCalendar.setNativeProps(this._weekCalendarStyles); + this.weekCalendar?.current?.setNativeProps(this._weekCalendarStyles); } } /** Scroll */ - scrollToDate(date) { - if (this.calendar) { - if (!this.props.horizontal) { - this.calendar.scrollToDay(XDate(date), 0, true); - } else if (this.getMonth(date) !== this.visibleMonth) { - // don't scroll if the month is already visible - this.calendar.scrollToMonth(XDate(date)); - } + scrollToDate(date: Date) { + if (!this.props.horizontal) { + this.calendar?.current?.scrollToDay(new XDate(date), 0, true); + } else if (this.getMonth(date) !== this.visibleMonth) { + // don't scroll if the month is already visible + this.calendar?.current?.scrollToMonth(new XDate(date)); } } - scrollPage(next) { + scrollPage(next: boolean) { if (this.props.horizontal) { const d = parseDate(this.props.context.date); - if (this.state.position === POSITIONS.OPEN) { + if (this.state.position === Positions.OPEN) { d.setDate(1); d.addMonths(next ? 1 : -1); } else { - const {firstDay} = this.props; + const {firstDay = 0} = this.props; let dayOfTheWeek = d.getDay(); if (dayOfTheWeek < firstDay && firstDay > 0) { dayOfTheWeek = 7 + dayOfTheWeek; @@ -183,7 +246,7 @@ class ExpandableCalendar extends Component { const firstDayOfWeek = (next ? 7 : -7) - dayOfTheWeek + firstDay; d.addDays(firstDayOfWeek); } - _.invoke(this.props.context, 'setDate', toMarkingFormat(d), UPDATE_SOURCES.PAGE_SCROLL); + _.invoke(this.props.context, 'setDate', toMarkingFormat(d), updateSources.PAGE_SCROLL); } } @@ -195,18 +258,18 @@ class ExpandableCalendar extends Component { return CLOSED_HEIGHT + WEEK_HEIGHT * (this.numberOfWeeks - 1) + (this.props.hideKnob ? 12 : KNOB_CONTAINER_HEIGHT); } - getYear(date) { - const d = XDate(date); + getYear(date: Date) { + const d = new XDate(date); return d.getFullYear(); } - getMonth(date) { - const d = XDate(date); + getMonth(date: Date) { + const d = new XDate(date); // getMonth() returns the month of the year (0-11). Value is zero-index, meaning Jan=0, Feb=1, Mar=2, etc. return d.getMonth() + 1; } - getNumberOfWeeksInMonth(month) { + getNumberOfWeeksInMonth(month: XDate) { const days = page(month, this.props.firstDay); return days.length / 7; } @@ -218,36 +281,38 @@ class ExpandableCalendar extends Component { return this.props.hideArrows || false; } - isLaterDate(date1, date2) { - if (date1.year > this.getYear(date2)) { - return true; - } - if (date1.year === this.getYear(date2)) { - if (date1.month > this.getMonth(date2)) { + isLaterDate(date1?: DateData, date2?: Date) { + if (date1 && date2) { + if (date1.year > this.getYear(date2)) { return true; } + if (date1.year === this.getYear(date2)) { + if (date1.month > this.getMonth(date2)) { + return true; + } + } } return false; } /** Pan Gesture */ - handleMoveShouldSetPanResponder = (e, gestureState) => { + handleMoveShouldSetPanResponder = (_: GestureResponderEvent, gestureState: PanResponderGestureState) => { if (this.props.disablePan) { return false; } - if (!this.props.horizontal && this.state.position === POSITIONS.OPEN) { + if (!this.props.horizontal && this.state.position === Positions.OPEN) { // disable pan detection when vertical calendar is open to allow calendar scroll return false; } - if (this.state.position === POSITIONS.CLOSED && gestureState.dy < 0) { + if (this.state.position === Positions.CLOSED && gestureState.dy < 0) { // disable pan detection to limit to closed height return false; } return gestureState.dy > 5 || gestureState.dy < -5; }; handlePanResponderGrant = () => {}; - handlePanResponderMove = (e, gestureState) => { + handlePanResponderMove = (_: GestureResponderEvent, gestureState: PanResponderGestureState) => { // limit min height to closed height this._wrapperStyles.style.height = Math.max(this.closedHeight, this._height + gestureState.dy); @@ -256,7 +321,7 @@ class ExpandableCalendar extends Component { this._headerStyles.style.top = Math.min(Math.max(-gestureState.dy, -HEADER_HEIGHT), 0); } else { // horizontal Week view - if (this.state.position === POSITIONS.CLOSED) { + if (this.state.position === Positions.CLOSED) { this._weekCalendarStyles.style.opacity = Math.min(1, Math.max(1 - gestureState.dy / 100, 0)); } } @@ -264,18 +329,18 @@ class ExpandableCalendar extends Component { this.updateNativeStyles(); }; handlePanResponderEnd = () => { - this._height = this._wrapperStyles.style.height; + this._height = Number(this._wrapperStyles.style.height); this.bounceToPosition(); }; /** Animated */ - bounceToPosition(toValue) { + bounceToPosition(toValue = 0) { if (!this.props.disablePan) { const {deltaY, position} = this.state; - const {openThreshold, closeThreshold} = this.props; + const {openThreshold = PAN_GESTURE_THRESHOLD, closeThreshold = PAN_GESTURE_THRESHOLD} = this.props; const threshold = - position === POSITIONS.OPEN ? this.openHeight - closeThreshold : this.closedHeight + openThreshold; + position === Positions.OPEN ? this.openHeight - closeThreshold : this.closedHeight + openThreshold; let isOpen = this._height >= threshold; const newValue = isOpen ? this.openHeight : this.closedHeight; @@ -299,26 +364,26 @@ class ExpandableCalendar extends Component { } } - onAnimatedFinished = ({finished}) => { - if (finished) { + onAnimatedFinished = (result: {finished: boolean}) => { + if (result?.finished) { // this.setPosition(); } }; setPosition() { const isClosed = this._height === this.closedHeight; - this.setState({position: isClosed ? POSITIONS.CLOSED : POSITIONS.OPEN}); + this.setState({position: isClosed ? Positions.CLOSED : Positions.OPEN}); } - resetWeekCalendarOpacity(isOpen) { + resetWeekCalendarOpacity(isOpen: boolean) { this._weekCalendarStyles.style.opacity = isOpen ? 0 : 1; this.updateNativeStyles(); } - closeHeader(isOpen) { + closeHeader(isOpen: boolean) { const {headerDeltaY} = this.state; - headerDeltaY.setValue(this._headerStyles.style.top); // set the start position for the animated value + headerDeltaY.setValue(Number(this._headerStyles.style.top)); // set the start position for the animated value if (!this.props.horizontal && !isOpen) { Animated.spring(headerDeltaY, { @@ -342,13 +407,13 @@ class ExpandableCalendar extends Component { this.scrollPage(true); }; - onDayPress = value => { + onDayPress = (value: DateData) => { // {year: 2019, month: 4, day: 22, timestamp: 1555977600000, dateString: "2019-04-23"} - _.invoke(this.props.context, 'setDate', value.dateString, UPDATE_SOURCES.DAY_PRESS); + _.invoke(this.props.context, 'setDate', value.dateString, updateSources.DAY_PRESS); setTimeout(() => { // to allows setDate to be completed - if (this.state.position === POSITIONS.OPEN) { + if (this.state.position === Positions.OPEN) { this.bounceToPosition(this.closedHeight); } }, 0); @@ -358,15 +423,14 @@ class ExpandableCalendar extends Component { } }; - onVisibleMonthsChange = value => { - const month = _.first(value) && _.first(value).month; - if (month && this.visibleMonth !== month) { - this.visibleMonth = month; // equivalent to this.getMonth(value[0].dateString) + onVisibleMonthsChange = (value: DateData[]) => { + if (this.visibleMonth !== _.first(value)?.month) { + this.visibleMonth = _.first(value)?.month; // equivalent to this.getMonth(value[0].dateString) // for horizontal scroll const {date, updateSource} = this.props.context; - if (this.visibleMonth !== this.getMonth(date) && updateSource !== UPDATE_SOURCES.DAY_PRESS) { + if (this.visibleMonth !== this.getMonth(date) && updateSource !== updateSources.DAY_PRESS) { const next = this.isLaterDate(_.first(value), date); this.scrollPage(next); } @@ -378,7 +442,7 @@ class ExpandableCalendar extends Component { if (numberOfWeeks !== this.numberOfWeeks) { this.numberOfWeeks = numberOfWeeks; this.openHeight = this.getOpenHeight(); - if (this.state.position === POSITIONS.OPEN) { + if (this.state.position === Positions.OPEN) { this.bounceToPosition(this.openHeight); } } @@ -400,7 +464,7 @@ class ExpandableCalendar extends Component { renderWeekDaysNames = memoize((weekDaysNames, calendarStyle) => { return ( - {weekDaysNames.map((day, index) => ( + {weekDaysNames.map((day: string, index: number) => ( {day} @@ -410,12 +474,12 @@ class ExpandableCalendar extends Component { }); renderHeader() { - const monthYear = XDate(this.props.context.date).toString('MMMM yyyy'); + const monthYear = new XDate(this.props.context.date).toString('MMMM yyyy'); const weekDaysNames = weekDayNames(this.props.firstDay); return ( (this.header = e)} + ref={this.header} style={[this.style.header, {height: HEADER_HEIGHT, top: this.state.headerDeltaY}]} pointerEvents={'none'} > @@ -431,19 +495,20 @@ class ExpandableCalendar extends Component { const {position} = this.state; const {disableWeekScroll} = this.props; const WeekComponent = disableWeekScroll ? Week : WeekCalendar; - + const weekCalendarProps = disableWeekScroll ? undefined : {allowShadow: false}; + return ( (this.weekCalendar = e)} - style={[this.style.weekContainer, position === POSITIONS.OPEN ? this.style.hidden : this.style.visible]} - pointerEvents={position === POSITIONS.CLOSED ? 'auto' : 'none'} + ref={this.weekCalendar} + style={[this.style.weekContainer, position === Positions.OPEN ? this.style.hidden : this.style.visible]} + pointerEvents={position === Positions.CLOSED ? 'auto' : 'none'} > { - if (_.isFunction(this.props.renderArrow)) { - return this.props.renderArrow(direction); + renderArrow = (direction: Direction) => { + const {renderArrow, rightArrowImageSource = RIGHT_ARROW, leftArrowImageSource = LEFT_ARROW, testID} = this.props; + + if (_.isFunction(renderArrow)) { + return renderArrow(direction); } return ( ); }; @@ -478,7 +545,7 @@ class ExpandableCalendar extends Component { render() { const {style, hideKnob, horizontal, allowShadow, theme, ...others} = this.props; const {deltaY, position, screenReaderEnabled} = this.state; - const isOpen = position === POSITIONS.OPEN; + const isOpen = position === Positions.OPEN; const themeObject = Object.assign(this.headerStyleOverride, theme); return ( @@ -493,13 +560,13 @@ class ExpandableCalendar extends Component { renderArrow={this.renderArrow} /> ) : ( - (this.wrapper = e)} style={{height: deltaY}} {...this.panResponder.panHandlers}> + (this.calendar = r)} + ref={this.calendar} current={this.initialDate} onDayPress={this.onDayPress} onVisibleMonthsChange={this.onVisibleMonthsChange} diff --git a/src/expandableCalendar/style.js b/src/expandableCalendar/style.ts similarity index 96% rename from src/expandableCalendar/style.js rename to src/expandableCalendar/style.ts index 2e13abf923..4b6aa65dce 100644 --- a/src/expandableCalendar/style.js +++ b/src/expandableCalendar/style.ts @@ -1,11 +1,12 @@ import {StyleSheet, Platform} from 'react-native'; import * as defaultStyle from '../style'; +import {Theme} from '../types'; + const commons = require('./commons'); -const STYLESHEET_ID = 'stylesheet.expandable.main'; export const HEADER_HEIGHT = 68; -export default function styleConstructor(theme = {}) { +export default function styleConstructor(theme: Theme = {}) { const appStyle = {...defaultStyle, ...theme}; return StyleSheet.create({ @@ -170,6 +171,6 @@ export default function styleConstructor(theme = {}) { marginLeft: appStyle.todayButtonPosition === 'right' ? 7 : undefined, marginRight: appStyle.todayButtonPosition === 'right' ? undefined : 7 }, - ...(theme[STYLESHEET_ID] || {}) + ...(theme?.stylesheet?.expandable?.main || {}) }); } diff --git a/src/expandableCalendar/week.js b/src/expandableCalendar/week.tsx similarity index 81% rename from src/expandableCalendar/week.js rename to src/expandableCalendar/week.tsx index 4b669aed3b..be5a697992 100644 --- a/src/expandableCalendar/week.js +++ b/src/expandableCalendar/week.tsx @@ -3,31 +3,37 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {View} from 'react-native'; +// @ts-expect-error import {getWeekDates, sameMonth} from '../dateutils'; +// @ts-expect-error import {parseDate, toMarkingFormat} from '../interface'; +// @ts-expect-error import {getState} from '../day-state-manager'; +// @ts-expect-error import {extractComponentProps} from '../component-updater'; import styleConstructor from './style'; -import Calendar from '../calendar'; +import Calendar, {CalendarProps} from '../calendar'; import Day from '../calendar/day/index'; // import BasicDay from '../calendar/day/basic'; -class Week extends PureComponent { + +interface Props extends CalendarProps { + current: XDate +} +export type WeekProps = Props; + + +class Week extends PureComponent { static displayName = 'IGNORE'; static propTypes = { ...Calendar.propTypes, - /** the current date */ current: PropTypes.any }; - constructor(props) { - super(props); - - this.style = styleConstructor(props.theme); - } + style = styleConstructor(this.props.theme); - getWeek(date) { + getWeek(date: XDate) { return getWeekDates(date, this.props.firstDay); } @@ -35,7 +41,7 @@ class Week extends PureComponent { // return {weekNumber}; // } - renderDay(day, id) { + renderDay(day: XDate, id: number) { const {current, hideExtraDays, markedDates} = this.props; const dayProps = extractComponentProps(Day, this.props); @@ -63,10 +69,10 @@ class Week extends PureComponent { render() { const {current} = this.props; const dates = this.getWeek(current); - const week = []; + const week: any[] = []; if (dates) { - dates.forEach((day, id) => { + dates.forEach((day: XDate, id: number) => { week.push(this.renderDay(day, id)); }, this); } diff --git a/src/types.ts b/src/types.ts index a1cf01d86b..fb54c6167b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ import {ColorValue, ViewStyle, TextStyle} from 'react-native'; -// @ts-expect-error import {UPDATE_SOURCES} from './expandableCalendar/commons';