Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support min & max date, DD-MM-YYYY date format #36

Merged
merged 13 commits into from
Jun 9, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"access": "public"
},
"dependencies": {
"@js-temporal/polyfill": "^0.4.2",
"@js-temporal/polyfill": "0.4.3",
"classnames": "^2.3.2"
},
"peerDependencies": {
Expand Down
16 changes: 3 additions & 13 deletions src/custom-calendars/nepaliCalendar.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { NEPALI_CALENDAR_DATA } from './nepaliCalendarData'
type CalendarYMD = { year: number; month: number; day: number }
type AssignmentOptions = { overflow?: 'constrain' | 'reject' }

/**
* https://tc39.es/proposal-temporal/docs/calendar.html
Expand Down Expand Up @@ -65,32 +64,23 @@ class NepaliCalendar extends Temporal.Calendar {
*
* A custom implementation of these methods is used to convert the calendar-space arguments to the ISO calendar.
*/
dateFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainDate {
dateFromFields(fields: CalendarYMD): Temporal.PlainDate {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
day: fields.day,
})
return new Temporal.PlainDate(year, month, day, this)
}
yearMonthFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainYearMonth {
yearMonthFromFields(fields: CalendarYMD): Temporal.PlainYearMonth {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
day: fields.day,
})
return new Temporal.PlainYearMonth(year, month, this, day)
}
monthDayFromFields(
fields: CalendarYMD,
options?: AssignmentOptions
): Temporal.PlainMonthDay {
monthDayFromFields(fields: CalendarYMD): Temporal.PlainMonthDay {
const { year, day, month } = _nepaliToIso({
year: fields.year,
month: fields.month,
Expand Down
46 changes: 46 additions & 0 deletions src/hooks/useDatePicker.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -620,3 +620,49 @@ it('should generate the correct calendar weeks when passed "Ethiopian" rather th
['27', '28', '29', '30', '1', '2', '3'],
])
})

describe('validation rules', () => {
it('should validate correct format', () => {
const onDateSelect = jest.fn()
const date = '2015-'
const options = {
// minDate: '2015-06-28',
// calendar: 'iso8601' as SupportedCalendar,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove commented out code please

}
const renderedHook = renderHook(() =>
useDatePicker({ onDateSelect, date, options })
)
const result = renderedHook.result?.current as UseDatePickerReturn

expect(result.isValid).toEqual(false)
})
it('should validate min', () => {
const onDateSelect = jest.fn()
const date = '2015-06-22'
const options = {
// calendar: 'iso8601' as SupportedCalendar,
}
const minDate = '2015-06-28'
const maxDate = '2018-06-28'
const renderedHook = renderHook(() =>
useDatePicker({ onDateSelect, date, options, minDate, maxDate })
)
const result = renderedHook.result?.current as UseDatePickerReturn

expect(result.isValid).toEqual(false)
})

it('should validate max date', () => {
const onDateSelect = jest.fn()
const date = '2019-06-22'
const options = {}
const minDate = '2015-06-28'
const maxDate = '2018-06-28'
const renderedHook = renderHook(() =>
useDatePicker({ onDateSelect, date, options, minDate, maxDate })
)
const result = renderedHook.result?.current as UseDatePickerReturn

expect(result.isValid).toEqual(false)
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you also add similar tests for ethiopic and nepali calendars please

})
116 changes: 57 additions & 59 deletions src/hooks/useDatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { dhis2CalendarsMap } from '../constants/dhis2CalendarsMap'
import { getNowInCalendar } from '../index'
import { PickerOptions, SupportedCalendar } from '../types'
import { extractDatePartsFromDateString } from '../utils'
import { formatYyyyMmDD, getCustomCalendarIfExists } from '../utils/helpers'
import {
formatDate,
getCustomCalendarIfExists,
extractAndValidateDateString,
} from '../utils/helpers'
import localisationHelpers from '../utils/localisationHelpers'
import { useCalendarWeekDays } from './internal/useCalendarWeekDays'
import {
Expand All @@ -24,6 +27,10 @@ type DatePickerOptions = {
calendarDate: Temporal.ZonedDateTime
calendarDateString: string
}) => void
minDate?: string
maxDate?: string
format?: string
validation?: string
}

export type UseDatePickerReturn = UseNavigationReturnType & {
Expand All @@ -37,51 +44,31 @@ export type UseDatePickerReturn = UseNavigationReturnType & {
isToday: boolean
isInCurrentMonth: boolean
}[][]
isValid: boolean
warningMessage: string
errorMessage: string
}

type UseDatePickerHookType = (options: DatePickerOptions) => UseDatePickerReturn

const fromDateParts = (date: string, options: PickerOptions) => {
let result: Temporal.PlainDateLike

try {
const { year, month, day } = extractDatePartsFromDateString(date)
result = { year, month, day }
} catch (err) {
console.warn(err)

const { year, month, day } = getNowInCalendar(
options.calendar,
options.timeZone
)

result = { year, month, day }
}

// for ethiopic, we need to make sure it's the correct era
// there is a discussion in the Temporal proposal whether this
// should be made the default era, for now this is a workaround
if (options.calendar === 'ethiopic') {
result.era = 'era1'
result.eraYear = result.year
delete result.year
}
return result
}
export const useDatePicker: UseDatePickerHookType = ({
onDateSelect,
date: dateParts,
date: dateString,
minDate,
maxDate,
format,
validation,
options,
}) => {
const calendar = getCustomCalendarIfExists(
dhis2CalendarsMap[options.calendar!] ?? options.calendar
dhis2CalendarsMap[options.calendar ?? 'gregorian'] ?? options.calendar
) as SupportedCalendar

const resolvedOptions = useResolvedLocaleOptions({
...options,
calendar,
})
const prevDateStringRef = useRef(dateParts)
const prevDateStringRef = useRef(dateString)

const todayZdt = useMemo(
() =>
Expand All @@ -92,13 +79,23 @@ export const useDatePicker: UseDatePickerHookType = ({
[resolvedOptions]
)

const date = dateParts
? (fromDateParts(
dateParts,
resolvedOptions
) as Temporal.YearOrEraAndEraYear &
Temporal.MonthOrMonthCode & { day: number })
: todayZdt
const result = extractAndValidateDateString(dateString, {
...resolvedOptions,
minDateString: minDate,
maxDateString: maxDate,
validation: validation,
})

const date = result as Temporal.YearOrEraAndEraYear &
Temporal.MonthOrMonthCode & {
day: number
isValid: boolean
warningMessage: string
errorMessage: string
format?: string
}

date.format = !date.format ? format : date.format

const temporalCalendar = useMemo(
() => Temporal.Calendar.from(resolvedOptions.calendar),
Expand All @@ -109,17 +106,13 @@ export const useDatePicker: UseDatePickerHookType = ({
[resolvedOptions]
)

const selectedDateZdt = useMemo(
() =>
date
? Temporal.Calendar.from(temporalCalendar)
.dateFromFields(date)
.toZonedDateTime({
timeZone: temporalTimeZone,
})
: null,
[date, temporalTimeZone, temporalCalendar]
)
const selectedDateZdt = date.isValid
? Temporal.Calendar.from(temporalCalendar)
.dateFromFields(date)
.toZonedDateTime({
timeZone: temporalTimeZone,
})
: null

const [firstZdtOfVisibleMonth, setFirstZdtOfVisibleMonth] = useState(() => {
const zdt = selectedDateZdt || todayZdt
Expand Down Expand Up @@ -148,21 +141,21 @@ export const useDatePicker: UseDatePickerHookType = ({
(zdt: Temporal.ZonedDateTime) => {
onDateSelect({
calendarDate: zdt,
calendarDateString: formatYyyyMmDD(zdt),
calendarDateString: formatDate(zdt, undefined, date.format),
})
},
[onDateSelect]
[onDateSelect, date.format]
)
const calendarWeekDaysZdts = useCalendarWeekDays(firstZdtOfVisibleMonth)

useEffect(() => {
if (dateParts === prevDateStringRef.current) {
if (dateString === prevDateStringRef.current) {
return
}

prevDateStringRef.current = dateParts
prevDateStringRef.current = dateString

if (!dateParts) {
if (!dateString) {
return
}

Expand All @@ -183,7 +176,7 @@ export const useDatePicker: UseDatePickerHookType = ({
}
}, [
date,
dateParts,
dateString,
firstZdtOfVisibleMonth,
calendarWeekDaysZdts,
temporalCalendar,
Expand All @@ -193,15 +186,17 @@ export const useDatePicker: UseDatePickerHookType = ({
calendarWeekDays: calendarWeekDaysZdts.map((week) =>
week.map((weekDayZdt) => ({
zdt: weekDayZdt,
calendarDate: formatYyyyMmDD(weekDayZdt),
calendarDate: formatDate(weekDayZdt, undefined, date.format),
label: localisationHelpers.localiseWeekLabel(
weekDayZdt.withCalendar(localeOptions.calendar),
localeOptions
),
onClick: () => selectDate(weekDayZdt),
isSelected: selectedDateZdt
?.withCalendar('iso8601')
.equals(weekDayZdt.withCalendar('iso8601')),
? selectedDateZdt
?.withCalendar('iso8601')
.equals(weekDayZdt.withCalendar('iso8601'))
: false,
isToday: todayZdt && weekDayZdt.equals(todayZdt),
isInCurrentMonth:
firstZdtOfVisibleMonth &&
Expand All @@ -210,5 +205,8 @@ export const useDatePicker: UseDatePickerHookType = ({
),
...navigation,
weekDayLabels,
isValid: date.isValid,
warningMessage: date.warningMessage,
errorMessage: date.errorMessage,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import { formatYyyyMmDD, localisationHelpers } from '../../utils/index'
import { formatDate, localisationHelpers } from '../../utils/index'
import { FixedPeriod } from '../types'

const { localiseDateLabel } = localisationHelpers
Expand Down Expand Up @@ -31,9 +31,9 @@ const buildDailyFixedPeriod: BuildDailyFixedPeriod = ({
id: value,
iso: value,
displayName,
name: formatYyyyMmDD(date),
startDate: formatYyyyMmDD(date),
endDate: formatYyyyMmDD(date),
name: formatDate(date),
startDate: formatDate(date),
endDate: formatDate(date),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import { fromAnyDate, formatYyyyMmDD, padWithZeroes } from '../../utils/index'
import { fromAnyDate, formatDate, padWithZeroes } from '../../utils/index'
import { FixedPeriod, PeriodType } from '../types'
import doesPeriodEndBefore from './does-period-end-before'

Expand Down Expand Up @@ -80,8 +80,8 @@ const generateFixedPeriodsWeekly: GenerateFixedPeriodsWeekly = ({
iso: value,
name,
displayName: name,
startDate: formatYyyyMmDD(date),
endDate: formatYyyyMmDD(endofWeek),
startDate: formatDate(date),
endDate: formatDate(endofWeek),
})
}
date = fromAnyDate({ date: endofWeek, calendar }).add({ days: 1 })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import {
formatYyyyMmDD,
formatDate,
isCustomCalendar,
padWithZeroes,
} from '../../utils/helpers'
Expand Down Expand Up @@ -76,8 +76,8 @@ const buildMonthlyFixedPeriod: BuildMonthlyFixedPeriod = ({
iso: id,
name,
displayName: name,
startDate: formatYyyyMmDD(month, 'startOfMonth'),
endDate: formatYyyyMmDD(endDate, 'endOfMonth'),
startDate: formatDate(month, 'startOfMonth'),
endDate: formatDate(endDate, 'endOfMonth'),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Temporal } from '@js-temporal/polyfill'
import { SupportedCalendar } from '../../types'
import {
fromAnyDate,
formatYyyyMmDD,
isCustomCalendar,
} from '../../utils/index'
import { fromAnyDate, formatDate, isCustomCalendar } from '../../utils/index'
import localisationHelpers from '../../utils/localisationHelpers'
import { financialYearFixedPeriodTypes } from '../period-type-groups'
import { FixedPeriod, PeriodType } from '../types'
Expand Down Expand Up @@ -47,8 +43,8 @@ const buildYearlyFixedPeriod: BuildYearlyFixedPeriod = ({
iso: value,
name,
displayName: name,
startDate: formatYyyyMmDD(startDate, 'startOfMonth'),
endDate: formatYyyyMmDD(endDate, 'endOfMonth'),
startDate: formatDate(startDate, 'startOfMonth'),
endDate: formatDate(endDate, 'endOfMonth'),
}
}

Expand Down
Loading
Loading