From 5e35de1c0ac37ad6147fb5e9db34c96cd5eccf0d Mon Sep 17 00:00:00 2001 From: Farhoud Shapouran Date: Fri, 26 Jan 2024 17:43:08 +0300 Subject: [PATCH 1/6] feat: add range mode --- .eslintrc.json | 4 + README.md | 8 +- example/src/App.tsx | 207 ++++++++++++++++---- package.json | 2 +- src/CalendarContext.tsx | 17 +- src/DateTimePicker.tsx | 281 ++++++++++++++++------------ src/__tests__/api.test.tsx | 10 +- src/__tests__/common.test.tsx | 2 +- src/components/Calendar.tsx | 7 +- src/components/Day.tsx | 59 +++++- src/components/DaySelector.tsx | 99 ++++++++-- src/components/Header.tsx | 11 +- src/components/TimePicker/Wheel.tsx | 14 +- src/components/TimeSelector.tsx | 5 +- src/components/YearSelector.tsx | 4 +- src/enums.ts | 1 + src/index.tsx | 4 +- src/types.ts | 46 ++++- src/utils.ts | 163 +++++++++++++--- 19 files changed, 702 insertions(+), 242 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..4aa47dd --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "@react-native-community", + "ignorePatterns": [".*.js", ".*.ts", "node_modules/", "lib/", "example/"] +} diff --git a/README.md b/README.md index 760d3ef..f4217d4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ export default function App() { setValue(date)} + onChange={(date) => setValue(date)} /> ) @@ -61,11 +61,11 @@ For more, take a look at the `/example` directory. | Name | Type | Default | Description | | ------------------------ | --------------- | --------------- | -------------------------------------------------------------------------------------- | | value | `DateType` | `Dayjs` | DatePicker value to display selected date | -| onValueChange | `func` | `() => {}` | Called when the new date selected from DatePicker | +| onChange | `Function` | `() => {}` | Called when the new date selected from DatePicker | | mode | `string` | `'datetime'` | Defines the DatePicker mode `['datetime', 'date', 'time']` | | locale | `string` | `'en'` | Defines the DatePicker locale | -| minimumDate | `DateType` | `null` | Defines DatePicker minimum selectable date | -| maximumDate | `DateType` | `null` | Defines DatePicker maximum selectable date | +| minDate | `DateType` | `null` | Defines DatePicker minimum selectable date | +| maxDate | `DateType` | `null` | Defines DatePicker maximum selectable date | | firstDayOfWeek | `number` | `0` | Defines the starting day of week, number 0-6, 0 - Sunday, 6 - Saturday | | displayFullDays | `boolean` | `false` | Defines show previous and next month's days in the current calendar view | | calendarTextStyle | `TextStyle` | `null` | Defines all text styles inside the calendar (Days, Months, Years, Hours, and Minutes) | diff --git a/example/src/App.tsx b/example/src/App.tsx index 2a8510f..7d819b6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { StyleSheet, View, @@ -7,8 +7,9 @@ import { Image, Linking, SafeAreaView, + TouchableOpacity, } from 'react-native'; -import DateTimePicker, { DateType } from 'react-native-ui-datepicker'; +import DateTimePicker, { DateType, ModeType } from 'react-native-ui-datepicker'; import dayjs from 'dayjs'; import 'dayjs/locale/en'; import 'dayjs/locale/de'; @@ -35,10 +36,51 @@ const Themes: ITheme[] = [ const Locales = ['en', 'de', 'es', 'fr', 'tr']; export default function App() { - const [value, setValue] = useState(dayjs()); + const [mode, setMode] = useState('single'); + + const [date, setDate] = useState(); + const [range, setRange] = React.useState<{ + startDate: DateType; + endDate: DateType; + }>({ startDate: undefined, endDate: undefined }); + const [theme, setTheme] = useState(Themes[0]); const [locale, setLocale] = useState('en'); + const onChangeSingle = useCallback( + (params: any) => { + setDate(params.date); + }, + [setDate] + ); + + const onChaneRange = useCallback( + ({ startDate, endDate }: any) => { + setRange({ startDate, endDate }); + }, + [setRange] + ); + + const onChangeMode = useCallback( + (value: ModeType) => { + setDate(undefined); + setRange({ startDate: undefined, endDate: undefined }); + setMode(value); + }, + [setMode, setDate, setRange] + ); + + const onChange = useCallback( + (params) => { + if (mode === 'single') { + onChangeSingle(params); + } else if (mode === 'range') { + onChaneRange(params); + } + }, + [mode] + ); + return ( @@ -91,16 +133,69 @@ export default function App() { ))} + + + Modes: + + onChangeMode('single')} + > + + Single + + + onChangeMode('range')} + > + + Range + + + setValue(date)} + onChange={onChange} headerButtonColor={theme?.mainColor} selectedItemColor={theme?.mainColor} // eslint-disable-next-line react-native/no-inline-styles @@ -112,35 +207,61 @@ export default function App() { todayContainerStyle={{ borderWidth: 1, }} - mode="datetime" /> - - - {dayjs(value).locale(locale).format('MMMM, DD, YYYY - HH:mm')} - - { - setValue(dayjs()); - }} - accessibilityRole="button" - accessibilityLabel="Today" - > - - + {mode === 'single' ? ( + + + {dayjs(date) + .locale(locale) + .format('MMMM, DD, YYYY - HH:mm')} + + onChangeSingle({ date: dayjs() })} + accessibilityRole="button" + accessibilityLabel="Today" > - Today + + + Today + + + + + ) : mode === 'range' ? ( + + + + Start Date: + + {range.startDate + ? dayjs(range.startDate) + .locale(locale) + .format('MMMM, DD, YYYY') + : '...'} + + + + End Date: + + {range.endDate + ? dayjs(range.endDate) + .locale(locale) + .format('MMMM, DD, YYYY') + : '...'} - + ) : null} @@ -219,6 +340,21 @@ const styles = StyleSheet.create({ localeButtonText: { fontSize: 15, }, + modesContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginBottom: 20, + }, + modeSelect: { + paddingHorizontal: 10, + paddingVertical: 8, + borderRadius: 8, + }, + modeSelectText: { + fontSize: 13, + fontWeight: 'bold', + }, datePickerContainer: { alignItems: 'center', }, @@ -232,12 +368,15 @@ const styles = StyleSheet.create({ shadowOpacity: 0.1, shadowOffset: { width: 0, height: 0 }, }, + footer: { + paddingHorizontal: 5, + paddingVertical: 5, + marginTop: 15, + }, footerContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 5, - paddingVertical: 5, }, todayButton: { paddingHorizontal: 16, diff --git a/package.json b/package.json index f2bbff5..ef29f19 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "scripts": { "test": "jest -u", "typecheck": "tsc --noEmit", - "lint": "eslint \"**/*.{js,ts,jsx,tsx}\"", + "lint": "eslint .", "prepack": "bob build", "release": "release-it", "example": "yarn --cwd example", diff --git a/src/CalendarContext.tsx b/src/CalendarContext.tsx index c89fe40..4f10dd8 100644 --- a/src/CalendarContext.tsx +++ b/src/CalendarContext.tsx @@ -2,19 +2,20 @@ import { createContext, useContext } from 'react'; import { CalendarViews } from './enums'; import type { DateType, - CalendarTheme, - CalendarModes, - CalendarState, + DatePickerBaseProps, + CalendarThemeProps, } from './types'; -export interface CalendarContextType extends CalendarState { - mode: CalendarModes; +export interface CalendarContextType extends DatePickerBaseProps { locale: string | ILocale; displayFullDays: boolean; - minimumDate: DateType; - maximumDate: DateType; firstDayOfWeek: number; - theme?: CalendarTheme; + theme: CalendarThemeProps; + calendarView: CalendarViews; + // selectedDate: DateType; + // selectedDates: DateType[]; + currentDate: DateType; // used for latest state of calendar based on Month and Year + currentYear: number; setCalendarView: (value: CalendarViews) => void; onSelectDate: (date: DateType) => void; onSelectMonth: (month: number) => void; diff --git a/src/DateTimePicker.tsx b/src/DateTimePicker.tsx index 5a55cf2..c26808b 100644 --- a/src/DateTimePicker.tsx +++ b/src/DateTimePicker.tsx @@ -1,13 +1,19 @@ -import React, { useEffect, useReducer } from 'react'; -import { getFormated, getDate, getDateYear } from './utils'; +import React, { memo, useCallback, useEffect, useReducer } from 'react'; +import { + getFormated, + getDate, + dateToUnix, + getEndOfDay, + getStartOfDay, +} from './utils'; import CalendarContext from './CalendarContext'; import { CalendarViews, CalendarActionKind } from './enums'; import type { DateType, - CalendarModes, CalendarAction, - CalendarState, - CalendarTheme, + LocalState, + DatePickerBaseProps, + CalendarThemeProps, HeaderProps, } from './types'; import Calendar from './components/Calendar'; @@ -15,79 +21,71 @@ import dayjs from 'dayjs'; import localeData from 'dayjs/plugin/localeData'; import relativeTime from 'dayjs/plugin/relativeTime'; import localizedFormat from 'dayjs/plugin/localizedFormat'; +import { SingleChange, RangeChange, MultiChange } from './types'; dayjs.extend(localeData); dayjs.extend(relativeTime); dayjs.extend(localizedFormat); -interface PropTypes extends CalendarTheme, HeaderProps { - value: DateType; - mode?: CalendarModes; - locale?: string | ILocale; - minimumDate?: DateType; - maximumDate?: DateType; - firstDayOfWeek?: number; - onValueChange?: (value: DateType) => void; - displayFullDays?: boolean; +export interface DatePickerSingleProps + extends CalendarThemeProps, + HeaderProps, + DatePickerBaseProps { + mode: 'single'; + date?: DateType; + onChange?: SingleChange; } -const DateTimePicker = ({ - value, - mode = 'datetime', - locale = 'en', - minimumDate = null, - maximumDate = null, - firstDayOfWeek = 0, - onValueChange = () => {}, - displayFullDays = false, - headerButtonsPosition = 'around', - headerContainerStyle, - headerTextContainerStyle, - headerTextStyle, - headerButtonStyle, - headerButtonColor, - headerButtonSize, - dayContainerStyle, - todayContainerStyle, - todayTextStyle, - monthContainerStyle, - yearContainerStyle, - weekDaysContainerStyle, - weekDaysTextStyle, - calendarTextStyle, - selectedTextStyle, - selectedItemColor, - timePickerContainerStyle, - timePickerTextStyle, - buttonPrevIcon, - buttonNextIcon, -}: Partial) => { - dayjs.locale(locale); +export interface DatePickerRangeProps + extends CalendarThemeProps, + HeaderProps, + DatePickerBaseProps { + mode: 'range'; + startDate?: DateType; + endDate?: DateType; + onChange?: RangeChange; +} + +export interface DatePickeMultipleProps + extends CalendarThemeProps, + HeaderProps, + DatePickerBaseProps { + mode: 'multiple'; + dates?: DateType[]; + onChange?: MultiChange; +} + +const DateTimePicker = ( + props: DatePickerSingleProps | DatePickerRangeProps | DatePickeMultipleProps +) => { + const { + mode, + locale = 'en', + displayFullDays = false, + firstDayOfWeek, + buttonPrevIcon, + buttonNextIcon, + // startYear, + // endYear, + minDate, + maxDate, + date, + startDate, + endDate, + // dates, + onChange, + ...rest + } = props; - const theme = { - headerButtonsPosition, - headerContainerStyle, - headerTextContainerStyle, - headerTextStyle, - headerButtonStyle, - headerButtonColor, - headerButtonSize, - dayContainerStyle, - todayContainerStyle, - todayTextStyle, - monthContainerStyle, - yearContainerStyle, - weekDaysContainerStyle, - weekDaysTextStyle, - calendarTextStyle, - selectedTextStyle, - selectedItemColor, - timePickerContainerStyle, - timePickerTextStyle, - }; + const firstDay = + firstDayOfWeek && firstDayOfWeek > 0 && firstDayOfWeek <= 6 + ? firstDayOfWeek + : 0; + + dayjs.locale(locale); const [state, dispatch] = useReducer( - (prevState: CalendarState, action: CalendarAction) => { + (prevState: LocalState, action: CalendarAction) => { switch (action.type) { case CalendarActionKind.SET_CALENDAR_VIEW: return { @@ -105,57 +103,86 @@ const DateTimePicker = ({ currentYear: action.payload, }; case CalendarActionKind.CHANGE_SELECTED_DATE: + const { date } = action.payload; return { ...prevState, - selectedDate: action.payload, + date, + currentDate: date, + }; + case CalendarActionKind.CHANGE_SELECTED_RANGE: + const { startDate, endDate } = action.payload; + return { + ...prevState, + startDate, + endDate, }; } }, { - calendarView: mode === 'time' ? CalendarViews.time : CalendarViews.day, - selectedDate: value ? getFormated(value) : new Date(), - currentDate: value ? getFormated(value) : new Date(), - currentYear: value ? getDateYear(value) : new Date().getFullYear(), + date: undefined, + startDate: undefined, + endDate: undefined, + dates: [], + calendarView: CalendarViews.day, + currentDate: new Date(), + currentYear: new Date().getFullYear(), } ); useEffect(() => { - dispatch({ - type: CalendarActionKind.CHANGE_SELECTED_DATE, - payload: value ? getFormated(value) : new Date(), - }); - dispatch({ - type: CalendarActionKind.CHANGE_CURRENT_DATE, - payload: value ? getFormated(value) : new Date(), - }); - dispatch({ - type: CalendarActionKind.CHANGE_CURRENT_YEAR, - payload: getDateYear(value), - }); - }, [value]); - - useEffect(() => { - dispatch({ - type: CalendarActionKind.SET_CALENDAR_VIEW, - payload: mode === 'time' ? CalendarViews.time : CalendarViews.day, - }); - }, [mode]); - - const actions = { - setCalendarView: (view: CalendarViews) => - dispatch({ type: CalendarActionKind.SET_CALENDAR_VIEW, payload: view }), - onSelectDate: (date: DateType) => { - onValueChange(date); + if (mode === 'single') { dispatch({ type: CalendarActionKind.CHANGE_SELECTED_DATE, - payload: date, + payload: { date }, }); + } else if (mode === 'range') { dispatch({ - type: CalendarActionKind.CHANGE_CURRENT_DATE, - payload: date, + type: CalendarActionKind.CHANGE_SELECTED_RANGE, + payload: { startDate, endDate }, }); + } + }, [mode, date, startDate, endDate]); + + const setCalendarView = useCallback((view: CalendarViews) => { + dispatch({ type: CalendarActionKind.SET_CALENDAR_VIEW, payload: view }); + }, []); + + const onSelectDate = useCallback( + (date: DateType) => { + if (onChange) { + dispatch({ + type: CalendarActionKind.CHANGE_CURRENT_DATE, + payload: date, + }); + if (mode === 'single') { + (onChange as SingleChange)({ + date, + }); + } else if (mode === 'range') { + const sd = state.startDate; + const ed = state.endDate; + let isStart: boolean = true; + + if (sd && !ed && dateToUnix(date) >= dateToUnix(sd!)) { + isStart = false; + } + + (onChange as RangeChange)({ + startDate: isStart ? getStartOfDay(date) : sd, + endDate: !isStart ? getEndOfDay(date) : undefined, + }); + } else if (mode === 'multiple') { + // (onChange as MultiChange)({ + // dates: [date], + // }); + } + } }, - onSelectMonth: (month: number) => { + [onChange, mode, state.startDate, state.endDate] + ); + + const onSelectMonth = useCallback( + (month: number) => { const newDate = getDate(state.currentDate).month(month); dispatch({ type: CalendarActionKind.CHANGE_CURRENT_DATE, @@ -166,7 +193,11 @@ const DateTimePicker = ({ payload: CalendarViews.day, }); }, - onSelectYear: (year: number) => { + [state.currentDate] + ); + + const onSelectYear = useCallback( + (year: number) => { const newDate = getDate(state.currentDate).year(year); dispatch({ type: CalendarActionKind.CHANGE_CURRENT_DATE, @@ -177,34 +208,46 @@ const DateTimePicker = ({ payload: CalendarViews.day, }); }, - onChangeMonth: (month: number) => { + [state.currentDate] + ); + + const onChangeMonth = useCallback( + (month: number) => { const newDate = getDate(state.currentDate).add(month, 'month'); dispatch({ type: CalendarActionKind.CHANGE_CURRENT_DATE, payload: getFormated(newDate), }); }, - onChangeYear: (year: number) => { - dispatch({ - type: CalendarActionKind.CHANGE_CURRENT_YEAR, - payload: year, - }); - }, - }; + [state.currentDate] + ); + + const onChangeYear = useCallback((year: number) => { + dispatch({ + type: CalendarActionKind.CHANGE_CURRENT_YEAR, + payload: year, + }); + }, []); return ( = 0 && firstDayOfWeek <= 6 ? firstDayOfWeek : 0, - theme, + minDate, + maxDate, + firstDayOfWeek: firstDay, + theme: rest, + setCalendarView, + onSelectDate, + onSelectMonth, + onSelectYear, + onChangeMonth, + onChangeYear, }} > { const selectedDate = new Date(); const month = selectedDate.toLocaleString('en-US', { month: 'long' }); - render(); + render(); expect(screen.getByText(month)).toBeVisible(); }); - // test('minimumDate should be applied after init', () => { - // const minimumDate = new Date(); + // test('minDate should be applied after init', () => { + // const minDate = new Date(); - // render(); + // render(); // expect( - // screen.getByTestId(dayjs(minimumDate).add(-1, 'day').format('YYYY/MM/DD')) + // screen.getByTestId(dayjs(minDate).add(-1, 'day').format('YYYY/MM/DD')) // ).toBeDisabled(); // }); }); diff --git a/src/__tests__/common.test.tsx b/src/__tests__/common.test.tsx index ce7ad8a..b7c2aee 100644 --- a/src/__tests__/common.test.tsx +++ b/src/__tests__/common.test.tsx @@ -4,7 +4,7 @@ import DateTimePicker from '../DateTimePicker'; describe('COMMON TESTS', () => { test('should render with default options', () => { - render(); + render(); expect(screen.toJSON()).toMatchSnapshot(); }); }); diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 2e61a2c..9455909 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -20,16 +20,17 @@ const CalendarView: Record = { interface PropTypes extends HeaderProps {} const Calendar = ({ buttonPrevIcon, buttonNextIcon }: PropTypes) => { - const { calendarView, mode } = useCalendarContext(); + const { calendarView } = useCalendarContext(); return ( - {mode !== 'time' ? ( + {/* {mode !== 'time' ? (
- ) : null} + ) : null} */} +
{CalendarView[calendarView]} ); diff --git a/src/components/Day.tsx b/src/components/Day.tsx index b421da1..c35aa8b 100644 --- a/src/components/Day.tsx +++ b/src/components/Day.tsx @@ -1,13 +1,16 @@ import React from 'react'; import { View, Text, Pressable, StyleSheet } from 'react-native'; -import { CalendarTheme, IDayObject } from '../types'; +import { CalendarThemeProps, IDayObject } from '../types'; import { CALENDAR_HEIGHT } from '../enums'; +import { addColorAlpha } from '../utils'; + +export const daySize = 46; interface Props extends Omit { isToday: boolean; isSelected: boolean; onSelectDate: (date: string) => void; - theme?: CalendarTheme; + theme?: CalendarThemeProps; } function EmptyDayPure() { @@ -23,9 +26,15 @@ const Day = ({ isCurrentMonth, isToday, isSelected, + inRange, + leftCrop, + rightCrop, onSelectDate, theme, }: Props) => { + //const bothWays = inRange && leftCrop && rightCrop; + const isCrop = inRange && (leftCrop || rightCrop) && !(leftCrop && rightCrop); + const dayContainerStyle = isCurrentMonth ? theme?.dayContainerStyle : { opacity: 0.3 }; @@ -55,8 +64,40 @@ const Day = ({ } : theme?.calendarTextStyle; + const rangeRootBackground = addColorAlpha(theme?.selectedItemColor, 0.15); + return ( + {inRange && !isCrop ? ( + + ) : null} + + {isCrop && leftCrop ? ( + + ) : null} + + {isCrop && rightCrop ? ( + + ) : null} + onSelectDate(date)} @@ -71,8 +112,8 @@ const Day = ({ accessibilityRole="button" accessibilityLabel={text} > - - {text} + + {text} @@ -81,8 +122,9 @@ const Day = ({ const styles = StyleSheet.create({ dayCell: { + position: 'relative', width: '14.2%', - height: CALENDAR_HEIGHT / 7 - 1, + height: CALENDAR_HEIGHT / 7, }, dayContainer: { flex: 1, @@ -98,6 +140,13 @@ const styles = StyleSheet.create({ disabledDay: { opacity: 0.3, }, + rangeRoot: { + position: 'absolute', + left: 0, + right: 0, + top: 2, + bottom: 2, + }, }); export default React.memo(Day); diff --git a/src/components/DaySelector.tsx b/src/components/DaySelector.tsx index 02c3db6..cd974ff 100644 --- a/src/components/DaySelector.tsx +++ b/src/components/DaySelector.tsx @@ -6,19 +6,24 @@ import { getParsedDate, getMonthDays, getWeekdaysMin, + getDaysInMonth, areDatesOnSameDay, + isDateBetween, getDate, getFormated, } from '../utils'; const DaySelector = () => { const { + mode, + startDate, + endDate, currentDate, - selectedDate, + date, onSelectDate, displayFullDays, - minimumDate, - maximumDate, + minDate, + maxDate, firstDayOfWeek, theme, } = useCalendarContext(); @@ -27,24 +32,89 @@ const DaySelector = () => { const daysGrid = useMemo( () => { const today = new Date(); + + const { fullDaysInMonth } = getDaysInMonth( + currentDate, + displayFullDays, + firstDayOfWeek + ); + return getMonthDays( currentDate, displayFullDays, - minimumDate, - maximumDate, + minDate, + maxDate, firstDayOfWeek - ).map((day) => { - return day - ? { - ...day, - isToday: areDatesOnSameDay(day.date, today), - isSelected: areDatesOnSameDay(day.date, selectedDate), + ).map((day, index) => { + if (day) { + let leftCrop = day.dayOfMonth === 1; + let rightCrop = day.dayOfMonth === fullDaysInMonth; + + const isFirstDayOfMonth = day.dayOfMonth === 1; + const isLastDayOfMonth = day.dayOfMonth === fullDaysInMonth; + + const isToday = areDatesOnSameDay(day.date, today); + let inRange = false; + let isSelected = false; + + if (mode === 'range') { + const selectedStartDay = areDatesOnSameDay(day.date, startDate); + const selectedEndDay = areDatesOnSameDay(day.date, endDate); + isSelected = selectedStartDay || selectedEndDay; + inRange = isDateBetween(day.date, { + startDate, + endDate, + }); + if (selectedStartDay) { + leftCrop = true; } - : null; + if (selectedEndDay) { + rightCrop = true; + } + if (index % 7 === 0 && !selectedStartDay) { + leftCrop = false; + } + + if (index % 7 === 6 && !selectedEndDay) { + rightCrop = false; + } + + if ( + (isFirstDayOfMonth && selectedEndDay) || + (isLastDayOfMonth && selectedStartDay) + ) { + inRange = false; + } + } else if (mode === 'single') { + isSelected = areDatesOnSameDay(day.date, date); + } + + return { + ...day, + isToday, + isSelected, + inRange, + leftCrop, + rightCrop, + }; + } else { + return null; + } }); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [month, year, displayFullDays, minimumDate, maximumDate, selectedDate] + [ + mode, + month, + year, + displayFullDays, + firstDayOfWeek, + minDate, + maxDate, + date, + startDate, + endDate, + ] ); const handleSelectDate = useCallback( @@ -80,6 +150,9 @@ const DaySelector = () => { theme={theme} isToday={day.isToday} isSelected={day.isSelected} + inRange={day.inRange} + leftCrop={day.leftCrop} + rightCrop={day.rightCrop} onSelectDate={handleSelectDate} /> ) : ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b5ccada..9a8478e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -11,20 +11,21 @@ const arrow_right = require('../assets/images/arrow_right.png'); const Header = ({ buttonPrevIcon, buttonNextIcon }: HeaderProps) => { const { + date, currentDate, - selectedDate, currentYear, onChangeMonth, onChangeYear, calendarView, setCalendarView, - mode, theme, locale, } = useCalendarContext(); const currentMonthText = dayjs(currentDate).locale(locale).format('MMMM'); + const displayTime = false; + const renderPrevButton = ( { {calendarView !== CalendarViews.year ? monthSelector : null} {yearSelector()} - {mode === 'datetime' && calendarView !== CalendarViews.year ? ( + {displayTime && calendarView !== CalendarViews.year ? ( setCalendarView( @@ -160,11 +161,11 @@ const Header = ({ buttonPrevIcon, buttonNextIcon }: HeaderProps) => { ) } accessibilityRole="button" - accessibilityLabel={dayjs(selectedDate).format('HH:mm')} + accessibilityLabel={dayjs(date).format('HH:mm')} > - {dayjs(selectedDate).format('HH:mm')} + {dayjs(date).format('HH:mm')} diff --git a/src/components/TimePicker/Wheel.tsx b/src/components/TimePicker/Wheel.tsx index 4a108fd..2038252 100644 --- a/src/components/TimePicker/Wheel.tsx +++ b/src/components/TimePicker/Wheel.tsx @@ -67,18 +67,22 @@ const Wheel = ({ let newValueIndex = valueIndex - Math.round(gestureState.dy / ((radius * 2) / displayCount)); - if (circular) + if (circular) { newValueIndex = (newValueIndex + items.length) % items.length; - else { - if (newValueIndex < 0) newValueIndex = 0; - else if (newValueIndex >= items.length) + } else { + if (newValueIndex < 0) { + newValueIndex = 0; + } else if (newValueIndex >= items.length) { newValueIndex = items.length - 1; + } } const newValue = items[newValueIndex] || 0; if (newValue === value) { translateY.setOffset(0); translateY.setValue(0); - } else setValue(newValue); + } else { + setValue(newValue); + } }, }); }, [ diff --git a/src/components/TimeSelector.tsx b/src/components/TimeSelector.tsx index fefdc40..c655b0a 100644 --- a/src/components/TimeSelector.tsx +++ b/src/components/TimeSelector.tsx @@ -10,9 +10,8 @@ function createNumberList(num: number) { } const TimeSelector = () => { - const { selectedDate, currentDate, onSelectDate, theme } = - useCalendarContext(); - const { hour, minute } = getParsedDate(selectedDate); + const { date, currentDate, onSelectDate, theme } = useCalendarContext(); + const { hour, minute } = getParsedDate(date); return ( diff --git a/src/components/YearSelector.tsx b/src/components/YearSelector.tsx index 44c447a..437df5b 100644 --- a/src/components/YearSelector.tsx +++ b/src/components/YearSelector.tsx @@ -11,9 +11,9 @@ import { useCalendarContext } from '../CalendarContext'; import { getDateYear, getYearRange } from '../utils'; const YearSelector = () => { - const { currentDate, currentYear, selectedDate, onSelectYear, theme } = + const { currentDate, currentYear, date, onSelectYear, theme } = useCalendarContext(); - const selectedYear = getDateYear(selectedDate); + const selectedYear = getDateYear(date); const generateCells = useCallback(() => { const years = getYearRange(currentYear); diff --git a/src/enums.ts b/src/enums.ts index 6a9a8bc..d9cbef8 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -10,6 +10,7 @@ export enum CalendarActionKind { CHANGE_CURRENT_DATE = 'CHANGE_CURRENT_DATE', CHANGE_CURRENT_YEAR = 'CHANGE_CURRENT_YEAR', CHANGE_SELECTED_DATE = 'CHANGE_SELECTED_DATE', + CHANGE_SELECTED_RANGE = 'CHANGE_SELECTED_RANGE', } export const CALENDAR_HEIGHT = 300; diff --git a/src/index.tsx b/src/index.tsx index e1a591c..d77a69a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,5 @@ import DateTimePicker from './DateTimePicker'; -import type { DateType } from './types'; +import type { DateType, ModeType } from './types'; -export { DateType }; +export type { DateType, ModeType }; export default DateTimePicker; diff --git a/src/types.ts b/src/types.ts index 7783b3f..fd072e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,15 +5,18 @@ import type { ReactNode } from 'react'; export type DateType = string | number | Dayjs | Date | null | undefined; -export type CalendarModes = 'datetime' | 'date' | 'time'; +export type ModeType = 'single' | 'range' | 'multiple'; export type HeaderButtonPositions = 'around' | 'right' | 'left'; -export type CalendarState = { +export type LocalState = { + date: DateType; + startDate: DateType; + endDate: DateType; + dates: DateType[]; calendarView: CalendarViews; - selectedDate: DateType; currentDate: DateType; // used for latest state of calendar based on Month and Year - currentYear: number; // used for pagination in YearSelector + currentYear: number; }; export type CalendarAction = { @@ -21,7 +24,7 @@ export type CalendarAction = { payload: any; }; -export type CalendarTheme = { +export type CalendarThemeProps = { headerButtonsPosition?: HeaderButtonPositions; headerContainerStyle?: ViewStyle; headerTextContainerStyle?: ViewStyle; @@ -54,4 +57,37 @@ export interface IDayObject { date: string; disabled: boolean; isCurrentMonth: boolean; + dayOfMonth?: number; + inRange: boolean; + leftCrop: boolean; + rightCrop: boolean; +} + +export type SingleChange = (params: { date: DateType }) => void; + +export type RangeChange = (params: { + startDate: DateType; + endDate: DateType; +}) => any; + +export type MultiChange = (params: { + dates: DateType[]; + datePressed: Date; + change: 'added' | 'removed'; +}) => any; + +export interface DatePickerBaseProps { + mode?: ModeType; + locale?: string | ILocale; + startYear?: number; + endYear?: number; + minDate?: DateType; + maxDate?: DateType; + firstDayOfWeek?: number; + displayFullDays?: boolean; + date?: DateType; + dates?: DateType[]; + startDate?: DateType; + endDate?: DateType; + onChange?: SingleChange | RangeChange | MultiChange; } diff --git a/src/utils.ts b/src/utils.ts index 22b73e3..d21d5b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -44,6 +44,23 @@ export function areDatesOnSameDay(a: DateType, b: DateType) { return date_a === date_b; } +export function isDateBetween( + date: DateType, + { + startDate, + endDate, + }: { + startDate?: DateType; + endDate?: DateType; + } +): boolean { + if (!startDate || !endDate) { + return false; + } + + return dayjs(date) <= endDate && dayjs(date) >= startDate; +} + export const getFormatedDate = (date: DateType, format: string) => dayjs(date).format(format); @@ -53,10 +70,62 @@ export const getYearRange = (year: number) => { const endYear = YEAR_PAGE_SIZE * Math.ceil(year / YEAR_PAGE_SIZE); let startYear = endYear === year ? endYear : endYear - YEAR_PAGE_SIZE; - if (startYear < 0) startYear = 0; + if (startYear < 0) { + startYear = 0; + } return Array.from({ length: YEAR_PAGE_SIZE }, (_, i) => startYear + i); }; +export function getDaysInMonth( + date: DateType, + displayFullDays: boolean | undefined, + firstDayOfWeek: number +) { + const daysInCurrentMonth = dayjs(date).daysInMonth(); + + const prevMonthDays = dayjs(date).add(-1, 'month').daysInMonth(); + const firstDay = dayjs(date).date(1 - firstDayOfWeek); + const prevMonthOffset = firstDay.day() % 7; + const daysInPrevMonth = displayFullDays ? prevMonthOffset : 0; + const monthDaysOffset = prevMonthOffset + daysInCurrentMonth; + const daysInNextMonth = displayFullDays + ? monthDaysOffset > 35 + ? 42 - monthDaysOffset + : 35 - monthDaysOffset + : 0; + + const fullDaysInMonth = + daysInPrevMonth + daysInCurrentMonth + daysInNextMonth; + + return { + prevMonthDays, + prevMonthOffset, + daysInCurrentMonth, + daysInNextMonth, + fullDaysInMonth, + }; +} + +export function getFirstDayOfMonth( + date: DateType, + firstDayOfWeek: number +): number { + const d = getDate(date); + return d.date(1 - firstDayOfWeek).day(); +} + +export function getStartOfDay(date: DateType): DateType { + return dayjs(date).startOf('day'); +} + +export function getEndOfDay(date: DateType): DateType { + return dayjs(date).endOf('day'); +} + +export function dateToUnix(date: DateType): number { + return dayjs(date).unix(); +} + /** * Get detailed date object * @@ -78,8 +147,8 @@ export const getParsedDate = (date: DateType) => { * * @param datetime - The current date that selected * @param displayFullDays - * @param minimumDate - min selectable date - * @param maximumDate - max selectable date + * @param minDate - min selectable date + * @param maxDate - max selectable date * @param firstDayOfWeek - first day of week, number 0-6, 0 – Sunday, 6 – Saturday * * @returns days array based on current date @@ -87,41 +156,57 @@ export const getParsedDate = (date: DateType) => { export const getMonthDays = ( datetime: DateType = dayjs(), displayFullDays: boolean, - minimumDate: DateType, - maximumDate: DateType, + minDate: DateType, + maxDate: DateType, firstDayOfWeek: number ): IDayObject[] => { const date = getDate(datetime); - const daysInMonth = date.daysInMonth(); - const prevMonthDays = date.add(-1, 'month').daysInMonth(); - const firstDay = date.date(1 - firstDayOfWeek); - const dayOfMonth = firstDay.day() % 7; + const { + prevMonthDays, + prevMonthOffset, + daysInCurrentMonth, + daysInNextMonth, + } = getDaysInMonth(datetime, displayFullDays, firstDayOfWeek); const prevDays = displayFullDays - ? Array.from({ length: dayOfMonth }, (_, i) => { - const day = i + (prevMonthDays - dayOfMonth + 1); + ? Array.from({ length: prevMonthOffset }, (_, index) => { + const day = index + (prevMonthDays - prevMonthOffset + 1); const thisDay = date.add(-1, 'month').date(day); - return generateDayObject(day, thisDay, minimumDate, maximumDate, false); + return generateDayObject( + day, + thisDay, + minDate, + maxDate, + false, + index + 1 + ); }) - : Array(dayOfMonth).fill(null); + : Array(prevMonthOffset).fill(null); - const monthDaysOffset = dayOfMonth + daysInMonth; - const nextMonthDays = displayFullDays - ? monthDaysOffset > 35 - ? 42 - monthDaysOffset - : 35 - monthDaysOffset - : 0; - - const currentDays = Array.from({ length: daysInMonth }, (_, i) => { - const day = i + 1; + const currentDays = Array.from({ length: daysInCurrentMonth }, (_, index) => { + const day = index + 1; const thisDay = date.date(day); - return generateDayObject(day, thisDay, minimumDate, maximumDate, true); + return generateDayObject( + day, + thisDay, + minDate, + maxDate, + true, + prevMonthOffset + day + ); }); - const nextDays = Array.from({ length: nextMonthDays }, (_, i) => { - const day = i + 1; + const nextDays = Array.from({ length: daysInNextMonth }, (_, index) => { + const day = index + 1; const thisDay = date.add(1, 'month').date(day); - return generateDayObject(day, thisDay, minimumDate, maximumDate, false); + return generateDayObject( + day, + thisDay, + minDate, + maxDate, + false, + daysInCurrentMonth + prevMonthOffset + day + ); }); return [...prevDays, ...currentDays, ...nextDays]; @@ -143,7 +228,8 @@ const generateDayObject = ( date: dayjs.Dayjs, minDate: DateType, maxDate: DateType, - isCurrentMonth: boolean + isCurrentMonth: boolean, + dayOfMonth: number ) => { let disabled = false; if (minDate) { @@ -158,5 +244,28 @@ const generateDayObject = ( date: getFormatedDate(date, DATE_FORMAT), disabled, isCurrentMonth, + dayOfMonth, }; }; + +export function addColorAlpha(color: string | undefined, opacity: number) { + //if it has an alpha, remove it + if (!color) { + color = '#000000'; + } + + if (color.length > 7) { + color = color.substring(0, color.length - 2); + } + + // coerce values so ti is between 0 and 1. + const _opacity = Math.round(Math.min(Math.max(opacity, 0), 1) * 255); + let opacityHex = _opacity.toString(16).toUpperCase(); + + // opacities near 0 need a trailing 0 + if (opacityHex.length === 1) { + opacityHex = '0' + opacityHex; + } + + return color + opacityHex; +} From 81518671bdc6e38d2fe1630c109f25e7eada4a70 Mon Sep 17 00:00:00 2001 From: Farhoud Shapouran Date: Fri, 26 Jan 2024 20:35:33 +0300 Subject: [PATCH 2/6] feat: add multiple mode --- example/src/App.tsx | 59 +++++++++++++++++-------- src/DateTimePicker.tsx | 80 ++++++++++++++++++++++++++-------- src/components/DaySelector.tsx | 45 ++++++++++++++++++- src/enums.ts | 1 + src/types.ts | 2 +- 5 files changed, 147 insertions(+), 40 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 7d819b6..d19001d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -38,44 +38,34 @@ const Locales = ['en', 'de', 'es', 'fr', 'tr']; export default function App() { const [mode, setMode] = useState('single'); - const [date, setDate] = useState(); + const [date, setDate] = useState(); const [range, setRange] = React.useState<{ startDate: DateType; endDate: DateType; }>({ startDate: undefined, endDate: undefined }); + const [dates, setDates] = useState(); const [theme, setTheme] = useState(Themes[0]); const [locale, setLocale] = useState('en'); - const onChangeSingle = useCallback( - (params: any) => { - setDate(params.date); - }, - [setDate] - ); - - const onChaneRange = useCallback( - ({ startDate, endDate }: any) => { - setRange({ startDate, endDate }); - }, - [setRange] - ); - const onChangeMode = useCallback( (value: ModeType) => { setDate(undefined); setRange({ startDate: undefined, endDate: undefined }); + setDates(undefined); setMode(value); }, - [setMode, setDate, setRange] + [setMode, setDate, setRange, setDates] ); const onChange = useCallback( (params) => { if (mode === 'single') { - onChangeSingle(params); + setDate(params.date); } else if (mode === 'range') { - onChaneRange(params); + setRange(params); + } else if (mode === 'multiple') { + setDates(params.dates); } }, [mode] @@ -182,6 +172,26 @@ export default function App() { Range + onChangeMode('multiple')} + > + + Multiple + + @@ -190,6 +200,7 @@ export default function App() { date={date} startDate={range.startDate} endDate={range.endDate} + dates={dates} //minDate={dayjs().startOf('day')} //maxDate={dayjs().add(3, 'day').endOf('day')} //firstDayOfWeek={1} @@ -217,7 +228,7 @@ export default function App() { .format('MMMM, DD, YYYY - HH:mm')} onChangeSingle({ date: dayjs() })} + onPress={() => setDate(dayjs())} accessibilityRole="button" accessibilityLabel="Today" > @@ -261,6 +272,16 @@ export default function App() { : '...'} + ) : mode === 'multiple' ? ( + + Selected Dates: + {dates && + dates.map((d, index) => ( + + {dayjs(d).locale(locale).format('MMMM, DD, YYYY')} + + ))} + ) : null} diff --git a/src/DateTimePicker.tsx b/src/DateTimePicker.tsx index c26808b..d6ab420 100644 --- a/src/DateTimePicker.tsx +++ b/src/DateTimePicker.tsx @@ -5,6 +5,7 @@ import { dateToUnix, getEndOfDay, getStartOfDay, + areDatesOnSameDay, } from './utils'; import CalendarContext from './CalendarContext'; import { CalendarViews, CalendarActionKind } from './enums'; @@ -15,13 +16,15 @@ import type { DatePickerBaseProps, CalendarThemeProps, HeaderProps, + SingleChange, + RangeChange, + MultiChange, } from './types'; import Calendar from './components/Calendar'; import dayjs from 'dayjs'; import localeData from 'dayjs/plugin/localeData'; import relativeTime from 'dayjs/plugin/relativeTime'; import localizedFormat from 'dayjs/plugin/localizedFormat'; -import { SingleChange, RangeChange, MultiChange } from './types'; dayjs.extend(localeData); dayjs.extend(relativeTime); @@ -72,7 +75,7 @@ const DateTimePicker = ( date, startDate, endDate, - // dates, + dates, onChange, ...rest } = props; @@ -82,6 +85,22 @@ const DateTimePicker = ( ? firstDayOfWeek : 0; + let currentDate = dayjs(); + + if (mode === 'single' && date) { + currentDate = dayjs(date); + } + + if (mode === 'range' && startDate) { + currentDate = dayjs(startDate); + } + + if (mode === 'multiple' && dates && dates.length > 0) { + currentDate = dayjs(dates[0]); + } + + let currentYear = currentDate.year(); + dayjs.locale(locale); const [state, dispatch] = useReducer( @@ -116,16 +135,22 @@ const DateTimePicker = ( startDate, endDate, }; + case CalendarActionKind.CHANGE_SELECTED_MULTIPLE: + const { dates } = action.payload; + return { + ...prevState, + dates, + }; } }, { - date: undefined, - startDate: undefined, - endDate: undefined, - dates: [], + date, + startDate, + endDate, + dates, calendarView: CalendarViews.day, - currentDate: new Date(), - currentYear: new Date().getFullYear(), + currentDate, + currentYear, } ); @@ -140,8 +165,13 @@ const DateTimePicker = ( type: CalendarActionKind.CHANGE_SELECTED_RANGE, payload: { startDate, endDate }, }); + } else if (mode === 'multiple') { + dispatch({ + type: CalendarActionKind.CHANGE_SELECTED_MULTIPLE, + payload: { dates }, + }); } - }, [mode, date, startDate, endDate]); + }, [mode, date, startDate, endDate, dates]); const setCalendarView = useCallback((view: CalendarViews) => { dispatch({ type: CalendarActionKind.SET_CALENDAR_VIEW, payload: view }); @@ -150,11 +180,12 @@ const DateTimePicker = ( const onSelectDate = useCallback( (date: DateType) => { if (onChange) { - dispatch({ - type: CalendarActionKind.CHANGE_CURRENT_DATE, - payload: date, - }); if (mode === 'single') { + dispatch({ + type: CalendarActionKind.CHANGE_CURRENT_DATE, + payload: date, + }); + (onChange as SingleChange)({ date, }); @@ -172,13 +203,26 @@ const DateTimePicker = ( endDate: !isStart ? getEndOfDay(date) : undefined, }); } else if (mode === 'multiple') { - // (onChange as MultiChange)({ - // dates: [date], - // }); + const safeDates = (state.dates as DateType[]) || []; + const newDate = getStartOfDay(date); + + const exists = safeDates.some((ed) => areDatesOnSameDay(ed, newDate)); + + const newDates = exists + ? safeDates.filter((ed) => !areDatesOnSameDay(ed, newDate)) + : [...safeDates, newDate]; + + newDates.sort((a, b) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1)); + + (onChange as MultiChange)({ + dates: newDates, + datePressed: newDate, + change: exists ? 'removed' : 'added', + }); } } }, - [onChange, mode, state.startDate, state.endDate] + [onChange, mode, state.startDate, state.endDate, state.dates] ); const onSelectMonth = useCallback( @@ -233,8 +277,6 @@ const DateTimePicker = ( { const { mode, + date, startDate, endDate, + dates, currentDate, - date, onSelectDate, displayFullDays, minDate, @@ -85,6 +87,46 @@ const DaySelector = () => { ) { inRange = false; } + } else if (mode === 'multiple') { + const safeDates = dates || []; + isSelected = safeDates.some((d) => areDatesOnSameDay(day.date, d)); + + const yesterday = dayjs(day.date).add(-1, 'day'); + const tomorrow = dayjs(day.date).add(1, 'day'); + + const yesterdaySelected = safeDates.some((d) => + areDatesOnSameDay(d, yesterday) + ); + const tomorrowSelected = safeDates.some((d) => + areDatesOnSameDay(d, tomorrow) + ); + + if (isSelected) { + if (tomorrowSelected && yesterdaySelected) { + inRange = true; + } + if (tomorrowSelected && !yesterdaySelected) { + inRange = true; + leftCrop = true; + } + + if (yesterdaySelected && !tomorrowSelected) { + inRange = true; + rightCrop = true; + } + + if (isFirstDayOfMonth && !tomorrowSelected) { + inRange = false; + } + + if (isLastDayOfMonth && !yesterdaySelected) { + inRange = false; + } + + if (inRange && !leftCrop && !rightCrop) { + isSelected = false; + } + } } else if (mode === 'single') { isSelected = areDatesOnSameDay(day.date, date); } @@ -114,6 +156,7 @@ const DaySelector = () => { date, startDate, endDate, + dates, ] ); diff --git a/src/enums.ts b/src/enums.ts index d9cbef8..79193ce 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -11,6 +11,7 @@ export enum CalendarActionKind { CHANGE_CURRENT_YEAR = 'CHANGE_CURRENT_YEAR', CHANGE_SELECTED_DATE = 'CHANGE_SELECTED_DATE', CHANGE_SELECTED_RANGE = 'CHANGE_SELECTED_RANGE', + CHANGE_SELECTED_MULTIPLE = 'CHANGE_SELECTED_MULTIPLE', } export const CALENDAR_HEIGHT = 300; diff --git a/src/types.ts b/src/types.ts index fd072e3..2e50fa1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,7 +72,7 @@ export type RangeChange = (params: { export type MultiChange = (params: { dates: DateType[]; - datePressed: Date; + datePressed: DateType; change: 'added' | 'removed'; }) => any; From 6cf40a51cee49bee8a81455873757ac305b97af3 Mon Sep 17 00:00:00 2001 From: Farhoud Shapouran Date: Fri, 26 Jan 2024 22:24:18 +0300 Subject: [PATCH 3/6] feat: add timepicker for single mode --- example/package.json | 1 + example/src/App.tsx | 47 ++++++++++++++++++++++++++++++---- example/yarn.lock | 5 ++++ src/CalendarContext.tsx | 2 -- src/DateTimePicker.tsx | 16 ++++++++---- src/components/DaySelector.tsx | 1 + src/components/Header.tsx | 8 +++--- src/types.ts | 1 + 8 files changed, 66 insertions(+), 15 deletions(-) diff --git a/example/package.json b/example/package.json index c6a856f..ae63fbc 100644 --- a/example/package.json +++ b/example/package.json @@ -17,6 +17,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.72.6", + "react-native-bouncy-checkbox": "^3.0.7", "react-native-web": "~0.19.6" }, "devDependencies": { diff --git a/example/src/App.tsx b/example/src/App.tsx index d19001d..e61f999 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -9,6 +9,7 @@ import { SafeAreaView, TouchableOpacity, } from 'react-native'; +import BouncyCheckbox from 'react-native-bouncy-checkbox'; import DateTimePicker, { DateType, ModeType } from 'react-native-ui-datepicker'; import dayjs from 'dayjs'; import 'dayjs/locale/en'; @@ -37,6 +38,7 @@ const Locales = ['en', 'de', 'es', 'fr', 'tr']; export default function App() { const [mode, setMode] = useState('single'); + const [timePicker, setTimePicker] = useState(false); const [date, setDate] = useState(); const [range, setRange] = React.useState<{ @@ -193,19 +195,48 @@ export default function App() { + + setTimePicker(!timePicker)} + disabled={mode !== 'single'} + /> + + (Works in Single mode) + + - {dayjs(date) - .locale(locale) - .format('MMMM, DD, YYYY - HH:mm')} + {date + ? dayjs(date) + .locale(locale) + .format( + timePicker + ? 'MMMM, DD, YYYY - HH:mm' + : 'MMMM, DD, YYYY' + ) + : '...'} setDate(dayjs())} diff --git a/example/yarn.lock b/example/yarn.lock index 495854a..fafc6b6 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -6979,6 +6979,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-native-bouncy-checkbox@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/react-native-bouncy-checkbox/-/react-native-bouncy-checkbox-3.0.7.tgz#011aef92b4fbde2a1f223165626c56a9ffe637fb" + integrity sha512-776TgMGt9wTpOQA1TcvFjL5VUn6o945wFYf3Ztqva62/vT2o3JAezLiP7hYh2Svd+PewfWBYSPMs4jeaSoS8Sg== + react-native-web@~0.19.6: version "0.19.9" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.9.tgz#6ee43e6c64d886b1d739f100fed07927541ee003" diff --git a/src/CalendarContext.tsx b/src/CalendarContext.tsx index 4f10dd8..abf9743 100644 --- a/src/CalendarContext.tsx +++ b/src/CalendarContext.tsx @@ -12,8 +12,6 @@ export interface CalendarContextType extends DatePickerBaseProps { firstDayOfWeek: number; theme: CalendarThemeProps; calendarView: CalendarViews; - // selectedDate: DateType; - // selectedDates: DateType[]; currentDate: DateType; // used for latest state of calendar based on Month and Year currentYear: number; setCalendarView: (value: CalendarViews) => void; diff --git a/src/DateTimePicker.tsx b/src/DateTimePicker.tsx index d6ab420..eb60651 100644 --- a/src/DateTimePicker.tsx +++ b/src/DateTimePicker.tsx @@ -65,6 +65,7 @@ const DateTimePicker = ( mode, locale = 'en', displayFullDays = false, + timePicker = false, firstDayOfWeek, buttonPrevIcon, buttonNextIcon, @@ -156,9 +157,11 @@ const DateTimePicker = ( useEffect(() => { if (mode === 'single') { + const newDate = date && (timePicker ? date : getStartOfDay(date)); + dispatch({ type: CalendarActionKind.CHANGE_SELECTED_DATE, - payload: { date }, + payload: { date: newDate }, }); } else if (mode === 'range') { dispatch({ @@ -171,7 +174,7 @@ const DateTimePicker = ( payload: { dates }, }); } - }, [mode, date, startDate, endDate, dates]); + }, [mode, date, startDate, endDate, dates, timePicker]); const setCalendarView = useCallback((view: CalendarViews) => { dispatch({ type: CalendarActionKind.SET_CALENDAR_VIEW, payload: view }); @@ -181,13 +184,15 @@ const DateTimePicker = ( (date: DateType) => { if (onChange) { if (mode === 'single') { + const newDate = timePicker ? date : getStartOfDay(date); + dispatch({ type: CalendarActionKind.CHANGE_CURRENT_DATE, - payload: date, + payload: newDate, }); (onChange as SingleChange)({ - date, + date: newDate, }); } else if (mode === 'range') { const sd = state.startDate; @@ -222,7 +227,7 @@ const DateTimePicker = ( } } }, - [onChange, mode, state.startDate, state.endDate, state.dates] + [onChange, mode, state.startDate, state.endDate, state.dates, timePicker] ); const onSelectMonth = useCallback( @@ -280,6 +285,7 @@ const DateTimePicker = ( locale, mode, displayFullDays, + timePicker, minDate, maxDate, firstDayOfWeek: firstDay, diff --git a/src/components/DaySelector.tsx b/src/components/DaySelector.tsx index 16f09c6..df0af84 100644 --- a/src/components/DaySelector.tsx +++ b/src/components/DaySelector.tsx @@ -29,6 +29,7 @@ const DaySelector = () => { firstDayOfWeek, theme, } = useCalendarContext(); + const { year, month, hour, minute } = getParsedDate(currentDate); const daysGrid = useMemo( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 9a8478e..88bf701 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -11,6 +11,7 @@ const arrow_right = require('../assets/images/arrow_right.png'); const Header = ({ buttonPrevIcon, buttonNextIcon }: HeaderProps) => { const { + mode, date, currentDate, currentYear, @@ -20,12 +21,11 @@ const Header = ({ buttonPrevIcon, buttonNextIcon }: HeaderProps) => { setCalendarView, theme, locale, + timePicker, } = useCalendarContext(); const currentMonthText = dayjs(currentDate).locale(locale).format('MMMM'); - const displayTime = false; - const renderPrevButton = ( { {calendarView !== CalendarViews.year ? monthSelector : null} {yearSelector()} - {displayTime && calendarView !== CalendarViews.year ? ( + {timePicker && + mode === 'single' && + calendarView !== CalendarViews.year ? ( setCalendarView( diff --git a/src/types.ts b/src/types.ts index 2e50fa1..bc12896 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,6 +85,7 @@ export interface DatePickerBaseProps { maxDate?: DateType; firstDayOfWeek?: number; displayFullDays?: boolean; + timePicker?: boolean; date?: DateType; dates?: DateType[]; startDate?: DateType; From 4ff8b3ad7bf90a3e6ecab1304468c88cfae04e17 Mon Sep 17 00:00:00 2001 From: Farhoud Shapouran Date: Fri, 26 Jan 2024 22:35:27 +0300 Subject: [PATCH 4/6] chore: update example --- example/src/App.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index e61f999..5aeffcc 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -97,6 +97,14 @@ export default function App() { ))} + + Locale: + {Locales.map((item, index) => ( - Modes: + Mode: Date: Sat, 27 Jan 2024 01:11:58 +0300 Subject: [PATCH 5/6] fix: day component memo bug based on Elolawyn comment in #46 --- package.json | 2 ++ src/components/Day.tsx | 71 ++++++++++++++++++++++++++++++------------ yarn.lock | 5 +++ 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index ef29f19..ac48f1d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "^12.3.2", "@types/jest": "^28.1.8", + "@types/lodash": "^4.14.202", "@types/react": "~17.0.21", "@types/react-native": "0.70.0", "@types/react-test-renderer": "^18.0.5", @@ -81,6 +82,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^28.1.3", + "lodash": "^4.17.21", "pod-install": "^0.1.0", "prettier": "^2.0.5", "react": "^18.2.0", diff --git a/src/components/Day.tsx b/src/components/Day.tsx index c35aa8b..f00d84f 100644 --- a/src/components/Day.tsx +++ b/src/components/Day.tsx @@ -3,6 +3,7 @@ import { View, Text, Pressable, StyleSheet } from 'react-native'; import { CalendarThemeProps, IDayObject } from '../types'; import { CALENDAR_HEIGHT } from '../enums'; import { addColorAlpha } from '../utils'; +import { isEqual } from 'lodash'; export const daySize = 46; @@ -10,7 +11,7 @@ interface Props extends Omit { isToday: boolean; isSelected: boolean; onSelectDate: (date: string) => void; - theme?: CalendarThemeProps; + theme: CalendarThemeProps; } function EmptyDayPure() { @@ -19,7 +20,7 @@ function EmptyDayPure() { export const EmptyDay = React.memo(EmptyDayPure); -const Day = ({ +function Day({ date, text, disabled, @@ -31,40 +32,51 @@ const Day = ({ rightCrop, onSelectDate, theme, -}: Props) => { +}: Props) { + const onPress = React.useCallback(() => { + onSelectDate(date); + }, [onSelectDate, date]); + + const { + calendarTextStyle, + dayContainerStyle, + selectedItemColor, + selectedTextStyle, + todayContainerStyle, + todayTextStyle, + } = theme; + //const bothWays = inRange && leftCrop && rightCrop; const isCrop = inRange && (leftCrop || rightCrop) && !(leftCrop && rightCrop); - const dayContainerStyle = isCurrentMonth - ? theme?.dayContainerStyle - : { opacity: 0.3 }; + const containerStyle = isCurrentMonth ? dayContainerStyle : { opacity: 0.3 }; const todayItemStyle = isToday ? { borderWidth: 2, - borderColor: theme?.selectedItemColor || '#0047FF', - ...theme?.todayContainerStyle, + borderColor: selectedItemColor || '#0047FF', + ...todayContainerStyle, } : null; const activeItemStyle = isSelected ? { - borderColor: theme?.selectedItemColor || '#0047FF', - backgroundColor: theme?.selectedItemColor || '#0047FF', + borderColor: selectedItemColor || '#0047FF', + backgroundColor: selectedItemColor || '#0047FF', } : null; const textStyle = isSelected - ? { color: '#fff', ...theme?.selectedTextStyle } + ? { color: '#fff', ...selectedTextStyle } : isToday ? { - ...theme?.calendarTextStyle, - color: theme?.selectedItemColor || '#0047FF', - ...theme?.todayTextStyle, + ...calendarTextStyle, + color: selectedItemColor || '#0047FF', + ...todayTextStyle, } - : theme?.calendarTextStyle; + : calendarTextStyle; - const rangeRootBackground = addColorAlpha(theme?.selectedItemColor, 0.15); + const rangeRootBackground = addColorAlpha(selectedItemColor, 0.15); return ( @@ -100,10 +112,10 @@ const Day = ({ onSelectDate(date)} + onPress={disabled ? undefined : onPress} style={[ styles.dayContainer, - dayContainerStyle, + containerStyle, todayItemStyle, activeItemStyle, disabled && styles.disabledDay, @@ -118,7 +130,7 @@ const Day = ({ ); -}; +} const styles = StyleSheet.create({ dayCell: { @@ -149,4 +161,23 @@ const styles = StyleSheet.create({ }, }); -export default React.memo(Day); +const customComparator = ( + prevProps: Readonly, + nextProps: Readonly +) => { + return ( + prevProps.date === nextProps.date && + prevProps.text === nextProps.text && + prevProps.disabled === nextProps.disabled && + prevProps.isCurrentMonth === nextProps.isCurrentMonth && + prevProps.isToday === nextProps.isToday && + prevProps.isSelected === nextProps.isSelected && + prevProps.inRange === nextProps.inRange && + prevProps.leftCrop === nextProps.leftCrop && + prevProps.rightCrop === nextProps.rightCrop && + prevProps.onSelectDate === nextProps.onSelectDate && + isEqual(prevProps.theme, nextProps.theme) + ); +}; + +export default React.memo(Day, customComparator); diff --git a/yarn.lock b/yarn.lock index fc4b706..6c22a2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2318,6 +2318,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/lodash@^4.14.202": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/minimist@^1.2.0", "@types/minimist@^1.2.2": version "1.2.5" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" From 191538e516a7667f9d05c3a9dc6575eac45b2901 Mon Sep 17 00:00:00 2001 From: Farhoud Shapouran Date: Sat, 27 Jan 2024 17:35:02 +0300 Subject: [PATCH 6/6] chore: upgrade dependencies --- package.json | 5 ++--- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ac48f1d..5f6bf88 100644 --- a/package.json +++ b/package.json @@ -76,13 +76,11 @@ "@types/react-native": "0.70.0", "@types/react-test-renderer": "^18.0.5", "commitlint": "^17.0.2", - "dayjs": "^1.11.7", "del-cli": "^5.0.0", "eslint": "^8.4.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^28.1.3", - "lodash": "^4.17.21", "pod-install": "^0.1.0", "prettier": "^2.0.5", "react": "^18.2.0", @@ -97,7 +95,6 @@ "@types/react": "17.0.43" }, "peerDependencies": { - "dayjs": "*", "react": "*", "react-native": "*" }, @@ -171,6 +168,8 @@ ] }, "dependencies": { + "dayjs": "^1.11.10", + "lodash": "^4.17.21", "uninstall": "^0.0.0" } } diff --git a/yarn.lock b/yarn.lock index 6c22a2e..9cc979f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3790,7 +3790,7 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@^1.11.7, dayjs@^1.8.15: +dayjs@^1.11.10, dayjs@^1.8.15: version "1.11.10" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==