diff --git a/.eslintrc.json b/.eslintrc.json index 381b230e..0f805c59 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,7 @@ { "files": ["sample/**", "test/**"], "rules": { + "@typescript-eslint/no-var-requires": "off", "import/no-unresolved": "off" } } diff --git a/package.json b/package.json index dc6ecdbb..81f86ebc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Ultimate calendar for your React app.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", - "source": "src/index.js", + "source": "src/index.ts", + "types": "dist/cjs/index.d.ts", "sideEffects": [ "*.css" ], diff --git a/src/Calendar.spec.jsx b/src/Calendar.spec.tsx similarity index 82% rename from src/Calendar.spec.jsx rename to src/Calendar.spec.tsx index 2746c99e..b7791733 100644 --- a/src/Calendar.spec.jsx +++ b/src/Calendar.spec.tsx @@ -11,7 +11,9 @@ const { format } = new Intl.DateTimeFormat('en-US', { year: 'numeric', }); -const event = new Event('click', { bubbles: true }); +const event = new Event('click', { + bubbles: true, +}) as unknown as React.MouseEvent; event.persist = () => { // Intentionally empty }; @@ -53,50 +55,74 @@ describe('Calendar', () => { }); it('uses given value when passed value using value prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.value).toEqual(new Date(2019, 0, 1)); }); it('uses given value when passed value using defaultValue prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.value).toEqual(new Date(2019, 0, 1)); }); it('renders given view when passed view using view prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.view).toBe('century'); }); it('renders given view when passed view using defaultView prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.view).toBe('century'); }); it('renders given active start date when passed active start date using activeStartDate prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(new Date(2019, 0, 1)); }); it('renders given active start date when passed active start date using activeStartDate prop', () => { - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(new Date(2019, 0, 1)); }); @@ -104,12 +130,16 @@ describe('Calendar', () => { const value = new Date(2018, 1, 15); const newValue = new Date(2018, 0, 15); const newActiveStartDate = new Date(2018, 0, 1); - const instance = createRef(); + const instance = createRef(); const { rerender } = render(); rerender(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(newActiveStartDate); }); @@ -117,26 +147,38 @@ describe('Calendar', () => { const value = new Date(2018, 1, 15); const newValue = new Date(2018, 0, 15); const newActiveStartDate = new Date(2018, 0, 1); - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.onChange(newValue, event); }); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(newActiveStartDate); }); it('changes Calendar view given new activeStartDate value', () => { const activeStartDate = new Date(2017, 0, 1); const newActiveStartDate = new Date(2018, 0, 1); - const instance = createRef(); + const instance = createRef(); const { rerender } = render(); rerender(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(newActiveStartDate); }); @@ -241,7 +283,7 @@ describe('Calendar', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); // The first date that this calendar should show is December 26, 2016. @@ -256,7 +298,7 @@ describe('Calendar', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(new Date(2017, 0, 1))); @@ -267,7 +309,7 @@ describe('Calendar', () => { const { container } = render(); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(new Date(2017, 0, 1))); @@ -285,7 +327,7 @@ describe('Calendar', () => { />, ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(defaultActiveStartDate)); @@ -298,7 +340,7 @@ describe('Calendar', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(defaultActiveStartDate)); @@ -311,7 +353,7 @@ describe('Calendar', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(activeStartDate)); @@ -323,7 +365,7 @@ describe('Calendar', () => { const { container } = render(); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(beginOfCurrentMonth)); @@ -334,7 +376,7 @@ describe('Calendar', () => { const { container } = render(); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); // The date of the first Monday that this calendar should show is May 28, 2012. @@ -346,7 +388,7 @@ describe('Calendar', () => { const { container } = render(); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); // The date of the first Monday that this calendar should show is May 28, 2012. @@ -356,24 +398,36 @@ describe('Calendar', () => { describe('handles drill up properly', () => { it('drills up when allowed', () => { - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setState({ view: 'month' }); }); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillUp(); }); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.view).toBe('year'); }); it('calls onDrillUp on drill up properly given view prop', () => { const onDrillUp = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillUp(); }); expect(onDrillUp).toHaveBeenCalledWith({ action: 'drillUp', activeStartDate: new Date(2017, 0, 1), + value: null, view: 'year', }); }); it('calls onDrillUp on drill up properly when not given view prop', () => { const onDrillUp = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( , ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setState({ view: 'month' }); }); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillUp(); }); expect(onDrillUp).toHaveBeenCalledWith({ action: 'drillUp', activeStartDate: new Date(2017, 0, 1), + value: null, view: 'year', }); }); it('refuses to drill up when already on minimum allowed detail', () => { const onDrillUp = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillUp(); }); @@ -434,24 +506,36 @@ describe('Calendar', () => { describe('handles drill down properly', () => { it('drills down when allowed', () => { - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setState({ view: 'century' }); }); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillDown(new Date(2011, 0, 1), event); }); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.view).toBe('decade'); }); it('calls onDrillDown on drill down given view prop', () => { const onDrillDown = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillDown(new Date(2011, 0, 1), event); }); expect(onDrillDown).toHaveBeenCalledWith({ action: 'drillDown', activeStartDate: new Date(2011, 0, 1), + value: null, view: 'decade', }); }); it('calls onDrillDown on drill down when not given view prop', () => { const onDrillDown = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setState({ view: 'century' }); }); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillDown(new Date(2011, 0, 1), event); }); expect(onDrillDown).toHaveBeenCalledWith({ action: 'drillDown', activeStartDate: new Date(2011, 0, 1), + value: null, view: 'decade', }); }); it('refuses to drill down when already on minimum allowed detail', () => { const onDrillDown = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.drillUp(); }); @@ -516,14 +618,22 @@ describe('Calendar', () => { describe('handles active start date change properly', () => { it('changes active start date when allowed', () => { - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setActiveStartDate(new Date(2019, 0, 1), 'onChange'); }); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + expect(instance.current.activeStartDate).toEqual(new Date(2019, 0, 1)); }); @@ -531,7 +641,7 @@ describe('Calendar', () => { const value = new Date(2019, 0, 15); const newActiveStartDate = new Date(2018, 0, 1); const onActiveStartDateChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setActiveStartDate(newActiveStartDate, 'onChange'); }); @@ -559,7 +673,7 @@ describe('Calendar', () => { const activeStartDate = new Date(2017, 0, 1); const newActiveStartDate = new Date(2018, 0, 1); const onActiveStartDateChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setActiveStartDate(newActiveStartDate, 'onChange'); }); @@ -587,7 +705,7 @@ describe('Calendar', () => { const activeStartDate = new Date(2017, 0, 1); const newActiveStartDate = new Date(2017, 0, 1); const onActiveStartDateChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setActiveStartDate(newActiveStartDate, 'onChange'); }); @@ -609,7 +731,7 @@ describe('Calendar', () => { const value = new Date(2017, 0, 1); const newActiveStartDate = new Date(2017, 0, 1); const onActiveStartDateChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setActiveStartDate(newActiveStartDate, 'onChange'); }); @@ -634,7 +760,7 @@ describe('Calendar', () => { const activeStartDate = new Date(2017, 0, 1); const newView = 'year'; const onViewChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setStateAndCallCallbacks({ action: 'onChange', activeStartDate, @@ -668,7 +798,7 @@ describe('Calendar', () => { const view = 'year'; const newView = 'month'; const onViewChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { ); act(() => { - instance.current.setStateAndCallCallbacks({ action, activeStartDate, view: newView }); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + + instance.current.setStateAndCallCallbacks({ + action, + activeStartDate, + view: newView, + }); }); expect(onViewChange).toHaveBeenCalledWith({ @@ -698,11 +836,15 @@ describe('Calendar', () => { const view = 'year'; const newView = 'year'; const onViewChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); act(() => { + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + instance.current.setStateAndCallCallbacks({ action, activeStartDate, @@ -717,10 +859,14 @@ describe('Calendar', () => { describe('calls onChange properly', () => { it('calls onChange function returning the beginning of selected period by default', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -732,10 +878,14 @@ describe('Calendar', () => { it('calls onChange function returning the beginning of the selected period when returnValue is set to "start"', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -747,10 +897,14 @@ describe('Calendar', () => { it('calls onChange function returning the end of the selected period when returnValue is set to "end"', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -762,10 +916,14 @@ describe('Calendar', () => { it('calls onChange function returning the beginning of selected period when returnValue is set to "range"', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -780,7 +938,7 @@ describe('Calendar', () => { it('calls onChange function returning the beginning of selected period, but no earlier than minDate', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { />, ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -803,7 +965,7 @@ describe('Calendar', () => { it('calls onChange function returning the beginning of selected period, but no later than maxDate', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { />, ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -826,7 +992,7 @@ describe('Calendar', () => { it('calls onChange function returning the end of selected period, but no earlier than minDate', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { />, ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -849,7 +1019,7 @@ describe('Calendar', () => { it('calls onChange function returning the end of selected period, but no later than maxDate', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { />, ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -872,10 +1046,14 @@ describe('Calendar', () => { it('does not call onChange function returning a range when selected one piece of a range by default', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -887,7 +1065,7 @@ describe('Calendar', () => { it('does not call onChange function returning a range when selected one piece of a range given allowPartialRange = false', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( { />, ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -910,12 +1092,16 @@ describe('Calendar', () => { it('calls onChange function returning a partial range when selected one piece of a range given allowPartialRange = true', () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render( , ); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -928,10 +1114,14 @@ describe('Calendar', () => { it('calls onChange function returning a range when selected two pieces of a range', async () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -951,10 +1141,14 @@ describe('Calendar', () => { it('calls onChange function returning a range when selected reversed two pieces of a range', async () => { const onChange = vi.fn(); - const instance = createRef(); + const instance = createRef(); render(); + if (!instance.current) { + throw new Error('Calendar ref is not set'); + } + const { onChange: onChangeInternal } = instance.current; act(() => { @@ -1008,7 +1202,7 @@ describe('Calendar', () => { const { container } = render(); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName('Long date'); @@ -1029,7 +1223,9 @@ describe('Calendar', () => { const { container } = render(); - const weekday = container.querySelector('.react-calendar__month-view__weekdays__weekday'); + const weekday = container.querySelector( + '.react-calendar__month-view__weekdays__weekday', + ) as HTMLDivElement; const abbr = weekday.querySelector('abbr'); expect(abbr).toHaveAccessibleName('Weekday'); diff --git a/src/Calendar.jsx b/src/Calendar.tsx similarity index 71% rename from src/Calendar.jsx rename to src/Calendar.tsx index a1bf3d41..097cee6c 100644 --- a/src/Calendar.jsx +++ b/src/Calendar.tsx @@ -20,16 +20,109 @@ import { } from './shared/propTypes'; import { between } from './shared/utils'; +import type { + Action, + CalendarType, + ClassName, + Detail, + LooseValue, + NavigationLabelFunc, + OnArgs, + OnChangeFunc, + OnClickWeekNumberFunc, + TileClassNameFunc, + TileContentFunc, + TileDisabledFunc, + Value, + View, +} from './shared/types'; + +import type { + formatDay as defaultFormatDay, + formatLongDate as defaultFormatLongDate, + formatMonth as defaultFormatMonth, + formatMonthYear as defaultFormatMonthYear, + formatShortWeekday as defaultFormatShortWeekday, + formatWeekday as defaultFormatWeekday, + formatYear as defaultFormatYear, +} from './shared/dateFormatter'; + +// eslint-disable-next-line no-use-before-define +type CalendarProps = typeof Calendar.defaultProps & { + activeStartDate?: Date; + allowPartialRange?: boolean; + calendarType?: CalendarType; + className?: ClassName; + defaultActiveStartDate?: Date; + defaultValue?: LooseValue; + defaultView?: View; + formatDay?: typeof defaultFormatDay; + formatLongDate?: typeof defaultFormatLongDate; + formatMonth?: typeof defaultFormatMonth; + formatMonthYear?: typeof defaultFormatMonthYear; + formatShortWeekday?: typeof defaultFormatShortWeekday; + formatWeekday?: typeof defaultFormatWeekday; + formatYear?: typeof defaultFormatYear; + goToRangeStartOnSelect?: boolean; + inputRef?: React.Ref; + locale?: string; + maxDate?: Date; + maxDetail?: Detail; + minDate?: Date; + minDetail?: Detail; + navigationAriaLabel?: string; + navigationAriaLive?: 'off' | 'polite' | 'assertive'; + navigationLabel?: NavigationLabelFunc; + next2AriaLabel?: string; + next2Label?: React.ReactNode; + nextAriaLabel?: string; + nextLabel?: React.ReactNode; + onActiveStartDateChange?: ({ action, activeStartDate, value, view }: OnArgs) => void; + onChange?: (value: Value, event: React.MouseEvent) => void; + onClickDay?: OnChangeFunc; + onClickDecade?: OnChangeFunc; + onClickMonth?: OnChangeFunc; + onClickWeekNumber?: OnClickWeekNumberFunc; + onClickYear?: OnChangeFunc; + onDrillDown?: () => void; + onDrillUp?: () => void; + onViewChange?: ({ action, activeStartDate, value, view }: OnArgs) => void; + prev2AriaLabel?: string; + prev2Label?: React.ReactNode; + prevAriaLabel?: string; + prevLabel?: React.ReactNode; + returnValue?: 'start' | 'end' | 'range'; + selectRange?: boolean; + showDoubleView?: boolean; + showFixedNumberOfWeeks?: boolean; + showNavigation?: boolean; + showNeighboringMonth?: boolean; + showWeekNumbers?: boolean; + tileClassName?: TileClassNameFunc | ClassName; + tileContent?: TileContentFunc | React.ReactNode; + tileDisabled?: TileDisabledFunc; + value?: LooseValue; + view?: View; +}; + +type CalendarState = { + action?: Action; + activeStartDate?: Date | null; + hover?: Date | null; + value?: Value; + view?: View; +}; + const defaultMinDate = new Date(); defaultMinDate.setFullYear(1, 0, 1); defaultMinDate.setHours(0, 0, 0, 0); const defaultMaxDate = new Date(8.64e15); const baseClassName = 'react-calendar'; -const allViews = ['century', 'decade', 'year', 'month']; -const allValueTypes = [...allViews.slice(1), 'day']; +const allViews = ['century', 'decade', 'year', 'month'] as const; +const allValueTypes = ['decade', 'year', 'month', 'day'] as const; -function toDate(value) { +function toDate(value: Date | string): Date { if (value instanceof Date) { return value; } @@ -40,14 +133,14 @@ function toDate(value) { /** * Returns views array with disallowed values cut off. */ -function getLimitedViews(minDetail, maxDetail) { +function getLimitedViews(minDetail: Detail, maxDetail: Detail) { return allViews.slice(allViews.indexOf(minDetail), allViews.indexOf(maxDetail) + 1); } /** * Determines whether a given view is allowed with currently applied settings. */ -function isViewAllowed(view, minDetail, maxDetail) { +function isViewAllowed(view: Detail, minDetail: Detail, maxDetail: Detail) { const views = getLimitedViews(minDetail, maxDetail); return views.indexOf(view) !== -1; @@ -57,7 +150,11 @@ function isViewAllowed(view, minDetail, maxDetail) { * Gets either provided view if allowed by minDetail and maxDetail, or gets * the default view if not allowed. */ -function getView(view, minDetail, maxDetail) { +function getView(view: View | undefined, minDetail: Detail, maxDetail: Detail): View { + if (!view) { + return maxDetail; + } + if (isViewAllowed(view, minDetail, maxDetail)) { return view; } @@ -68,16 +165,17 @@ function getView(view, minDetail, maxDetail) { /** * Returns value type that can be returned with currently applied settings. */ -function getValueType(maxDetail) { - return allValueTypes[allViews.indexOf(maxDetail)]; -} +function getValueType(view: typeof allViews[T]): typeof allValueTypes[T] { + const index = allViews.indexOf(view) as T; -function getValue(value, index) { - if (!value) { - return null; - } + return allValueTypes[index]; +} - const rawValue = Array.isArray(value) && value.length === 2 ? value[index] : value; +function getValue( + value: string | Date | null | undefined | (string | Date | null | undefined)[], + index: 0 | 1, +): Date | null { + const rawValue = Array.isArray(value) ? value[index] : value; if (!rawValue) { return null; @@ -92,7 +190,14 @@ function getValue(value, index) { return valueDate; } -function getDetailValue({ value, minDate, maxDate, maxDetail }, index) { +type DetailArgs = { + value?: LooseValue; + minDate: Date; + maxDate: Date; + maxDetail: Detail; +}; + +function getDetailValue({ value, minDate, maxDate, maxDetail }: DetailArgs, index: 0 | 1) { const valuePiece = getValue(value, index); if (!valuePiece) { @@ -100,6 +205,7 @@ function getDetailValue({ value, minDate, maxDate, maxDetail }, index) { } const valueType = getValueType(maxDetail); + const detailValueFrom = (() => { switch (index) { case 0: @@ -114,11 +220,11 @@ function getDetailValue({ value, minDate, maxDate, maxDetail }, index) { return between(detailValueFrom, minDate, maxDate); } -const getDetailValueFrom = (args) => getDetailValue(args, 0); +const getDetailValueFrom = (args: DetailArgs) => getDetailValue(args, 0); -const getDetailValueTo = (args) => getDetailValue(args, 1); +const getDetailValueTo = (args: DetailArgs) => getDetailValue(args, 1); -const getDetailValueArray = (args) => { +const getDetailValueArray = (args: DetailArgs) => { const { value } = args; if (Array.isArray(value)) { @@ -128,7 +234,12 @@ const getDetailValueArray = (args) => { return [getDetailValueFrom, getDetailValueTo].map((fn) => fn(args)); }; -function getActiveStartDate(props) { +function getActiveStartDate( + props: DetailArgs & { + minDetail: Detail; + view?: View; + }, +) { const { maxDate, maxDetail, minDate, minDetail, value, view } = props; const rangeType = getView(view, minDetail, maxDetail); @@ -143,7 +254,7 @@ function getActiveStartDate(props) { return getBegin(rangeType, valueFrom); } -function getInitialActiveStartDate(props) { +function getInitialActiveStartDate(props: CalendarProps) { const { activeStartDate, defaultActiveStartDate, @@ -172,12 +283,14 @@ function getInitialActiveStartDate(props) { }); } -const getIsSingleValue = (value) => value && [].concat(value).length === 1; +function getIsSingleValue(value: T | T[]): value is T { + return value && (!Array.isArray(value) || value.length === 1); +} const isActiveStartDate = PropTypes.instanceOf(Date); const isLooseValue = PropTypes.oneOfType([PropTypes.string, isValue]); -export default class Calendar extends Component { +export default class Calendar extends Component { static defaultProps = { goToRangeStartOnSelect: true, maxDate: defaultMaxDate, @@ -246,9 +359,13 @@ export default class Calendar extends Component { view: isView, }; - state = { + state: Readonly = { activeStartDate: this.props.defaultActiveStartDate, - value: this.props.defaultValue, + hover: null, + value: + typeof this.props.defaultValue === 'string' + ? toDate(this.props.defaultValue) + : this.props.defaultValue, view: this.props.defaultView, }; @@ -259,16 +376,24 @@ export default class Calendar extends Component { return activeStartDateProps || activeStartDateState || getInitialActiveStartDate(this.props); } - get value() { + get value(): Value { const { selectRange, value: valueProps } = this.props; const { value: valueState } = this.state; - // In the middle of range selection, use value from state - if (selectRange && getIsSingleValue(valueState)) { - return valueState; + const rawValue = (() => { + // In the middle of range selection, use value from state + if (selectRange && getIsSingleValue(valueState)) { + return valueState; + } + + return valueProps !== undefined ? valueProps : valueState; + })(); + + if (!rawValue) { + return null; } - return valueProps !== undefined ? valueProps : valueState; + return typeof rawValue === 'string' ? toDate(rawValue) : rawValue; } get valueType() { @@ -312,7 +437,7 @@ export default class Calendar extends Component { /** * Gets current value in a desired format. */ - getProcessedValue(value) { + getProcessedValue(value: Date) { const { minDate, maxDate, maxDetail, returnValue } = this.props; const processFunction = (() => { @@ -336,14 +461,25 @@ export default class Calendar extends Component { }); } - setStateAndCallCallbacks = (nextState, event, callback) => { + setStateAndCallCallbacks = ( + nextState: { + action: Action; + activeStartDate: Date | null; + value?: Value; + view?: View; + }, + event?: React.MouseEvent | undefined, + callback?: ({ action, activeStartDate, value, view }: OnArgs) => void, + ) => { const { activeStartDate: previousActiveStartDate, view: previousView } = this; const { allowPartialRange, onActiveStartDateChange, onChange, onViewChange, selectRange } = this.props; const prevArgs = { + action: undefined, activeStartDate: previousActiveStartDate, + value: undefined, view: previousView, }; @@ -351,11 +487,11 @@ export default class Calendar extends Component { const args = { action: nextState.action, activeStartDate: nextState.activeStartDate || this.activeStartDate, - value: nextState.value || this.value, - view: nextState.view || this.view, + value: ('value' in nextState && nextState.value) || this.value, + view: ('view' in nextState && nextState.view) || this.view, }; - function shouldUpdate(key) { + function shouldUpdate(key: keyof OnArgs) { // Key must exist, and… if (!(key in nextState)) { return false; @@ -387,16 +523,24 @@ export default class Calendar extends Component { if (shouldUpdate('value')) { if (onChange) { + if (!event) { + throw new Error('event is required'); + } + if (selectRange) { const isSingleValue = getIsSingleValue(nextState.value); if (!isSingleValue) { - onChange(nextState.value, event); + onChange(nextState.value || null, event); } else if (allowPartialRange) { - onChange([nextState.value], event); + if (Array.isArray(nextState.value)) { + throw new Error('value must not be an array'); + } + + onChange([nextState.value || null], event); } } else { - onChange(nextState.value, event); + onChange(nextState.value || null, event); } } } @@ -408,14 +552,14 @@ export default class Calendar extends Component { /** * Called when the user uses navigation buttons. */ - setActiveStartDate = (nextActiveStartDate, action) => { + setActiveStartDate = (nextActiveStartDate: Date, action: Action) => { this.setStateAndCallCallbacks({ action, activeStartDate: nextActiveStartDate, }); }; - drillDown = (nextActiveStartDate, event) => { + drillDown = (nextActiveStartDate: Date, event: React.MouseEvent) => { if (!this.drillDownAvailable) { return; } @@ -469,7 +613,7 @@ export default class Calendar extends Component { ); }; - onChange = (value, event) => { + onChange = (value: Date, event: React.MouseEvent) => { const { value: previousValue } = this; const { goToRangeStartOnSelect, selectRange } = this.props; @@ -477,15 +621,24 @@ export default class Calendar extends Component { const isFirstValueInRange = selectRange && !getIsSingleValue(previousValue); - let nextValue; + let nextValue: Value; if (selectRange) { // Range selection turned on const { valueType } = this; + if (isFirstValueInRange) { // Value has 0 or 2 elements - either way we're starting a new array // First value nextValue = getBegin(valueType, value); } else { + if (!previousValue) { + throw new Error('previousValue is required'); + } + + if (Array.isArray(previousValue)) { + throw new Error('previousValue must not be an array'); + } + // Second value nextValue = getValueRange(valueType, previousValue, value); } @@ -519,7 +672,7 @@ export default class Calendar extends Component { ); }; - onClickTile = (value, event) => { + onClickTile = (value: Date, event: React.MouseEvent) => { const { view } = this; const { onClickDay, onClickDecade, onClickMonth, onClickYear } = this.props; @@ -541,7 +694,7 @@ export default class Calendar extends Component { if (callback) callback(value, event); }; - onMouseOver = (value) => { + onMouseOver = (value: Date) => { this.setState((prevState) => { if (prevState.hover && prevState.hover.getTime() === value.getTime()) { return null; @@ -555,7 +708,7 @@ export default class Calendar extends Component { this.setState({ hover: null }); }; - renderContent(next) { + renderContent(next?: boolean) { const { activeStartDate: currentActiveStartDate, onMouseOver, valueType, value, view } = this; const { calendarType, @@ -706,7 +859,7 @@ export default class Calendar extends Component { render() { const { className, inputRef, selectRange, showDoubleView } = this.props; const { onMouseLeave, value } = this; - const valueArray = [].concat(value); + const valueArray = Array.isArray(value) ? value : [value]; return (
{this.renderContent()} {showDoubleView ? this.renderContent(true) : null} diff --git a/src/Calendar/Navigation.spec.jsx b/src/Calendar/Navigation.spec.tsx similarity index 88% rename from src/Calendar/Navigation.spec.jsx rename to src/Calendar/Navigation.spec.tsx index 7198317d..f3b2301e 100644 --- a/src/Calendar/Navigation.spec.jsx +++ b/src/Calendar/Navigation.spec.tsx @@ -16,12 +16,14 @@ describe('Navigation', () => { // Intentionally empty }, views: allViews, - }; + view: 'month', + } satisfies React.ComponentProps; it('renders prev2, prev, drill up, next and next2 buttons', () => { const { container } = render(); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; const prev2 = children[0]; const prev = children[1]; @@ -40,7 +42,8 @@ describe('Navigation', () => { it('renders prev, drill up, next and buttons only for century view', () => { const { container } = render(); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; const prev = children[0]; const drillUp = children[1]; @@ -104,7 +107,8 @@ describe('Navigation', () => { />, ); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; const prev2 = children[0]; const prev = children[1]; @@ -122,7 +126,8 @@ describe('Navigation', () => { , ); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; const navigation = children[2]; @@ -142,7 +147,8 @@ describe('Navigation', () => { />, ); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; const prev2 = children[0]; const prev = children[1]; @@ -162,7 +168,9 @@ describe('Navigation', () => { const { container } = render(); - const button = container.querySelector('button.react-calendar__navigation__label'); + const button = container.querySelector( + 'button.react-calendar__navigation__label', + ) as HTMLButtonElement; fireEvent.click(button); @@ -178,10 +186,10 @@ describe('Navigation', () => { const arrows = container.querySelectorAll('button.react-calendar__navigation__arrow'); - const prev2 = arrows[0]; - const prev = arrows[1]; - const next = arrows[2]; - const next2 = arrows[3]; + const prev2 = arrows[0] as HTMLButtonElement; + const prev = arrows[1] as HTMLButtonElement; + const next = arrows[2] as HTMLButtonElement; + const next2 = arrows[3] as HTMLButtonElement; fireEvent.click(prev2); fireEvent.click(prev); @@ -193,7 +201,7 @@ describe('Navigation', () => { describe('month navigation', () => { const monthSetActiveStartDateFn = vi.fn(); - let monthViewArrows; + let monthViewArrows: NodeListOf; beforeEach(() => { const { container: monthContainer } = render( @@ -208,7 +216,7 @@ describe('Navigation', () => { }); it('jumps 12 months back on prev2 button click for month view', () => { - const prev2 = monthViewArrows[0]; + const prev2 = monthViewArrows[0] as HTMLButtonElement; fireEvent.click(prev2); @@ -216,7 +224,7 @@ describe('Navigation', () => { }); it('jumps 1 month back on prev button click for month view', () => { - const prev = monthViewArrows[1]; + const prev = monthViewArrows[1] as HTMLButtonElement; fireEvent.click(prev); @@ -224,7 +232,7 @@ describe('Navigation', () => { }); it('jumps 1 month forward on next button click for month view', () => { - const next = monthViewArrows[2]; + const next = monthViewArrows[2] as HTMLButtonElement; fireEvent.click(next); @@ -232,7 +240,7 @@ describe('Navigation', () => { }); it('jumps 12 months forward on next2 button click for month view', () => { - const next2 = monthViewArrows[3]; + const next2 = monthViewArrows[3] as HTMLButtonElement; fireEvent.click(next2); @@ -242,7 +250,7 @@ describe('Navigation', () => { describe('year navigation', () => { const yearSetActiveStartDateFn = vi.fn(); - let yearViewArrows; + let yearViewArrows: NodeListOf; beforeEach(() => { const { container: yearContainer } = render( @@ -253,7 +261,7 @@ describe('Navigation', () => { }); it('jumps 10 years back on prev2 button click for year view', () => { - const prev2 = yearViewArrows[0]; + const prev2 = yearViewArrows[0] as HTMLButtonElement; fireEvent.click(prev2); @@ -261,7 +269,7 @@ describe('Navigation', () => { }); it('jumps 1 year back on prev button click for year view', () => { - const prev2 = yearViewArrows[1]; + const prev2 = yearViewArrows[1] as HTMLButtonElement; fireEvent.click(prev2); @@ -269,7 +277,7 @@ describe('Navigation', () => { }); it('jumps 1 year forward on next button click for year view', () => { - const next = yearViewArrows[2]; + const next = yearViewArrows[2] as HTMLButtonElement; fireEvent.click(next); @@ -277,7 +285,7 @@ describe('Navigation', () => { }); it('jumps 10 years forward on next2 button click for year view', () => { - const next2 = yearViewArrows[3]; + const next2 = yearViewArrows[3] as HTMLButtonElement; fireEvent.click(next2); @@ -287,7 +295,7 @@ describe('Navigation', () => { describe('decade navigation', () => { const decadeSetActiveStartDateFn = vi.fn(); - let decadeViewArrows; + let decadeViewArrows: NodeListOf; beforeEach(() => { const { container: decadeContainer } = render( @@ -304,7 +312,7 @@ describe('Navigation', () => { }); it('jumps 10 decades back on prev2 button click for decade view', () => { - const prev2 = decadeViewArrows[0]; + const prev2 = decadeViewArrows[0] as HTMLButtonElement; fireEvent.click(prev2); @@ -312,7 +320,7 @@ describe('Navigation', () => { }); it('jumps 1 decade back on prev button click for decade view', () => { - const prev = decadeViewArrows[1]; + const prev = decadeViewArrows[1] as HTMLButtonElement; fireEvent.click(prev); @@ -320,7 +328,7 @@ describe('Navigation', () => { }); it('jumps 1 decade forward on next button click for decade view', () => { - const next = decadeViewArrows[2]; + const next = decadeViewArrows[2] as HTMLButtonElement; fireEvent.click(next); @@ -328,7 +336,7 @@ describe('Navigation', () => { }); it('jumps 10 decades forward on next2 button click for decade view', () => { - const next2 = decadeViewArrows[3]; + const next2 = decadeViewArrows[3] as HTMLButtonElement; fireEvent.click(next2); @@ -338,7 +346,7 @@ describe('Navigation', () => { describe('century navigation', () => { const centurySetActiveStartDateFn = vi.fn(); - let centuryViewArrows; + let centuryViewArrows: NodeListOf; beforeEach(() => { const { container: centuryContainer } = render( @@ -355,7 +363,7 @@ describe('Navigation', () => { }); it('jumps 1 century back on prev button click for century view', () => { - const prev = centuryViewArrows[0]; + const prev = centuryViewArrows[0] as HTMLButtonElement; fireEvent.click(prev); @@ -363,7 +371,7 @@ describe('Navigation', () => { }); it('jumps 1 century forward on next button click for century view', () => { - const next = centuryViewArrows[1]; + const next = centuryViewArrows[1] as HTMLButtonElement; fireEvent.click(next); diff --git a/src/Calendar/Navigation.jsx b/src/Calendar/Navigation.tsx similarity index 79% rename from src/Calendar/Navigation.jsx rename to src/Calendar/Navigation.tsx index 2357fb7c..e57807fd 100644 --- a/src/Calendar/Navigation.jsx +++ b/src/Calendar/Navigation.tsx @@ -18,8 +18,35 @@ import { } from '../shared/dateFormatter'; import { isView, isViews } from '../shared/propTypes'; +import type { Action, NavigationLabelFunc, RangeType } from '../shared/types'; + const className = 'react-calendar__navigation'; +type NavigationProps = { + activeStartDate: Date; + drillUp: () => void; + formatMonthYear?: typeof defaultFormatMonthYear; + formatYear?: typeof defaultFormatYear; + locale?: string; + maxDate?: Date; + minDate?: Date; + navigationAriaLabel?: string; + navigationAriaLive?: 'off' | 'polite' | 'assertive'; + navigationLabel?: NavigationLabelFunc; + next2AriaLabel?: string; + next2Label?: React.ReactNode; + nextAriaLabel?: string; + nextLabel?: React.ReactNode; + prev2AriaLabel?: string; + prev2Label?: React.ReactNode; + prevAriaLabel?: string; + prevLabel?: React.ReactNode; + setActiveStartDate: (nextActiveStartDate: Date, action: Action) => void; + showDoubleView?: boolean; + view: RangeType; + views: string[]; +}; + export default function Navigation({ activeStartDate, drillUp, @@ -43,15 +70,18 @@ export default function Navigation({ showDoubleView, view, views, -}) { +}: NavigationProps) { const drillUpAvailable = views.indexOf(view) > 0; const shouldShowPrevNext2Buttons = view !== 'century'; const previousActiveStartDate = getBeginPrevious(view, activeStartDate); - const previousActiveStartDate2 = - shouldShowPrevNext2Buttons && getBeginPrevious2(view, activeStartDate); + const previousActiveStartDate2 = shouldShowPrevNext2Buttons + ? getBeginPrevious2(view, activeStartDate) + : undefined; const nextActiveStartDate = getBeginNext(view, activeStartDate); - const nextActiveStartDate2 = shouldShowPrevNext2Buttons && getBeginNext2(view, activeStartDate); + const nextActiveStartDate2 = shouldShowPrevNext2Buttons + ? getBeginNext2(view, activeStartDate) + : undefined; const prevButtonDisabled = (() => { if (previousActiveStartDate.getFullYear() < 0) { @@ -64,7 +94,7 @@ export default function Navigation({ const prev2ButtonDisabled = shouldShowPrevNext2Buttons && (() => { - if (previousActiveStartDate2.getFullYear() < 0) { + if ((previousActiveStartDate2 as Date).getFullYear() < 0) { return true; } const previousActiveEndDate = getEndPrevious2(view, activeStartDate); @@ -74,14 +104,14 @@ export default function Navigation({ const nextButtonDisabled = maxDate && maxDate < nextActiveStartDate; const next2ButtonDisabled = - shouldShowPrevNext2Buttons && maxDate && maxDate < nextActiveStartDate2; + shouldShowPrevNext2Buttons && maxDate && maxDate < (nextActiveStartDate2 as Date); function onClickPrevious() { setActiveStartDate(previousActiveStartDate, 'prev'); } function onClickPrevious2() { - setActiveStartDate(previousActiveStartDate2, 'prev2'); + setActiveStartDate(previousActiveStartDate2 as Date, 'prev2'); } function onClickNext() { @@ -89,10 +119,10 @@ export default function Navigation({ } function onClickNext2() { - setActiveStartDate(nextActiveStartDate2, 'next2'); + setActiveStartDate(nextActiveStartDate2 as Date, 'next2'); } - function renderLabel(date) { + function renderLabel(date: Date) { const label = (() => { switch (view) { case 'century': @@ -112,7 +142,7 @@ export default function Navigation({ ? navigationLabel({ date, label, - locale: locale || getUserLocale(), + locale: locale || getUserLocale() || undefined, view, }) : label; diff --git a/src/CenturyView.spec.jsx b/src/CenturyView.spec.tsx similarity index 92% rename from src/CenturyView.spec.jsx rename to src/CenturyView.spec.tsx index 31e76359..a8ea1639 100644 --- a/src/CenturyView.spec.jsx +++ b/src/CenturyView.spec.tsx @@ -8,7 +8,7 @@ import CenturyView from './CenturyView'; describe('CenturyView', () => { const defaultProps = { activeStartDate: new Date(2017, 0, 1), - }; + } satisfies React.ComponentProps; it('renders proper view when given activeStartDate', () => { const activeStartDate = new Date(2001, 0, 1); @@ -44,7 +44,7 @@ describe('CenturyView', () => { it('applies tileClassName to its tiles conditionally when given a function that returns a string', () => { const activeStartDate = new Date(2001, 0, 1); - const tileClassNameFn = ({ date }) => { + const tileClassNameFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return 'firstDayOfTheMonth'; } @@ -77,7 +77,7 @@ describe('CenturyView', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); expect(firstDayTileContent).toBeInTheDocument(); @@ -85,7 +85,7 @@ describe('CenturyView', () => { it('renders tileContent in its tiles conditionally when given a function that returns a node', () => { const activeStartDate = new Date(2001, 0, 1); - const tileContentFn = ({ date }) => { + const tileContentFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return
; } @@ -104,8 +104,8 @@ describe('CenturyView', () => { const tiles = container.querySelectorAll('.react-calendar__tile'); - const firstDayTile = tiles[0]; - const secondDayTile = tiles[1]; + const firstDayTile = tiles[0] as HTMLDivElement; + const secondDayTile = tiles[1] as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); const secondDayTileContent = secondDayTile.querySelector('.testContent'); diff --git a/src/CenturyView.jsx b/src/CenturyView.tsx similarity index 64% rename from src/CenturyView.jsx rename to src/CenturyView.tsx index 4a3720d5..759ec4b7 100644 --- a/src/CenturyView.jsx +++ b/src/CenturyView.tsx @@ -2,7 +2,9 @@ import React from 'react'; import Decades from './CenturyView/Decades'; -export default function CenturyView(props) { +type CenturyViewProps = React.ComponentProps; + +export default function CenturyView(props: CenturyViewProps) { function renderDecades() { return ; } diff --git a/src/CenturyView/Decade.spec.jsx b/src/CenturyView/Decade.spec.tsx similarity index 95% rename from src/CenturyView/Decade.spec.jsx rename to src/CenturyView/Decade.spec.tsx index 710a527c..4b1164f4 100644 --- a/src/CenturyView/Decade.spec.jsx +++ b/src/CenturyView/Decade.spec.tsx @@ -8,6 +8,12 @@ const tileProps = { activeStartDate: new Date(2018, 0, 1), classes: ['react-calendar__tile'], date: new Date(2011, 0, 1), + onClick: () => { + // Intentionally empty + }, + onMouseOver: () => { + // Intentionally empty + }, point: 2011, }; @@ -30,9 +36,7 @@ describe('Decade', () => { }); it('renders component without abbreviation', () => { - const { container } = render( - , - ); + const { container } = render(); const abbr = container.querySelector('abbr'); @@ -86,7 +90,7 @@ describe('Decade', () => { const { container } = render(); - fireEvent.click(container.querySelector('.react-calendar__tile')); + fireEvent.click(container.querySelector('.react-calendar__tile') as HTMLDivElement); expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalledWith(date, expect.any(Object)); @@ -98,7 +102,7 @@ describe('Decade', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.mouseOver(tile); expect(onMouseOver).toHaveBeenCalled(); @@ -111,7 +115,7 @@ describe('Decade', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.focus(tile); expect(onMouseOver).toHaveBeenCalled(); diff --git a/src/CenturyView/Decade.jsx b/src/CenturyView/Decade.tsx similarity index 67% rename from src/CenturyView/Decade.jsx rename to src/CenturyView/Decade.tsx index 7c494b70..441d07a5 100644 --- a/src/CenturyView/Decade.jsx +++ b/src/CenturyView/Decade.tsx @@ -10,13 +10,25 @@ import { tileProps } from '../shared/propTypes'; const className = 'react-calendar__century-view__decades__decade'; -export default function Decade({ classes, formatYear = defaultFormatYear, ...otherProps }) { +type DecadeProps = { + classes?: string[]; + formatYear?: typeof defaultFormatYear; +} & Omit< + React.ComponentProps, + 'children' | 'maxDateTransform' | 'minDateTransform' | 'view' +>; + +export default function Decade({ + classes = [], + formatYear = defaultFormatYear, + ...otherProps +}: DecadeProps) { const { date, locale } = otherProps; return ( ; + +export default function Decades(props: DecadesProps) { const { activeStartDate } = props; const start = getBeginOfCenturyYear(activeStartDate); const end = start + 99; diff --git a/src/DecadeView.spec.jsx b/src/DecadeView.spec.tsx similarity index 91% rename from src/DecadeView.spec.jsx rename to src/DecadeView.spec.tsx index 1bd2b0c5..99b9f588 100644 --- a/src/DecadeView.spec.jsx +++ b/src/DecadeView.spec.tsx @@ -7,7 +7,7 @@ import DecadeView from './DecadeView'; describe('DecadeView', () => { const defaultProps = { activeStartDate: new Date(2017, 0, 1), - }; + } satisfies React.ComponentProps; it('renders proper view when given activeStartDate', () => { const activeStartDate = new Date(2011, 0, 1); @@ -39,7 +39,7 @@ describe('DecadeView', () => { it('applies tileClassName to its tiles conditionally when given a function that returns a string', () => { const activeStartDate = new Date(2011, 0, 1); - const tileClassNameFn = ({ date }) => { + const tileClassNameFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return 'firstDayOfTheMonth'; } @@ -72,7 +72,7 @@ describe('DecadeView', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); expect(firstDayTileContent).toBeInTheDocument(); @@ -80,7 +80,7 @@ describe('DecadeView', () => { it('renders tileContent in its tiles conditionally when given a function that returns a node', () => { const activeStartDate = new Date(2011, 0, 1); - const tileContentFn = ({ date }) => { + const tileContentFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return
; } @@ -99,8 +99,8 @@ describe('DecadeView', () => { const tiles = container.querySelectorAll('.react-calendar__tile'); - const firstDayTile = tiles[0]; - const secondDayTile = tiles[1]; + const firstDayTile = tiles[0] as HTMLDivElement; + const secondDayTile = tiles[1] as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); const secondDayTileContent = secondDayTile.querySelector('.testContent'); diff --git a/src/DecadeView.jsx b/src/DecadeView.tsx similarity index 63% rename from src/DecadeView.jsx rename to src/DecadeView.tsx index ed6936a0..4914dad3 100644 --- a/src/DecadeView.jsx +++ b/src/DecadeView.tsx @@ -2,7 +2,9 @@ import React from 'react'; import Years from './DecadeView/Years'; -export default function DecadeView(props) { +type DecadeViewProps = React.ComponentProps; + +export default function DecadeView(props: DecadeViewProps) { function renderYears() { return ; } diff --git a/src/DecadeView/Year.spec.jsx b/src/DecadeView/Year.spec.tsx similarity index 96% rename from src/DecadeView/Year.spec.jsx rename to src/DecadeView/Year.spec.tsx index 5382b2c4..1ad999c4 100644 --- a/src/DecadeView/Year.spec.jsx +++ b/src/DecadeView/Year.spec.tsx @@ -8,6 +8,12 @@ const tileProps = { activeStartDate: new Date(2018, 0, 1), classes: ['react-calendar__tile'], date: new Date(2018, 0, 1), + onClick: () => { + // Intentionally empty + }, + onMouseOver: () => { + // Intentionally empty + }, point: 2018, }; @@ -30,7 +36,7 @@ describe('Year', () => { }); it('renders component without abbreviation', () => { - const { container } = render(); + const { container } = render(); const abbr = container.querySelector('abbr'); @@ -84,7 +90,7 @@ describe('Year', () => { const { container } = render(); - fireEvent.click(container.querySelector('.react-calendar__tile')); + fireEvent.click(container.querySelector('.react-calendar__tile') as HTMLDivElement); expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalledWith(date, expect.any(Object)); @@ -96,7 +102,7 @@ describe('Year', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.mouseOver(tile); expect(onMouseOver).toHaveBeenCalled(); @@ -109,7 +115,7 @@ describe('Year', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.focus(tile); expect(onMouseOver).toHaveBeenCalled(); diff --git a/src/DecadeView/Year.jsx b/src/DecadeView/Year.tsx similarity index 65% rename from src/DecadeView/Year.jsx rename to src/DecadeView/Year.tsx index 047cd005..8eeaae29 100644 --- a/src/DecadeView/Year.jsx +++ b/src/DecadeView/Year.tsx @@ -9,13 +9,25 @@ import { tileProps } from '../shared/propTypes'; const className = 'react-calendar__decade-view__years__year'; -export default function Year({ classes, formatYear = defaultFormatYear, ...otherProps }) { +type YearProps = { + classes?: string[]; + formatYear?: typeof defaultFormatYear; +} & Omit< + React.ComponentProps, + 'children' | 'maxDateTransform' | 'minDateTransform' | 'view' +>; + +export default function Year({ + classes = [], + formatYear = defaultFormatYear, + ...otherProps +}: YearProps) { const { date, locale } = otherProps; return ( ; + +export default function Years(props: YearsProps) { const { activeStartDate } = props; const start = getBeginOfDecadeYear(activeStartDate); const end = start + 9; diff --git a/src/Flex.spec.jsx b/src/Flex.spec.tsx similarity index 88% rename from src/Flex.spec.jsx rename to src/Flex.spec.tsx index ac349951..8c9b0aec 100644 --- a/src/Flex.spec.jsx +++ b/src/Flex.spec.tsx @@ -44,7 +44,8 @@ describe('Flex', () => { , ); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = wrapper.children; expect(children).toHaveLength(3); expect(children[0]).toHaveTextContent('Hey'); @@ -60,7 +61,8 @@ describe('Flex', () => { , ); - const children = [...container.firstElementChild.children]; + const wrapper = container.firstElementChild as HTMLDivElement; + const children = Array.from(wrapper.children); children.forEach((child) => expect(child).toHaveStyle('flex-basis: 33.333333333333336%')); expect(children[0]).toHaveStyle('margin-left: 33.333333333333336%'); diff --git a/src/Flex.jsx b/src/Flex.tsx similarity index 81% rename from src/Flex.jsx rename to src/Flex.tsx index cdfcd0f2..7ed695d0 100644 --- a/src/Flex.jsx +++ b/src/Flex.tsx @@ -1,7 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -function toPercent(num) { +type FlexProps = React.HTMLAttributes & { + children: React.ReactElement[]; + className?: string; + count: number; + direction?: 'row' | 'column'; + offset?: number; + wrap?: boolean; +}; + +function toPercent(num: number): string { return `${num}%`; } @@ -14,7 +23,7 @@ export default function Flex({ style, wrap, ...otherProps -}) { +}: FlexProps) { return (
{ const defaultProps = { activeStartDate: new Date(2017, 0, 1), - }; + } satisfies React.ComponentProps; it('renders proper view when given activeStartDate', () => { const activeStartDate = new Date(2017, 0, 1); @@ -27,7 +27,7 @@ describe('MonthView', () => { />, ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(activeStartDate)); @@ -47,7 +47,7 @@ describe('MonthView', () => { it('applies tileClassName to its tiles conditionally when given a function that returns a string', () => { const activeStartDate = new Date(2017, 0, 1); - const tileClassNameFn = ({ date }) => { + const tileClassNameFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return 'firstDayOfTheMonth'; } @@ -80,7 +80,7 @@ describe('MonthView', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); expect(firstDayTileContent).toBeInTheDocument(); @@ -88,7 +88,7 @@ describe('MonthView', () => { it('renders tileContent in its tiles conditionally when given a function that returns a node', () => { const activeStartDate = new Date(2017, 0, 1); - const tileContentFn = ({ date }) => { + const tileContentFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return
; } @@ -107,8 +107,8 @@ describe('MonthView', () => { const tiles = container.querySelectorAll('.react-calendar__tile'); - const firstDayTile = tiles[0]; - const secondDayTile = tiles[1]; + const firstDayTile = tiles[0] as HTMLDivElement; + const secondDayTile = tiles[1] as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); const secondDayTileContent = secondDayTile.querySelector('.testContent'); @@ -172,7 +172,9 @@ describe('MonthView', () => { const { container } = render(); - const weekday = container.querySelector('.react-calendar__month-view__weekdays__weekday'); + const weekday = container.querySelector( + '.react-calendar__month-view__weekdays__weekday', + ) as HTMLDivElement; const abbr = weekday.querySelector('abbr'); expect(abbr).toHaveAccessibleName('Weekday'); diff --git a/src/MonthView.jsx b/src/MonthView.tsx similarity index 77% rename from src/MonthView.jsx rename to src/MonthView.tsx index 0e87be8e..43cd0c79 100644 --- a/src/MonthView.jsx +++ b/src/MonthView.tsx @@ -7,17 +7,26 @@ import Weekdays from './MonthView/Weekdays'; import WeekNumbers from './MonthView/WeekNumbers'; import { CALENDAR_TYPES, CALENDAR_TYPE_LOCALES } from './shared/const'; -import { isCalendarType } from './shared/propTypes'; +import { isCalendarType, tileGroupProps } from './shared/propTypes'; -function getCalendarTypeFromLocale(locale) { - return ( - Object.keys(CALENDAR_TYPE_LOCALES).find((calendarType) => - CALENDAR_TYPE_LOCALES[calendarType].includes(locale), - ) || CALENDAR_TYPES.ISO_8601 - ); +import type { CalendarType } from './shared/types'; + +function getCalendarTypeFromLocale(locale: string): CalendarType { + for (const [calendarType, locales] of Object.entries(CALENDAR_TYPE_LOCALES)) { + if (locales.includes(locale)) { + return calendarType as CalendarType; + } + } + + return CALENDAR_TYPES.ISO_8601; } -export default function MonthView(props) { +type MonthViewProps = { + showWeekNumbers?: boolean; +} & React.ComponentProps & + React.ComponentProps; + +export default function MonthView(props: MonthViewProps) { const { activeStartDate, locale, onMouseLeave, showFixedNumberOfWeeks } = props; const { calendarType = getCalendarTypeFromLocale(locale), @@ -86,13 +95,12 @@ export default function MonthView(props) { } MonthView.propTypes = { - activeStartDate: PropTypes.instanceOf(Date).isRequired, + ...tileGroupProps, calendarType: isCalendarType, formatDay: PropTypes.func, formatLongDate: PropTypes.func, formatShortWeekday: PropTypes.func, formatWeekday: PropTypes.func, - locale: PropTypes.string, onClickWeekNumber: PropTypes.func, onMouseLeave: PropTypes.func, showFixedNumberOfWeeks: PropTypes.bool, diff --git a/src/MonthView/Day.spec.jsx b/src/MonthView/Day.spec.tsx similarity index 97% rename from src/MonthView/Day.spec.jsx rename to src/MonthView/Day.spec.tsx index 781257aa..30cb79b0 100644 --- a/src/MonthView/Day.spec.jsx +++ b/src/MonthView/Day.spec.tsx @@ -9,6 +9,12 @@ const tileProps = { classes: ['react-calendar__tile'], currentMonthIndex: 0, date: new Date(2018, 0, 1), + onClick: () => { + // Intentionally empty + }, + onMouseOver: () => { + // Intentionally empty + }, }; describe('Day', () => { @@ -106,7 +112,7 @@ describe('Day', () => { const { container } = render(); - fireEvent.click(container.querySelector('.react-calendar__tile')); + fireEvent.click(container.querySelector('.react-calendar__tile') as HTMLDivElement); expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalledWith(date, expect.any(Object)); @@ -118,7 +124,7 @@ describe('Day', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.mouseOver(tile); expect(onMouseOver).toHaveBeenCalled(); @@ -131,7 +137,7 @@ describe('Day', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.focus(tile); expect(onMouseOver).toHaveBeenCalled(); diff --git a/src/MonthView/Day.jsx b/src/MonthView/Day.tsx similarity index 56% rename from src/MonthView/Day.jsx rename to src/MonthView/Day.tsx index e27165bb..f97dcc56 100644 --- a/src/MonthView/Day.jsx +++ b/src/MonthView/Day.tsx @@ -11,27 +11,53 @@ import { } from '../shared/dateFormatter'; import { tileProps } from '../shared/propTypes'; +import type { CalendarType } from '../shared/types'; + const className = 'react-calendar__month-view__days__day'; +type DayProps = { + calendarType?: CalendarType; + classes?: string[]; + currentMonthIndex: number; + formatDay?: typeof defaultFormatDay; + formatLongDate?: typeof defaultFormatLongDate; +} & Omit< + React.ComponentProps, + 'children' | 'formatAbbr' | 'maxDateTransform' | 'minDateTransform' | 'view' +>; + export default function Day({ calendarType, - classes, + classes = [], currentMonthIndex, formatDay = defaultFormatDay, formatLongDate = defaultFormatLongDate, ...otherProps -}) { +}: DayProps) { const { date, locale } = otherProps; + const classesProps: string[] = []; + + if (classes) { + classesProps.push(...classes); + } + + if (className) { + classesProps.push(className); + } + + if (isWeekend(date, calendarType)) { + classesProps.push(`${className}--weekend`); + } + + if (date.getMonth() !== currentMonthIndex) { + classesProps.push(`${className}--neighboringMonth`); + } + return ( ; + +export default function Days(props: DaysProps) { const { activeStartDate, calendarType } = props; const { showFixedNumberOfWeeks, showNeighboringMonth, ...otherProps } = props; diff --git a/src/MonthView/WeekNumber.jsx b/src/MonthView/WeekNumber.jsx deleted file mode 100644 index 289d0c0e..00000000 --- a/src/MonthView/WeekNumber.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const className = 'react-calendar__tile'; - -export default function WeekNumber({ date, onClickWeekNumber, weekNumber, ...otherProps }) { - const props = { - className, - ...otherProps, - }; - - const children = {weekNumber}; - - return onClickWeekNumber ? ( - - ) : ( -
{children}
- ); -} - -WeekNumber.propTypes = { - date: PropTypes.instanceOf(Date).isRequired, - onClickWeekNumber: PropTypes.func, - weekNumber: PropTypes.node.isRequired, -}; diff --git a/src/MonthView/WeekNumber.spec.jsx b/src/MonthView/WeekNumber.spec.tsx similarity index 87% rename from src/MonthView/WeekNumber.spec.jsx rename to src/MonthView/WeekNumber.spec.tsx index ba25976c..262814d0 100644 --- a/src/MonthView/WeekNumber.spec.jsx +++ b/src/MonthView/WeekNumber.spec.tsx @@ -8,7 +8,7 @@ describe(' component', () => { const defaultProps = { date: new Date(2019, 0, 1), weekNumber: 1, - }; + } satisfies React.ComponentProps; it('renders div by default', () => { const { container } = render(); @@ -29,10 +29,10 @@ describe(' component', () => { }); it('renders weekNumber properly', () => { - const weekNumber = '42'; + const weekNumber = 42; const { container } = render(); - expect(container).toHaveTextContent(weekNumber); + expect(container).toHaveTextContent(`${weekNumber}`); }); }); diff --git a/src/MonthView/WeekNumber.tsx b/src/MonthView/WeekNumber.tsx new file mode 100644 index 00000000..ecb1c26e --- /dev/null +++ b/src/MonthView/WeekNumber.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import type { OnClickWeekNumberFunc } from '../shared/types'; + +const className = 'react-calendar__tile'; + +type ButtonProps = Omit, 'onClick'> & { + onClickWeekNumber: OnClickWeekNumberFunc; +}; + +type DivProps = React.HTMLAttributes & { + onClickWeekNumber?: undefined; +}; + +type WeekNumberProps = (T extends OnClickWeekNumberFunc + ? ButtonProps + : DivProps) & { + date: Date; + weekNumber: number; +}; + +export default function WeekNumber(props: WeekNumberProps) { + const { onClickWeekNumber, weekNumber } = props; + + const children = {weekNumber}; + + if (onClickWeekNumber) { + const { date, onClickWeekNumber, weekNumber, ...otherProps } = props; + + return ( + + ); + } else { + const { date, onClickWeekNumber, weekNumber, ...otherProps } = props; + + return ( +
+ {children} +
+ ); + } +} + +WeekNumber.propTypes = { + date: PropTypes.instanceOf(Date).isRequired, + onClickWeekNumber: PropTypes.func, + weekNumber: PropTypes.node.isRequired, +}; diff --git a/src/MonthView/WeekNumbers.spec.jsx b/src/MonthView/WeekNumbers.spec.tsx similarity index 96% rename from src/MonthView/WeekNumbers.spec.jsx rename to src/MonthView/WeekNumbers.spec.tsx index 0dc1512d..f2716974 100644 --- a/src/MonthView/WeekNumbers.spec.jsx +++ b/src/MonthView/WeekNumbers.spec.tsx @@ -7,7 +7,7 @@ import WeekNumbers from './WeekNumbers'; describe('.react-calendar__month-view__weekNumbers', () => { const defaultProps = { activeStartDate: new Date(2017, 0, 1), - }; + } satisfies Partial>; it('renders proper weekNumbers for a year that starts in week 1 (ISO 8601)', () => { const { container } = render( @@ -110,7 +110,7 @@ describe('.react-calendar__month-view__weekNumbers', () => { />, ); - const firstChild = container.querySelector('button.react-calendar__tile'); + const firstChild = container.querySelector('button.react-calendar__tile') as HTMLButtonElement; fireEvent.click(firstChild); expect(onClickWeekNumber).toHaveBeenCalledWith(52, new Date(2016, 11, 26), expect.any(Object)); @@ -122,7 +122,7 @@ describe('.react-calendar__month-view__weekNumbers', () => { , ); - const firstChild = container.querySelector('button.react-calendar__tile'); + const firstChild = container.querySelector('button.react-calendar__tile') as HTMLButtonElement; fireEvent.click(firstChild); expect(onClickWeekNumber).toHaveBeenCalledWith(1, new Date(2017, 0, 1), expect.any(Object)); diff --git a/src/MonthView/WeekNumbers.jsx b/src/MonthView/WeekNumbers.tsx similarity index 70% rename from src/MonthView/WeekNumbers.jsx rename to src/MonthView/WeekNumbers.tsx index e076aa85..ad487c47 100644 --- a/src/MonthView/WeekNumbers.jsx +++ b/src/MonthView/WeekNumbers.tsx @@ -8,7 +8,17 @@ import Flex from '../Flex'; import { getBeginOfWeek, getDayOfWeek, getWeekNumber } from '../shared/dates'; import { isCalendarType } from '../shared/propTypes'; -export default function WeekNumbers(props) { +import type { CalendarType, OnClickWeekNumberFunc } from '../shared/types'; + +type WeekNumbersProps = { + activeStartDate: Date; + calendarType: CalendarType; + onClickWeekNumber?: OnClickWeekNumberFunc; + onMouseLeave?: () => void; + showFixedNumberOfWeeks?: boolean; +}; + +export default function WeekNumbers(props: WeekNumbersProps) { const { activeStartDate, calendarType, onClickWeekNumber, onMouseLeave, showFixedNumberOfWeeks } = props; @@ -47,14 +57,22 @@ export default function WeekNumbers(props) { onMouseOver={onMouseLeave} style={{ flexBasis: 'calc(100% * (1 / 8)', flexShrink: 0 }} > - {weekNumbers.map((weekNumber, weekIndex) => ( - - ))} + {weekNumbers.map((weekNumber, weekIndex) => { + const date = dates[weekIndex]; + + if (!date) { + throw new Error('date is not defined'); + } + + return ( + + ); + })} ); } diff --git a/src/MonthView/Weekdays.spec.jsx b/src/MonthView/Weekdays.spec.tsx similarity index 84% rename from src/MonthView/Weekdays.spec.jsx rename to src/MonthView/Weekdays.spec.tsx index b9c511fd..8842e345 100644 --- a/src/MonthView/Weekdays.spec.jsx +++ b/src/MonthView/Weekdays.spec.tsx @@ -7,13 +7,13 @@ import Weekdays from './Weekdays'; describe('Weekdays', () => { const defaultProps = { calendarType: 'ISO 8601', - }; + } satisfies React.ComponentProps; it('renders proper weekdays (ISO 8601)', () => { const { container } = render(); const weekdays = container.querySelectorAll('.react-calendar__month-view__weekdays__weekday'); - const firstWeekday = weekdays[0]; + const [firstWeekday] = weekdays as unknown as [HTMLDivElement]; const firstWeekdayAbbr = firstWeekday.querySelector('abbr'); expect(weekdays).toHaveLength(7); @@ -25,7 +25,7 @@ describe('Weekdays', () => { const { container } = render(); const weekdays = container.querySelectorAll('.react-calendar__month-view__weekdays__weekday'); - const firstWeekday = weekdays[0]; + const [firstWeekday] = weekdays as unknown as [HTMLDivElement]; const firstWeekdayAbbr = firstWeekday.querySelector('abbr'); expect(weekdays).toHaveLength(7); @@ -44,7 +44,9 @@ describe('Weekdays', () => { it('renders weekdays with custom weekdays formatting', () => { const { container } = render( 'Weekday'} />); - const firstWeekday = container.querySelector('.react-calendar__month-view__weekdays__weekday'); + const firstWeekday = container.querySelector( + '.react-calendar__month-view__weekdays__weekday', + ) as HTMLDivElement; const firstWeekdayAbbr = firstWeekday.querySelector('abbr'); expect(firstWeekdayAbbr).toHaveAccessibleName('Weekday'); diff --git a/src/MonthView/Weekdays.jsx b/src/MonthView/Weekdays.tsx similarity index 85% rename from src/MonthView/Weekdays.jsx rename to src/MonthView/Weekdays.tsx index fd052641..d7ca03d3 100644 --- a/src/MonthView/Weekdays.jsx +++ b/src/MonthView/Weekdays.tsx @@ -11,11 +11,20 @@ import { formatWeekday as defaultFormatWeekday, } from '../shared/dateFormatter'; import { isCalendarType } from '../shared/propTypes'; +import type { CalendarType } from '../shared/types'; const className = 'react-calendar__month-view__weekdays'; const weekdayClassName = `${className}__weekday`; -export default function Weekdays(props) { +type WeekdaysProps = { + calendarType: CalendarType; + formatShortWeekday?: typeof defaultFormatShortWeekday; + formatWeekday?: typeof defaultFormatWeekday; + locale?: string; + onMouseLeave?: () => void; +}; + +export default function Weekdays(props: WeekdaysProps) { const { calendarType, formatShortWeekday = defaultFormatShortWeekday, diff --git a/src/Tile.spec.jsx b/src/Tile.spec.tsx similarity index 94% rename from src/Tile.spec.jsx rename to src/Tile.spec.tsx index 7e15301b..1b6955c7 100644 --- a/src/Tile.spec.jsx +++ b/src/Tile.spec.tsx @@ -10,9 +10,16 @@ describe(' component', () => { children: '', classes: [], date: new Date(2019, 0, 1), - maxDateTransform: (date) => date, - minDateTransform: (date) => date, - }; + maxDateTransform: (date: Date) => date, + minDateTransform: (date: Date) => date, + onClick: () => { + // Intentionally empty + }, + onMouseOver: () => { + // Intentionally empty + }, + view: 'month', + } satisfies React.ComponentProps; it('renders button properly', () => { const { container } = render(); @@ -25,7 +32,7 @@ describe(' component', () => { const { container } = render(); - const button = container.querySelector('button'); + const button = container.querySelector('button') as HTMLButtonElement; fireEvent.click(button); diff --git a/src/Tile.jsx b/src/Tile.tsx similarity index 56% rename from src/Tile.jsx rename to src/Tile.tsx index 419f0416..386f3526 100644 --- a/src/Tile.jsx +++ b/src/Tile.tsx @@ -4,7 +4,43 @@ import clsx from 'clsx'; import { tileProps } from './shared/propTypes'; -function datesAreDifferent(date1, date2) { +import type { + ClassName, + TileClassNameFunc, + TileContentFunc, + TileDisabledFunc, + View, +} from './shared/types'; + +type TileProps = { + activeStartDate: Date; + children: React.ReactNode; + classes?: ClassName; + date: Date; + formatAbbr?: (locale: string | undefined, date: Date) => string; + locale?: string; + maxDate?: Date; + maxDateTransform: (date: Date) => Date; + minDate?: Date; + minDateTransform: (date: Date) => Date; + onClick: (date: Date, event: React.MouseEvent) => void; + onMouseOver: (date: Date) => void; + style?: React.CSSProperties; + tileClassName?: TileClassNameFunc | ClassName; + tileContent?: TileContentFunc | React.ReactNode; + tileDisabled?: TileDisabledFunc; + view: View; +}; + +type TileState = { + activeStartDateProps?: TileProps['activeStartDate']; + tileClassName?: ClassName; + tileClassNameProps?: TileProps['tileClassName']; + tileContent?: React.ReactNode; + tileContentProps?: TileProps['tileContent']; +}; + +function datesAreDifferent(date1?: Date, date2?: Date) { return ( (date1 && !date2) || (!date1 && date2) || @@ -12,13 +48,7 @@ function datesAreDifferent(date1, date2) { ); } -function getValue(nextProps, prop) { - const { activeStartDate, date, view } = nextProps; - - return typeof prop === 'function' ? prop({ activeStartDate, date, view }) : prop; -} - -export default class Tile extends Component { +export default class Tile extends Component { static propTypes = { ...tileProps, children: PropTypes.node.isRequired, @@ -27,16 +57,19 @@ export default class Tile extends Component { minDateTransform: PropTypes.func.isRequired, }; - static getDerivedStateFromProps(nextProps, prevState) { - const { activeStartDate, tileClassName, tileContent } = nextProps; + static getDerivedStateFromProps(nextProps: TileProps, prevState: TileState) { + const { activeStartDate, date, tileClassName, tileContent, view } = nextProps; + + const nextState: TileState = {}; - const nextState = {}; + const args = { activeStartDate, date, view }; if ( tileClassName !== prevState.tileClassNameProps || datesAreDifferent(activeStartDate, prevState.activeStartDateProps) ) { - nextState.tileClassName = getValue(nextProps, tileClassName); + nextState.tileClassName = + typeof tileClassName === 'function' ? tileClassName(args) : tileClassName; nextState.tileClassNameProps = tileClassName; } @@ -44,7 +77,7 @@ export default class Tile extends Component { tileContent !== prevState.tileContentProps || datesAreDifferent(activeStartDate, prevState.activeStartDateProps) ) { - nextState.tileContent = getValue(nextProps, tileContent); + nextState.tileContent = typeof tileContent === 'function' ? tileContent(args) : tileContent; nextState.tileContentProps = tileContent; } @@ -53,7 +86,7 @@ export default class Tile extends Component { return nextState; } - state = {}; + state: Readonly = {}; render() { const { diff --git a/src/TileGroup.jsx b/src/TileGroup.tsx similarity index 72% rename from src/TileGroup.jsx rename to src/TileGroup.tsx index 50ba422d..9e0cb3ec 100644 --- a/src/TileGroup.jsx +++ b/src/TileGroup.tsx @@ -6,7 +6,24 @@ import Flex from './Flex'; import { getTileClasses } from './shared/utils'; import { tileGroupProps } from './shared/propTypes'; -export default function TileGroup({ +import type { RangeType } from './shared/types'; + +type TileGroupProps = { + className?: string; + count?: number; + dateTransform: (point: number) => Date; + dateType: RangeType; + end: number; + hover?: Date; + offset?: number; + start: number; + step?: number; + tile: T; + value?: Date; + valueType: RangeType; +} & React.ComponentProps; + +export default function TileGroup({ className, count = 3, dateTransform, @@ -20,7 +37,7 @@ export default function TileGroup({ value, valueType, ...tileProps -}) { +}: TileGroupProps) { const tiles = []; for (let point = start; point <= end; point += step) { const date = dateTransform(point); diff --git a/src/YearView.spec.jsx b/src/YearView.spec.tsx similarity index 91% rename from src/YearView.spec.jsx rename to src/YearView.spec.tsx index 13688b12..98ac8d1f 100644 --- a/src/YearView.spec.jsx +++ b/src/YearView.spec.tsx @@ -9,7 +9,7 @@ const { format } = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'nume describe('YearView', () => { const defaultProps = { activeStartDate: new Date(2017, 0, 1), - }; + } satisfies React.ComponentProps; it('renders proper view when given activeStartDate', () => { const activeStartDate = new Date(2017, 0, 1); @@ -18,7 +18,7 @@ describe('YearView', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileTimeAbbr = firstDayTile.querySelector('abbr'); expect(firstDayTileTimeAbbr).toHaveAccessibleName(format(activeStartDate)); @@ -38,7 +38,7 @@ describe('YearView', () => { it('applies tileClassName to its tiles conditionally when given a function that returns a string', () => { const activeStartDate = new Date(2017, 0, 1); - const tileClassNameFn = ({ date }) => { + const tileClassNameFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return 'firstDayOfTheMonth'; } @@ -71,7 +71,7 @@ describe('YearView', () => { , ); - const firstDayTile = container.querySelector('.react-calendar__tile'); + const firstDayTile = container.querySelector('.react-calendar__tile') as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); expect(firstDayTileContent).toBeInTheDocument(); @@ -79,7 +79,7 @@ describe('YearView', () => { it('renders tileContent in its tiles conditionally when given a function that returns a node', () => { const activeStartDate = new Date(2017, 0, 1); - const tileContentFn = ({ date }) => { + const tileContentFn = ({ date }: { date: Date }) => { if (date.getTime() === activeStartDate.getTime()) { return
; } @@ -98,8 +98,8 @@ describe('YearView', () => { const tiles = container.querySelectorAll('.react-calendar__tile'); - const firstDayTile = tiles[0]; - const secondDayTile = tiles[1]; + const firstDayTile = tiles[0] as HTMLDivElement; + const secondDayTile = tiles[1] as HTMLDivElement; const firstDayTileContent = firstDayTile.querySelector('.testContent'); const secondDayTileContent = secondDayTile.querySelector('.testContent'); diff --git a/src/YearView.jsx b/src/YearView.tsx similarity index 64% rename from src/YearView.jsx rename to src/YearView.tsx index 05cc9c66..a582b25d 100644 --- a/src/YearView.jsx +++ b/src/YearView.tsx @@ -2,7 +2,9 @@ import React from 'react'; import Months from './YearView/Months'; -export default function YearView(props) { +type YearViewProps = React.ComponentProps; + +export default function YearView(props: YearViewProps) { function renderMonths() { return ; } diff --git a/src/YearView/Month.spec.jsx b/src/YearView/Month.spec.tsx similarity index 96% rename from src/YearView/Month.spec.jsx rename to src/YearView/Month.spec.tsx index a726e65d..90c4d6a1 100644 --- a/src/YearView/Month.spec.jsx +++ b/src/YearView/Month.spec.tsx @@ -8,6 +8,12 @@ const tileProps = { activeStartDate: new Date(2018, 0, 1), classes: ['react-calendar__tile'], date: new Date(2018, 0, 1), + onClick: () => { + // Intentionally empty + }, + onMouseOver: () => { + // Intentionally empty + }, }; describe('Month', () => { @@ -29,7 +35,7 @@ describe('Month', () => { }); it('renders component with proper abbreviation', () => { - const { container } = render(); + const { container } = render(); const abbr = container.querySelector('abbr'); @@ -84,7 +90,7 @@ describe('Month', () => { const { container } = render(); - fireEvent.click(container.querySelector('.react-calendar__tile')); + fireEvent.click(container.querySelector('.react-calendar__tile') as HTMLDivElement); expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalledWith(date, expect.any(Object)); @@ -96,7 +102,7 @@ describe('Month', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.mouseOver(tile); expect(onMouseOver).toHaveBeenCalled(); @@ -109,7 +115,7 @@ describe('Month', () => { const { container } = render(); - const tile = container.querySelector('.react-calendar__tile'); + const tile = container.querySelector('.react-calendar__tile') as HTMLDivElement; fireEvent.focus(tile); expect(onMouseOver).toHaveBeenCalled(); diff --git a/src/YearView/Month.jsx b/src/YearView/Month.tsx similarity index 72% rename from src/YearView/Month.jsx rename to src/YearView/Month.tsx index 91f2b671..37d1d32e 100644 --- a/src/YearView/Month.jsx +++ b/src/YearView/Month.tsx @@ -12,18 +12,27 @@ import { tileProps } from '../shared/propTypes'; const className = 'react-calendar__year-view__months__month'; +type MonthProps = { + classes?: string[]; + formatMonth?: typeof defaultFormatMonth; + formatMonthYear?: typeof defaultFormatMonthYear; +} & Omit< + React.ComponentProps, + 'children' | 'formatAbbr' | 'maxDateTransform' | 'minDateTransform' | 'view' +>; + export default function Month({ - classes, + classes = [], formatMonth = defaultFormatMonth, formatMonthYear = defaultFormatMonthYear, ...otherProps -}) { +}: MonthProps) { const { date, locale } = otherProps; return ( ; + +export default function Months(props: MonthsProps) { const { activeStartDate } = props; const start = 0; const end = 11; diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/shared/const.js b/src/shared/const.ts similarity index 92% rename from src/shared/const.js rename to src/shared/const.ts index 7468421e..00b755b8 100644 --- a/src/shared/const.js +++ b/src/shared/const.ts @@ -3,7 +3,7 @@ export const CALENDAR_TYPES = { HEBREW: 'Hebrew', ISO_8601: 'ISO 8601', US: 'US', -}; +} as const; export const CALENDAR_TYPE_LOCALES = { [CALENDAR_TYPES.US]: [ @@ -52,4 +52,4 @@ export const CALENDAR_TYPE_LOCALES = { [CALENDAR_TYPES.HEBREW]: ['he', 'he-IL'], }; -export const WEEKDAYS = [...Array(7)].map((el, index) => index); +export const WEEKDAYS = [0, 1, 2, 3, 4, 5, 6] as const; diff --git a/src/shared/dateFormatter.js b/src/shared/dateFormatter.ts similarity index 53% rename from src/shared/dateFormatter.js rename to src/shared/dateFormatter.ts index 9c64e3c0..18a73fbd 100644 --- a/src/shared/dateFormatter.js +++ b/src/shared/dateFormatter.ts @@ -2,8 +2,10 @@ import getUserLocale from 'get-user-locale'; const formatterCache = new Map(); -function getFormatter(options) { - return (locale, date) => { +function getFormatter( + options: Intl.DateTimeFormatOptions, +): (locale: string | undefined, date: Date) => string { + return function formatter(locale: string | undefined, date: Date): string { const localeWithDefault = locale || getUserLocale(); if (!formatterCache.has(localeWithDefault)) { @@ -13,7 +15,10 @@ function getFormatter(options) { const formatterCacheLocale = formatterCache.get(localeWithDefault); if (!formatterCacheLocale.has(options)) { - formatterCacheLocale.set(options, new Intl.DateTimeFormat(localeWithDefault, options).format); + formatterCacheLocale.set( + options, + new Intl.DateTimeFormat(localeWithDefault || undefined, options).format, + ); } return formatterCacheLocale.get(options)(date); @@ -29,23 +34,36 @@ function getFormatter(options) { * * @param {Date} date Date. */ -function toSafeHour(date) { +function toSafeHour(date: Date): Date { const safeDate = new Date(date); return new Date(safeDate.setHours(12)); } -function getSafeFormatter(options) { +function getSafeFormatter( + options: Intl.DateTimeFormatOptions, +): (locale: string | undefined, date: Date) => string { return (locale, date) => getFormatter(options)(locale, toSafeHour(date)); } -const formatDateOptions = { day: 'numeric', month: 'numeric', year: 'numeric' }; -const formatDayOptions = { day: 'numeric' }; -const formatLongDateOptions = { day: 'numeric', month: 'long', year: 'numeric' }; -const formatMonthOptions = { month: 'long' }; -const formatMonthYearOptions = { month: 'long', year: 'numeric' }; -const formatShortWeekdayOptions = { weekday: 'short' }; -const formatWeekdayOptions = { weekday: 'long' }; -const formatYearOptions = { year: 'numeric' }; +const formatDateOptions = { + day: 'numeric', + month: 'numeric', + year: 'numeric', +} satisfies Intl.DateTimeFormatOptions; +const formatDayOptions = { day: 'numeric' } satisfies Intl.DateTimeFormatOptions; +const formatLongDateOptions = { + day: 'numeric', + month: 'long', + year: 'numeric', +} satisfies Intl.DateTimeFormatOptions; +const formatMonthOptions = { month: 'long' } satisfies Intl.DateTimeFormatOptions; +const formatMonthYearOptions = { + month: 'long', + year: 'numeric', +} satisfies Intl.DateTimeFormatOptions; +const formatShortWeekdayOptions = { weekday: 'short' } satisfies Intl.DateTimeFormatOptions; +const formatWeekdayOptions = { weekday: 'long' } satisfies Intl.DateTimeFormatOptions; +const formatYearOptions = { year: 'numeric' } satisfies Intl.DateTimeFormatOptions; export const formatDate = getSafeFormatter(formatDateOptions); export const formatDay = getSafeFormatter(formatDayOptions); diff --git a/src/shared/dates.js b/src/shared/dates.ts similarity index 82% rename from src/shared/dates.js rename to src/shared/dates.ts index 1add63ed..dcf7062a 100644 --- a/src/shared/dates.js +++ b/src/shared/dates.ts @@ -33,13 +33,15 @@ import { import { CALENDAR_TYPES, WEEKDAYS } from './const'; import { formatYear as defaultFormatYear } from './dateFormatter'; +import type { CalendarType, RangeType } from './types'; + const SUNDAY = WEEKDAYS[0]; const FRIDAY = WEEKDAYS[5]; const SATURDAY = WEEKDAYS[6]; /* Simple getters - getting a property of a given point in time */ -export function getDayOfWeek(date, calendarType = CALENDAR_TYPES.ISO_8601) { +export function getDayOfWeek(date: Date, calendarType: CalendarType = CALENDAR_TYPES.ISO_8601) { const weekday = date.getDay(); switch (calendarType) { @@ -60,7 +62,7 @@ export function getDayOfWeek(date, calendarType = CALENDAR_TYPES.ISO_8601) { * Century */ -export function getBeginOfCenturyYear(date) { +export function getBeginOfCenturyYear(date: Date) { const beginOfCentury = getCenturyStart(date); return getYear(beginOfCentury); } @@ -68,7 +70,7 @@ export function getBeginOfCenturyYear(date) { /** * Decade */ -export function getBeginOfDecadeYear(date) { +export function getBeginOfDecadeYear(date: Date) { const beginOfDecade = getDecadeStart(date); return getYear(beginOfDecade); } @@ -83,7 +85,7 @@ export function getBeginOfDecadeYear(date) { * @param {Date} date Date. * @param {string} [calendarType="ISO 8601"] Calendar type. */ -export function getBeginOfWeek(date, calendarType = CALENDAR_TYPES.ISO_8601) { +export function getBeginOfWeek(date: Date, calendarType: CalendarType = CALENDAR_TYPES.ISO_8601) { const year = getYear(date); const monthIndex = getMonthIndex(date); const day = date.getDate() - getDayOfWeek(date, calendarType); @@ -98,7 +100,7 @@ export function getBeginOfWeek(date, calendarType = CALENDAR_TYPES.ISO_8601) { * @param {Date} date Date. * @param {string} [calendarType="ISO 8601"] Calendar type. */ -export function getWeekNumber(date, calendarType = CALENDAR_TYPES.ISO_8601) { +export function getWeekNumber(date: Date, calendarType: CalendarType = CALENDAR_TYPES.ISO_8601) { const calendarTypeForWeekNumber = calendarType === CALENDAR_TYPES.US ? CALENDAR_TYPES.US : CALENDAR_TYPES.ISO_8601; const beginOfWeek = getBeginOfWeek(date, calendarType); @@ -126,7 +128,7 @@ export function getWeekNumber(date, calendarType = CALENDAR_TYPES.ISO_8601) { * @param {string} rangeType Range type (e.g. 'day') * @param {Date} date Date. */ -export function getBegin(rangeType, date) { +export function getBegin(rangeType: RangeType, date: Date) { switch (rangeType) { case 'century': return getCenturyStart(date); @@ -143,7 +145,7 @@ export function getBegin(rangeType, date) { } } -export function getBeginPrevious(rangeType, date) { +export function getBeginPrevious(rangeType: RangeType, date: Date) { switch (rangeType) { case 'century': return getPreviousCenturyStart(date); @@ -158,7 +160,7 @@ export function getBeginPrevious(rangeType, date) { } } -export function getBeginNext(rangeType, date) { +export function getBeginNext(rangeType: RangeType, date: Date) { switch (rangeType) { case 'century': return getNextCenturyStart(date); @@ -173,7 +175,7 @@ export function getBeginNext(rangeType, date) { } } -export function getBeginPrevious2(rangeType, date) { +export function getBeginPrevious2(rangeType: RangeType, date: Date) { switch (rangeType) { case 'decade': return getPreviousDecadeStart(date, -100); @@ -186,7 +188,7 @@ export function getBeginPrevious2(rangeType, date) { } } -export function getBeginNext2(rangeType, date) { +export function getBeginNext2(rangeType: RangeType, date: Date) { switch (rangeType) { case 'decade': return getNextDecadeStart(date, 100); @@ -205,7 +207,7 @@ export function getBeginNext2(rangeType, date) { * @param {string} rangeType Range type (e.g. 'day') * @param {Date} date Date. */ -export function getEnd(rangeType, date) { +export function getEnd(rangeType: RangeType, date: Date) { switch (rangeType) { case 'century': return getCenturyEnd(date); @@ -222,7 +224,7 @@ export function getEnd(rangeType, date) { } } -export function getEndPrevious(rangeType, date) { +export function getEndPrevious(rangeType: RangeType, date: Date) { switch (rangeType) { case 'century': return getPreviousCenturyEnd(date); @@ -237,7 +239,7 @@ export function getEndPrevious(rangeType, date) { } } -export function getEndPrevious2(rangeType, date) { +export function getEndPrevious2(rangeType: RangeType, date: Date) { switch (rangeType) { case 'decade': return getPreviousDecadeEnd(date, -100); @@ -256,7 +258,7 @@ export function getEndPrevious2(rangeType, date) { * @param {string} rangeType Range type (e.g. 'day') * @param {Date} date Date. */ -export function getRange(rangeType, date) { +export function getRange(rangeType: RangeType, date: Date): [Date, Date] { switch (rangeType) { case 'century': return getCenturyRange(date); @@ -280,12 +282,16 @@ export function getRange(rangeType, date) { * @param {Date} date1 First date. * @param {Date} date2 Second date. */ -export function getValueRange(rangeType, date1, date2) { - const rawNextValue = [date1, date2].sort((a, b) => a.getTime() - b.getTime()); +export function getValueRange(rangeType: RangeType, date1: Date, date2: Date) { + const rawNextValue = [date1, date2].sort((a, b) => a.getTime() - b.getTime()) as [Date, Date]; return [getBegin(rangeType, rawNextValue[0]), getEnd(rangeType, rawNextValue[1])]; } -function toYearLabel(locale, formatYear = defaultFormatYear, dates) { +function toYearLabel( + locale: string | undefined, + formatYear: (locale: string | undefined, date: Date) => string = defaultFormatYear, + dates: Date[], +) { return dates.map((date) => formatYear(locale, date)).join(' – '); } @@ -304,7 +310,11 @@ function toYearLabel(locale, formatYear = defaultFormatYear, dates) { * @param {FormatYear} formatYear Function to format a year. * @param {Date|string|number} date Date or a year as a string or as a number. */ -export function getCenturyLabel(locale, formatYear, date) { +export function getCenturyLabel( + locale: string | undefined, + formatYear: (locale: string | undefined, date: Date) => string, + date: Date, +) { return toYearLabel(locale, formatYear, getCenturyRange(date)); } @@ -316,7 +326,11 @@ export function getCenturyLabel(locale, formatYear, date) { * @param {FormatYear} formatYear Function to format a year. * @param {Date|string|number} date Date or a year as a string or as a number. */ -export function getDecadeLabel(locale, formatYear, date) { +export function getDecadeLabel( + locale: string | undefined, + formatYear: (locale: string | undefined, date: Date) => string, + date: Date, +) { return toYearLabel(locale, formatYear, getDecadeRange(date)); } @@ -325,7 +339,7 @@ export function getDecadeLabel(locale, formatYear, date) { * * @param {Date} date Date. */ -export function isCurrentDayOfWeek(date) { +export function isCurrentDayOfWeek(date: Date) { return date.getDay() === new Date().getDay(); } @@ -335,7 +349,7 @@ export function isCurrentDayOfWeek(date) { * @param {Date} date Date. * @param {string} [calendarType="ISO 8601"] Calendar type. */ -export function isWeekend(date, calendarType = CALENDAR_TYPES.ISO_8601) { +export function isWeekend(date: Date, calendarType: CalendarType = CALENDAR_TYPES.ISO_8601) { const weekday = date.getDay(); switch (calendarType) { case CALENDAR_TYPES.ARABIC: diff --git a/src/shared/propTypes.js b/src/shared/propTypes.ts similarity index 86% rename from src/shared/propTypes.js rename to src/shared/propTypes.ts index 7105f837..c3fc88f2 100644 --- a/src/shared/propTypes.js +++ b/src/shared/propTypes.ts @@ -12,7 +12,7 @@ export const isClassName = PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.string), ]); -export function isMinDate(props, propName, componentName) { +export function isMinDate(props: Record, propName: string, componentName: string) { const { [propName]: minDate } = props; if (!minDate) { @@ -36,7 +36,7 @@ export function isMinDate(props, propName, componentName) { return null; } -export function isMaxDate(props, propName, componentName) { +export function isMaxDate(props: Record, propName: string, componentName: string) { const { [propName]: maxDate } = props; if (!maxDate) { @@ -74,13 +74,17 @@ export const isValue = PropTypes.oneOfType([ export const isViews = PropTypes.arrayOf(PropTypes.oneOf(allViews)); -export function isView(props, propName, componentName) { +export function isView( + props: Record & { views?: string[] }, + propName: string, + componentName: string, +) { const { [propName]: view } = props; const { views } = props; const allowedViews = views || allViews; - if (view !== undefined && allowedViews.indexOf(view) === -1) { + if (view !== undefined && (typeof view !== 'string' || allowedViews.indexOf(view) === -1)) { return new Error( `Invalid prop \`${propName}\` of value \`${view}\` supplied to \`${componentName}\`, expected one of [${allowedViews .map((a) => `"${a}"`) @@ -92,7 +96,7 @@ export function isView(props, propName, componentName) { return null; } -isView.isRequired = (props, propName, componentName) => { +isView.isRequired = (props: Record, propName: string, componentName: string) => { const { [propName]: view } = props; if (!view) { diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 00000000..afa7f88b --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,60 @@ +import type { CALENDAR_TYPES } from './const'; + +export type Action = 'prev' | 'prev2' | 'next' | 'next2' | 'onChange' | 'drillUp' | 'drillDown'; + +export type CalendarType = typeof CALENDAR_TYPES[keyof typeof CALENDAR_TYPES]; + +export type ClassName = string | string[]; + +export type Detail = 'century' | 'decade' | 'year' | 'month'; + +export type LooseValue = string | Date | null | (Date | null)[]; + +export type Range = [T, T]; + +export type RangeType = 'century' | 'decade' | 'year' | 'month' | 'day'; + +export type Value = Date | null | (Date | null)[]; + +export type View = 'century' | 'decade' | 'year' | 'month'; + +export type NavigationLabelArgs = { + date: Date; + label: string; + locale: string | undefined; + view: View; +}; + +export type NavigationLabelFunc = ({ + date, + label, + locale, + view, +}: NavigationLabelArgs) => React.ReactNode; + +export type OnArgs = { + action: Action; + activeStartDate: Date | null; + value: Date | null | (Date | null)[]; + view: View; +}; + +export type OnChangeFunc = (value: Date, event: React.MouseEvent) => void; + +export type OnClickWeekNumberFunc = ( + weekNumber: number, + date: Date, + event: React.MouseEvent, +) => void; + +export type TileArgs = { + activeStartDate: Date; + date: Date; + view: View; +}; + +export type TileClassNameFunc = (args: TileArgs) => string; + +export type TileContentFunc = (args: TileArgs) => React.ReactNode; + +export type TileDisabledFunc = (args: TileArgs) => boolean; diff --git a/src/shared/utils.spec.js b/src/shared/utils.spec.ts similarity index 83% rename from src/shared/utils.spec.js rename to src/shared/utils.spec.ts index 3fd6f049..3a1adc36 100644 --- a/src/shared/utils.spec.js +++ b/src/shared/utils.spec.ts @@ -7,6 +7,8 @@ import { getTileClasses, } from './utils'; +import type { Range } from './types'; + describe('between', () => { it('returns value when value is within set boundaries', () => { const value = new Date(2017, 6, 1); @@ -46,7 +48,7 @@ describe('between', () => { describe('isValueWithinRange', () => { it('returns true for a value between range bonduaries', () => { const value = new Date(2017, 6, 1); - const range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const valueWithin = isValueWithinRange(value, range); @@ -55,7 +57,7 @@ describe('isValueWithinRange', () => { it('returns true for a value on the first range bonduary', () => { const value = new Date(2017, 0, 1); - const range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const valueWithin = isValueWithinRange(value, range); @@ -64,7 +66,7 @@ describe('isValueWithinRange', () => { it('returns true for a value on the last range bonduary', () => { const value = new Date(2018, 0, 1); - const range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const valueWithin = isValueWithinRange(value, range); @@ -73,7 +75,7 @@ describe('isValueWithinRange', () => { it('returns true for a value smaller than both range bonduaries', () => { const value = new Date(2016, 0, 1); - const range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const valueWithin = isValueWithinRange(value, range); @@ -82,7 +84,7 @@ describe('isValueWithinRange', () => { it('returns true for a value larger than both range bonduaries', () => { const value = new Date(2019, 0, 1); - const range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const valueWithin = isValueWithinRange(value, range); @@ -92,8 +94,8 @@ describe('isValueWithinRange', () => { describe('isRangeWithinRange', () => { it('returns true for range fitting within another range', () => { - const greaterRange = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; - const smallerRange = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const greaterRange: Range = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; + const smallerRange: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; const rangeWithin = isRangeWithinRange(greaterRange, smallerRange); @@ -101,8 +103,8 @@ describe('isRangeWithinRange', () => { }); it('returns true for a range identical with another range', () => { - const greaterRange = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; - const smallerRange = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const greaterRange: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const smallerRange: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; const rangeWithin = isRangeWithinRange(greaterRange, smallerRange); @@ -110,8 +112,8 @@ describe('isRangeWithinRange', () => { }); it('returns false for a range that starts outside of another range', () => { - const greaterRange = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; - const smallerRange = [new Date(2010, 0, 1), new Date(2017, 0, 1)]; + const greaterRange: Range = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; + const smallerRange: Range = [new Date(2010, 0, 1), new Date(2017, 0, 1)]; const rangeWithin = isRangeWithinRange(greaterRange, smallerRange); @@ -119,8 +121,8 @@ describe('isRangeWithinRange', () => { }); it('returns false for a range that ends outside of another range', () => { - const greaterRange = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; - const smallerRange = [new Date(2016, 0, 1), new Date(2021, 0, 1)]; + const greaterRange: Range = [new Date(2011, 0, 1), new Date(2020, 0, 1)]; + const smallerRange: Range = [new Date(2016, 0, 1), new Date(2021, 0, 1)]; const rangeWithin = isRangeWithinRange(greaterRange, smallerRange); @@ -130,8 +132,8 @@ describe('isRangeWithinRange', () => { describe('doRangesOverlap', () => { it('returns true for overlapping ranges', () => { - const range1 = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; - const range2 = [new Date(2016, 6, 1), new Date(2017, 6, 1)]; + const range1: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const range2: Range = [new Date(2016, 6, 1), new Date(2017, 6, 1)]; const rangesOverlap = doRangesOverlap(range1, range2); const rangesOverlapReversed = doRangesOverlap(range2, range1); @@ -141,8 +143,8 @@ describe('doRangesOverlap', () => { }); it('returns true for touching ranges', () => { - const range1 = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; - const range2 = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; + const range1: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const range2: Range = [new Date(2017, 0, 1), new Date(2018, 0, 1)]; const rangesOverlap = doRangesOverlap(range1, range2); const rangesOverlapReversed = doRangesOverlap(range2, range1); @@ -152,8 +154,8 @@ describe('doRangesOverlap', () => { }); it('returns false for ranges that do not overlap', () => { - const range1 = [new Date(2006, 0, 1), new Date(2007, 0, 1)]; - const range2 = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; + const range1: Range = [new Date(2006, 0, 1), new Date(2007, 0, 1)]; + const range2: Range = [new Date(2016, 0, 1), new Date(2017, 0, 1)]; const rangesOverlap = doRangesOverlap(range1, range2); const rangesOverlapReversed = doRangesOverlap(range2, range1); @@ -165,15 +167,18 @@ describe('doRangesOverlap', () => { describe('getTileClasses', () => { it('throws an error when given no value', () => { + // @ts-expect-error-next-line expect(() => getTileClasses()).toThrow(); }); it('throws an error when given date but not given dateType parameter ', () => { + // @ts-expect-error-next-line expect(() => getTileClasses({ date: new Date(2017, 0, 1) })).toThrow(); }); it('throws an error when given date and value but not given valueType parameter ', () => { expect(() => + // @ts-expect-error-next-line getTileClasses({ date: new Date(2017, 0, 1), dateType: 'month', @@ -233,9 +238,10 @@ describe('getTileClasses', () => { describe('range classes', () => { it('returns range flag set to true when passed a date within value array', () => { + const value: Range = [new Date(2017, 0, 1), new Date(2017, 6, 1)]; + const result = getTileClasses({ - value: [new Date(2017, 0, 1), new Date(2017, 6, 1)], - valueType: 'month', + value, date: new Date(2017, 3, 1), dateType: 'month', }); @@ -246,9 +252,10 @@ describe('getTileClasses', () => { }); it('returns range & rangeStart flags set to true when passed a date equal to value start', () => { + const value: Range = [new Date(2017, 0, 1), new Date(2017, 6, 1)]; + const result = getTileClasses({ - value: [new Date(2017, 0, 1), new Date(2017, 6, 1)], - valueType: 'month', + value, date: new Date(2017, 0, 1), dateType: 'month', }); @@ -259,9 +266,10 @@ describe('getTileClasses', () => { }); it('returns range & rangeEnd flags set to true when passed a date equal to value end', () => { + const value: Range = [new Date(2017, 0, 1), new Date(2017, 6, 1)]; + const result = getTileClasses({ - value: [new Date(2017, 0, 1), new Date(2017, 6, 1)], - valueType: 'month', + value, date: new Date(2017, 6, 1), dateType: 'month', }); diff --git a/src/shared/utils.js b/src/shared/utils.ts similarity index 50% rename from src/shared/utils.js rename to src/shared/utils.ts index 951d1a1b..bdfb9ce4 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.ts @@ -1,35 +1,43 @@ import { getRange } from './dates'; +import type { Range, RangeType } from './types'; + /** * Returns a value no smaller than min and no larger than max. * - * @param {*} value Value to return. - * @param {*} min Minimum return value. - * @param {*} max Maximum return value. + * @param {Date} value Value to return. + * @param {Date} min Minimum return value. + * @param {Date} max Maximum return value. */ -export function between(value, min, max) { +export function between(value: T, min?: T | null, max?: T | null): T { if (min && min > value) { return min; } + if (max && max < value) { return max; } + return value; } -export function isValueWithinRange(value, range) { +export function isValueWithinRange(value: Date, range: Range): boolean { return range[0] <= value && range[1] >= value; } -export function isRangeWithinRange(greaterRange, smallerRange) { +export function isRangeWithinRange(greaterRange: Range, smallerRange: Range): boolean { return greaterRange[0] <= smallerRange[0] && greaterRange[1] >= smallerRange[1]; } -export function doRangesOverlap(range1, range2) { +export function doRangesOverlap(range1: Range, range2: Range): boolean { return isValueWithinRange(range1[0], range2) || isValueWithinRange(range1[1], range2); } -function getRangeClassNames(valueRange, dateRange, baseClassName) { +function getRangeClassNames( + valueRange: Range, + dateRange: Range, + baseClassName: string, +): string[] { const isRange = doRangesOverlap(dateRange, valueRange); const classes = []; @@ -56,12 +64,25 @@ function getRangeClassNames(valueRange, dateRange, baseClassName) { return classes; } -export function getTileClasses(args) { +type ValueRangeOrValueWithValueType> = U extends Range + ? { value: U; valueType?: undefined } + : { value: U; valueType: RangeType }; + +type DateRangeOrDateWithDateType> = U extends Range + ? { date: U; dateType?: undefined } + : { date: U; dateType: RangeType }; + +export function getTileClasses( + args: { + hover?: Date; + } & ValueRangeOrValueWithValueType & + DateRangeOrDateWithDateType, +): string[] { if (!args) { throw new Error('args is required'); } - const { value, valueType, date, dateType, hover } = args; + const { value, date, hover } = args; const className = 'react-calendar__tile'; const classes = [className]; @@ -70,12 +91,20 @@ export function getTileClasses(args) { return classes; } - if (!Array.isArray(date) && !dateType) { - throw new Error('dateType is required when date is not an array of two dates'); - } - const now = new Date(); - const dateRange = Array.isArray(date) ? date : getRange(dateType, date); + const dateRange = (() => { + if (Array.isArray(date)) { + return date; + } + + const { dateType } = args; + + if (!dateType) { + throw new Error('dateType is required when date is not an array of two dates'); + } + + return getRange(dateType, date); + })(); if (isValueWithinRange(now, dateRange)) { classes.push(`${className}--now`); @@ -85,11 +114,19 @@ export function getTileClasses(args) { return classes; } - if (!Array.isArray(value) && !valueType) { - throw new Error('valueType is required when value is not an array of two dates'); - } + const valueRange = (() => { + if (Array.isArray(value)) { + return value; + } + + const { valueType } = args; + + if (!valueType) { + throw new Error('valueType is required when value is not an array of two dates'); + } - const valueRange = Array.isArray(value) ? value : getRange(valueType, value); + return getRange(valueType, value); + })(); if (isRangeWithinRange(valueRange, dateRange)) { classes.push(`${className}--active`); @@ -101,10 +138,11 @@ export function getTileClasses(args) { classes.push(...valueRangeClassNames); - const valueArray = [].concat(value); + const valueArray = Array.isArray(value) ? value : [value]; if (hover && valueArray.length === 1) { - const hoverRange = hover > valueRange[0] ? [valueRange[0], hover] : [hover, valueRange[0]]; + const hoverRange: Range = + hover > valueRange[0] ? [valueRange[0], hover] : [hover, valueRange[0]]; const hoverRangeClassNames = getRangeClassNames(hoverRange, dateRange, `${className}--hover`); classes.push(...hoverRangeClassNames); diff --git a/tsconfig.build.json b/tsconfig.build.json index 0ecc338a..699011c4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/**/*.spec.js", "src/**/*.spec.jsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"] + "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"] } diff --git a/tsconfig.json b/tsconfig.json index 505941dd..7e30adc0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { - "allowJs": true, - "declaration": false, + "declaration": true, "esModuleInterop": true, "isolatedModules": true, "jsx": "react",