Skip to content

Commit

Permalink
feat: add disallowFocusOnDisabledDates prop
Browse files Browse the repository at this point in the history
  • Loading branch information
kimamula authored and Kenji Imamula committed Jun 13, 2022
1 parent 246d7ea commit d62e0eb
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface DayPickerContextValue extends DayPickerBase {
numberOfMonths: number;
styles: Styles;
today: Date;
disallowFocusOnDisabledDates?: boolean;
}

/**
Expand Down Expand Up @@ -145,7 +146,8 @@ export function DayPickerProvider(props: DayPickerProviderProps): JSX.Element {
},
toDate,
today: initialProps.today ?? defaults.today,
weekStartsOn: initialProps.weekStartsOn
weekStartsOn: initialProps.weekStartsOn,
disallowFocusOnDisabledDates: initialProps.disallowFocusOnDisabledDates
};

return (
Expand Down
94 changes: 28 additions & 66 deletions packages/react-day-picker/src/contexts/Focus/FocusContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ 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 { useModifiers } from '../Modifiers';
import { useNavigation } from '../Navigation';
import { getInitialFocusTarget } from './utils/getInitialFocusTarget';
import { useDayPicker } from '../DayPicker';

/** Represents the value of the [[NavigationContext]]. */
export type FocusContextValue = {
Expand Down Expand Up @@ -56,6 +60,7 @@ export const FocusContext = createContext<FocusContextValue | undefined>(
export function FocusProvider(props: { children: ReactNode }): JSX.Element {
const navigation = useNavigation();
const modifiers = useModifiers();
const { fromDate, toDate, disallowFocusOnDisabledDates } = useDayPicker();

const [focusedDay, setFocusedDay] = useState<Date | undefined>();
const [lastFocused, setLastFocused] = useState<Date | undefined>();
Expand All @@ -79,75 +84,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);
navigation.goToDate(dayToFocus, focusedDay);
focus(dayToFocus);
};

const focusEndOfWeek = (): void => {
if (!focusedDay) return;
const dayToFocus = endOfWeek(focusedDay);
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 (disallowFocusOnDisabledDates) {
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), false);
const focusEndOfWeek = () => moveFocus((date) => endOfWeek(date), 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,
Expand Down
122 changes: 122 additions & 0 deletions packages/react-day-picker/src/contexts/Focus/useFocusContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FocusContextValue>;

Expand Down Expand Up @@ -80,6 +81,17 @@ 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 unfocusable', async () => {
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -90,6 +102,17 @@ 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 unfocusable', async () => {
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -100,6 +123,18 @@ 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 unfocusable', async () => {
const fromDate = addDays(day, -3);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -110,6 +145,18 @@ 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 unfocusable', async () => {
const toDate = addDays(day, 4);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -120,6 +167,18 @@ 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 unfocusable', async () => {
const fromDate = addDays(startOfWeek(day), 1);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -130,6 +189,21 @@ 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 unfocusable', async () => {
const toDate = addDays(endOfWeek(day), -1);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -140,6 +214,18 @@ 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 unfocusable', async () => {
const fromDate = addDays(day, -10);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -150,6 +236,18 @@ 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 unfocusable', async () => {
const toDate = addDays(day, 10);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -160,6 +258,18 @@ 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 unfocusable', async () => {
const fromDate = addMonths(day, -10);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand All @@ -170,6 +280,18 @@ 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 unfocusable', async () => {
const toDate = addMonths(day, 10);
const { result } = customRenderHook(() => useFocusContext(), {
disallowFocusOnDisabledDates: true,
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', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/react-day-picker/src/types/DayPickerBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ export interface DayPickerBase {
*/
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;

/**
* Disallow focus on disabled dates.
*/
disallowFocusOnDisabledDates?: boolean;

onDayClick?: DayClickEventHandler;
onDayFocus?: DayFocusEventHandler;
onDayBlur?: DayFocusEventHandler;
Expand Down

0 comments on commit d62e0eb

Please sign in to comment.