diff --git a/packages/react-day-picker/src/contexts/Focus/FocusContext.tsx b/packages/react-day-picker/src/contexts/Focus/FocusContext.tsx index 98b48554c7..c46c55c262 100644 --- a/packages/react-day-picker/src/contexts/Focus/FocusContext.tsx +++ b/packages/react-day-picker/src/contexts/Focus/FocusContext.tsx @@ -6,6 +6,9 @@ import addWeeks from 'date-fns/addWeeks'; import addYears from 'date-fns/addYears'; import endOfWeek from 'date-fns/endOfWeek'; import startOfWeek from 'date-fns/startOfWeek'; +import max from 'date-fns/max'; +import min from 'date-fns/min'; +import isSameDay from 'date-fns/isSameDay'; import { useDayPicker } from 'contexts/DayPicker'; @@ -67,7 +70,7 @@ export function FocusProvider(props: { children: ReactNode }): JSX.Element { modifiers ); - const { weekStartsOn } = useDayPicker(); + const { fromDate, toDate, weekStartsOn } = useDayPicker(); // TODO: cleanup and test obscure code below const focusTarget = @@ -83,75 +86,32 @@ export function FocusProvider(props: { children: ReactNode }): JSX.Element { setFocusedDay(date); }; - const focusDayBefore = () => { + const moveFocus = (addFn: typeof addDays, after: boolean) => { if (!focusedDay) return; - const before = addDays(focusedDay, -1); - focus(before); - navigation.goToDate(before, focusedDay); - }; - const focusDayAfter = () => { - if (!focusedDay) return; - const after = addDays(focusedDay, 1); - focus(after); - navigation.goToDate(after, focusedDay); - }; - const focusWeekBefore = () => { - if (!focusedDay) return; - const up = addWeeks(focusedDay, -1); - focus(up); - navigation.goToDate(up, focusedDay); - }; - const focusWeekAfter = () => { - if (!focusedDay) return; - const down = addWeeks(focusedDay, 1); - focus(down); - navigation.goToDate(down, focusedDay); - }; - - const focusStartOfWeek = (): void => { - if (!focusedDay) return; - const dayToFocus = startOfWeek(focusedDay, { weekStartsOn }); - navigation.goToDate(dayToFocus, focusedDay); - focus(dayToFocus); - }; - - const focusEndOfWeek = (): void => { - if (!focusedDay) return; - const dayToFocus = endOfWeek(focusedDay, { weekStartsOn }); - navigation.goToDate(dayToFocus, focusedDay); - focus(dayToFocus); - }; - - const focusMonthBefore = (): void => { - if (!focusedDay) return; - - const monthBefore = addMonths(focusedDay, -1); - navigation.goToDate(monthBefore, focusedDay); - focus(monthBefore); + let newFocusedDay = addFn(focusedDay, after ? 1 : -1); + if (!after && fromDate) { + newFocusedDay = max([fromDate, newFocusedDay]); + } + if (after && toDate) { + newFocusedDay = min([toDate, newFocusedDay]); + } + if (isSameDay(focusedDay, newFocusedDay)) return; + navigation.goToDate(newFocusedDay, focusedDay); + focus(newFocusedDay); }; - const focusMonthAfter = () => { - if (!focusedDay) return; - const monthAfter = addMonths(focusedDay, 1); - navigation.goToDate(monthAfter, focusedDay); - focus(monthAfter); - }; - - const focusYearBefore = () => { - if (!focusedDay) return; - - const yearBefore = addYears(focusedDay, -1); - navigation.goToDate(yearBefore, focusedDay); - focus(yearBefore); - }; - - const focusYearAfter = () => { - if (!focusedDay) return; - - const yearAfter = addYears(focusedDay, 1); - navigation.goToDate(yearAfter, focusedDay); - focus(yearAfter); - }; + const focusDayBefore = () => moveFocus(addDays, false); + const focusDayAfter = () => moveFocus(addDays, true); + const focusWeekBefore = () => moveFocus(addWeeks, false); + const focusWeekAfter = () => moveFocus(addWeeks, true); + const focusStartOfWeek = () => + moveFocus((date) => startOfWeek(date, { weekStartsOn }), false); + const focusEndOfWeek = () => + moveFocus((date) => endOfWeek(date, { weekStartsOn }), true); + const focusMonthBefore = () => moveFocus(addMonths, false); + const focusMonthAfter = () => moveFocus(addMonths, true); + const focusYearBefore = () => moveFocus(addYears, false); + const focusYearAfter = () => moveFocus(addYears, true); const value: FocusContextValue = { focusedDay, diff --git a/packages/react-day-picker/src/contexts/Focus/useFocusContext.test.ts b/packages/react-day-picker/src/contexts/Focus/useFocusContext.test.ts index 06309002bf..1199ce97a6 100644 --- a/packages/react-day-picker/src/contexts/Focus/useFocusContext.test.ts +++ b/packages/react-day-picker/src/contexts/Focus/useFocusContext.test.ts @@ -12,6 +12,7 @@ import { customRenderHook } from 'test/render'; import { freezeBeforeAll } from 'test/utils'; import { FocusContextValue, useFocusContext } from 'contexts/Focus'; +import isSameDay from 'date-fns/isSameDay'; let renderResult: RenderResult; @@ -80,6 +81,16 @@ describe('when a day is focused', () => { test('should focus the day before', () => { expect(renderResult.current.focusedDay).toEqual(dayBefore); }); + test('should not focus the day before if the day is disabled', async () => { + const { result } = customRenderHook(() => useFocusContext(), { + fromDate: day + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusDayBefore(); + }); + expect(result.current.focusedDay).toEqual(day); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusDayAfter" is called', () => { @@ -90,6 +101,16 @@ describe('when a day is focused', () => { const dayAfter = addDays(day, 1); expect(renderResult.current.focusedDay).toEqual(dayAfter); }); + test('should not focus the day after if the day is disabled', async () => { + const { result } = customRenderHook(() => useFocusContext(), { + toDate: day + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusDayAfter(); + }); + expect(result.current.focusedDay).toEqual(day); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusWeekBefore" is called', () => { @@ -100,6 +121,17 @@ describe('when a day is focused', () => { const prevWeek = addWeeks(day, -1); expect(renderResult.current.focusedDay).toEqual(prevWeek); }); + test('should not focus the day in the previous week if the day is disabled', async () => { + const fromDate = addDays(day, -3); + const { result } = customRenderHook(() => useFocusContext(), { + fromDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusWeekBefore(); + }); + expect(result.current.focusedDay).toEqual(fromDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusWeekAfter" is called', () => { @@ -110,6 +142,17 @@ describe('when a day is focused', () => { const nextWeek = addWeeks(day, 1); expect(renderResult.current.focusedDay).toEqual(nextWeek); }); + test('should not focus the day in the next week if the day is disabled', async () => { + const toDate = addDays(day, 4); + const { result } = customRenderHook(() => useFocusContext(), { + toDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusWeekAfter(); + }); + expect(result.current.focusedDay).toEqual(toDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusStartOfWeek" is called', () => { @@ -120,6 +163,17 @@ describe('when a day is focused', () => { const firstDayOfWeek = startOfWeek(day); expect(renderResult.current.focusedDay).toEqual(firstDayOfWeek); }); + test('should not focus the first day of the week if the day is disabled', async () => { + const fromDate = addDays(startOfWeek(day), 1); + const { result } = customRenderHook(() => useFocusContext(), { + fromDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusStartOfWeek(); + }); + expect(result.current.focusedDay).toEqual(fromDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusEndOfWeek" is called', () => { @@ -130,6 +184,20 @@ describe('when a day is focused', () => { const lastDayOfWeek = endOfWeek(day); expect(renderResult.current.focusedDay).toEqual(lastDayOfWeek); }); + test('should not focus the last day of the week if the day is disabled', async () => { + const toDate = addDays(endOfWeek(day), -1); + const { result } = customRenderHook(() => useFocusContext(), { + toDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusEndOfWeek(); + }); + expect( + result.current.focusedDay && + isSameDay(result.current.focusedDay, toDate) + ).toBe(true); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusMonthBefore" is called', () => { @@ -140,6 +208,17 @@ describe('when a day is focused', () => { const monthBefore = addMonths(day, -1); expect(renderResult.current.focusedDay).toEqual(monthBefore); }); + test('should not focus the day in the month before if the day is disabled', async () => { + const fromDate = addDays(day, -10); + const { result } = customRenderHook(() => useFocusContext(), { + fromDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusMonthBefore(); + }); + expect(result.current.focusedDay).toEqual(fromDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusMonthAfter" is called', () => { @@ -150,6 +229,17 @@ describe('when a day is focused', () => { const monthAfter = addMonths(day, 1); expect(renderResult.current.focusedDay).toEqual(monthAfter); }); + test('should not focus the day in the month after if the day is disabled', async () => { + const toDate = addDays(day, 10); + const { result } = customRenderHook(() => useFocusContext(), { + toDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusMonthAfter(); + }); + expect(result.current.focusedDay).toEqual(toDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusYearBefore" is called', () => { @@ -160,6 +250,17 @@ describe('when a day is focused', () => { const prevYear = addYears(day, -1); expect(renderResult.current.focusedDay).toEqual(prevYear); }); + test('should not focus the day in the year before if the day is disabled', async () => { + const fromDate = addMonths(day, -10); + const { result } = customRenderHook(() => useFocusContext(), { + fromDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusYearBefore(); + }); + expect(result.current.focusedDay).toEqual(fromDate); + }); test.todo('should call the navigation goToDate'); }); describe('when "focusYearAfter" is called', () => { @@ -170,6 +271,17 @@ describe('when a day is focused', () => { const nextYear = addYears(day, 1); expect(renderResult.current.focusedDay).toEqual(nextYear); }); + test('should not focus the day in the year after if the day is disabled', async () => { + const toDate = addMonths(day, 10); + const { result } = customRenderHook(() => useFocusContext(), { + toDate + }); + await act(async () => { + await result.current.focus(day); + await result.current.focusYearAfter(); + }); + expect(result.current.focusedDay).toEqual(toDate); + }); test.todo('should call the navigation goToDate'); }); describe('when blur is called', () => {