Skip to content

Commit

Permalink
feat: add year month selector to calendar component
Browse files Browse the repository at this point in the history
  • Loading branch information
alaa-yahia committed Feb 10, 2025
1 parent c86d95f commit 71f78b9
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 16 deletions.
129 changes: 120 additions & 9 deletions src/hooks/internal/useNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Temporal } from '@js-temporal/polyfill'
import { Dispatch, SetStateAction, useMemo } from 'react'
import { PickerOptions } from '../../types'
import {
PickerOptionsWithResolvedCalendar,
SupportedCalendar,
} from '../../types'
import localisationHelpers from '../../utils/localisationHelpers'

export type UseNavigationReturnType = {
Expand All @@ -10,6 +13,7 @@ export type UseNavigationReturnType = {
}
currYear: {
label: string | number
value: string | number
}
nextYear: {
label: string | number
Expand All @@ -26,11 +30,61 @@ export type UseNavigationReturnType = {
label: string | undefined
navigateTo: () => void
}
months: Array<{
label: string
value: number
}>
navigateToMonth: (month: number) => void
navigateToYear: (year: number) => void
}

type Month = {
value: number
dateForLabel?: Temporal.PlainDate
}

const getAvailableMonths = (calendarSystem = 'iso8601'): Month[] => {
try {
const calendar = new Temporal.Calendar(calendarSystem)
const referenceDate = calendar.dateFromFields({
year: 2000,
month: 1,
day: 1,
})

const months: Month[] = []
let monthIndex = 1

while (monthIndex <= 13) {
try {
const date = calendar.dateFromFields({
year: referenceDate.year,
month: monthIndex,
day: 1,
})
const monthInfo = calendar.monthsInYear(date)
if (monthIndex > monthInfo) {
break
}
months.push({
value: monthIndex,
dateForLabel: date,
})
monthIndex++
} catch (e) {
break
}
}
return months
} catch (e) {
return getAvailableMonths('iso8601')
}
}

type UseNavigationHook = (
firstZdtOfVisibleMonth: Temporal.ZonedDateTime,
setFirstZdtOfVisibleMonth: Dispatch<SetStateAction<Temporal.ZonedDateTime>>,
localeOptions: PickerOptions
localeOptions: PickerOptionsWithResolvedCalendar
) => UseNavigationReturnType
/**
* internal hook used by useDatePicker to build the navigation of the calendar
Expand Down Expand Up @@ -62,7 +116,9 @@ export const useNavigation: UseNavigationHook = (

const options = {
locale: localeOptions.locale,
calendar: localeOptions.calendar,
calendar: localeOptions.calendar.id as
| SupportedCalendar
| undefined,
numberingSystem: localeOptions.numberingSystem,
}

Expand All @@ -76,53 +132,108 @@ export const useNavigation: UseNavigationHook = (
month: 'long' as const,
}

const monthsData = getAvailableMonths(options.calendar)
const months = monthsData
.map((month) => ({
value: month.value,
label:
(month.dateForLabel &&
localisationHelpers.localiseMonth(
month.dateForLabel.toZonedDateTime({
timeZone: firstZdtOfVisibleMonth.timeZone,
}),
{ ...localeOptions, calendar: options.calendar },
monthFormat
)) ||
'',
}))
.filter((month): month is { label: string; value: number } =>
Boolean(month.label)
)

const navigateToMonth = (monthNum: number) => {
try {
setFirstZdtOfVisibleMonth(
firstZdtOfVisibleMonth.with({ month: monthNum, day: 1 })
)
} catch (e) {
console.error('Invalid month navigation:', e)
}
}

const navigateToYear = (year: number) => {
try {
setFirstZdtOfVisibleMonth(firstZdtOfVisibleMonth.with({ year }))
} catch (e) {
console.error('Invalid year navigation:', e)
}
}

return {
prevYear: {
label: localisationHelpers.localiseYear(
prevYear,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
yearNumericFormat
),
navigateTo: () => setFirstZdtOfVisibleMonth(prevYear),
},
currYear: {
label: localisationHelpers.localiseYear(
firstZdtOfVisibleMonth,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
yearNumericFormat
),
value:
options.calendar === 'ethiopic'
? firstZdtOfVisibleMonth.eraYear ??
String(
localisationHelpers.localiseYear(
firstZdtOfVisibleMonth,
{
...localeOptions,
calendar: options.calendar,
},
yearNumericFormat
)
// Ethiopic years - when localised to English - add the era (i.e. 2015 ERA1)
).split(' ')[0]
: firstZdtOfVisibleMonth.year,
},
nextYear: {
label: localisationHelpers.localiseYear(
nextYear,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
yearNumericFormat
),
navigateTo: () => setFirstZdtOfVisibleMonth(nextYear),
},
prevMonth: {
label: localisationHelpers.localiseMonth(
prevMonth,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
monthFormat
),
navigateTo: () => setFirstZdtOfVisibleMonth(prevMonth),
},
currMonth: {
label: localisationHelpers.localiseMonth(
firstZdtOfVisibleMonth,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
monthFormat
),
},
nextMonth: {
label: localisationHelpers.localiseMonth(
nextMonth,
localeOptions,
{ ...localeOptions, calendar: options.calendar },
monthFormat
),
navigateTo: () => setFirstZdtOfVisibleMonth(nextMonth),
},
months,
navigateToMonth,
navigateToYear,
}
}, [firstZdtOfVisibleMonth, localeOptions, setFirstZdtOfVisibleMonth])
}
8 changes: 5 additions & 3 deletions src/hooks/internal/useWeekDayLabels.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Temporal } from '@js-temporal/polyfill'
import { useMemo } from 'react'
import { PickerOptions } from '../../types'
import { PickerOptionsWithResolvedCalendar } from '../../types'
import localisationHelpers from '../../utils/localisationHelpers'

export const useWeekDayLabels = (localeOptions: PickerOptions) =>
export const useWeekDayLabels = (
localeOptions: PickerOptionsWithResolvedCalendar
) =>
useMemo(() => {
if (!localeOptions.calendar) {
throw new Error('a calendar must be provided to useWeekDayLabels')
Expand All @@ -29,7 +31,7 @@ export const useWeekDayLabels = (localeOptions: PickerOptions) =>

const getWeekDayString: (
date: Temporal.ZonedDateTime,
localeOptions: PickerOptions
localeOptions: PickerOptionsWithResolvedCalendar
) => string = (date, localeOptions) => {
return localisationHelpers.localiseWeekDayLabel(date, localeOptions)
}
4 changes: 2 additions & 2 deletions src/hooks/useDatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const useDatePicker: UseDatePickerHookType = ({
const localeOptions = useMemo(
() => ({
locale: resolvedOptions.locale,
calendar: temporalCalendar as unknown as SupportedCalendar,
calendar: temporalCalendar,
timeZone: temporalTimeZone,
weekDayFormat: resolvedOptions.weekDayFormat,
numberingSystem: resolvedOptions.numberingSystem,
Expand Down Expand Up @@ -191,7 +191,7 @@ export const useDatePicker: UseDatePickerHookType = ({
dateValue: formatDate(weekDayZdt, undefined, format),
label: localisationHelpers.localiseWeekLabel(
weekDayZdt.withCalendar(localeOptions.calendar),
localeOptions
{...localeOptions, calendar: resolvedOptions.calendar}
),
onClick: () => selectDate(weekDayZdt),
isSelected: selectedDateZdt
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export type WeekDayFormat = 'narrow' | 'short' | 'long'

export type PickerOptions = Partial<ResolvedLocaleOptions>

export type PickerOptionsWithResolvedCalendar = Omit<PickerOptions, 'calendar'> & {
calendar: Temporal.CalendarProtocol
}

export type ResolvedLocaleOptions = {
calendar: SupportedCalendar
locale: string
Expand Down
8 changes: 6 additions & 2 deletions src/utils/localisationHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
customCalendars,
CustomCalendarTypes,
} from '../custom-calendars'
import { PickerOptions, SupportedCalendar } from '../types'
import {
PickerOptions,
PickerOptionsWithResolvedCalendar,
SupportedCalendar,
} from '../types'
import { formatDate, isCustomCalendar } from './helpers'

const getPartialLocaleMatch: (
Expand Down Expand Up @@ -137,7 +141,7 @@ const localiseMonth = (

export const localiseWeekDayLabel = (
zdt: Temporal.ZonedDateTime,
localeOptions: PickerOptions
localeOptions: PickerOptionsWithResolvedCalendar
) => {
if (!localeOptions.calendar) {
throw new Error('no calendar provided to localise function')
Expand Down

0 comments on commit 71f78b9

Please sign in to comment.