diff --git a/src/hooks/internal/useNavigation.ts b/src/hooks/internal/useNavigation.ts index 3eeccd3..63b5afb 100644 --- a/src/hooks/internal/useNavigation.ts +++ b/src/hooks/internal/useNavigation.ts @@ -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 = { @@ -10,6 +13,7 @@ export type UseNavigationReturnType = { } currYear: { label: string | number + value: string | number } nextYear: { label: string | number @@ -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>, - localeOptions: PickerOptions + localeOptions: PickerOptionsWithResolvedCalendar ) => UseNavigationReturnType /** * internal hook used by useDatePicker to build the navigation of the calendar @@ -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, } @@ -76,11 +132,48 @@ 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), @@ -88,14 +181,29 @@ export const useNavigation: UseNavigationHook = ( 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), @@ -103,7 +211,7 @@ export const useNavigation: UseNavigationHook = ( prevMonth: { label: localisationHelpers.localiseMonth( prevMonth, - localeOptions, + { ...localeOptions, calendar: options.calendar }, monthFormat ), navigateTo: () => setFirstZdtOfVisibleMonth(prevMonth), @@ -111,18 +219,21 @@ export const useNavigation: UseNavigationHook = ( 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]) } diff --git a/src/hooks/internal/useWeekDayLabels.ts b/src/hooks/internal/useWeekDayLabels.ts index 491dd8f..27b2439 100644 --- a/src/hooks/internal/useWeekDayLabels.ts +++ b/src/hooks/internal/useWeekDayLabels.ts @@ -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') @@ -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) } diff --git a/src/hooks/useDatePicker.ts b/src/hooks/useDatePicker.ts index 3124194..fa44086 100644 --- a/src/hooks/useDatePicker.ts +++ b/src/hooks/useDatePicker.ts @@ -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, @@ -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 diff --git a/src/types.ts b/src/types.ts index 148f5f0..1ec1e1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,10 @@ export type WeekDayFormat = 'narrow' | 'short' | 'long' export type PickerOptions = Partial +export type PickerOptionsWithResolvedCalendar = Omit & { + calendar: Temporal.CalendarProtocol +} + export type ResolvedLocaleOptions = { calendar: SupportedCalendar locale: string diff --git a/src/utils/localisationHelpers.ts b/src/utils/localisationHelpers.ts index 1157dc2..d399ec8 100644 --- a/src/utils/localisationHelpers.ts +++ b/src/utils/localisationHelpers.ts @@ -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: ( @@ -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')