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 10, 2022
1 parent 246d7ea commit b9e7e09
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 72 deletions.
106 changes: 36 additions & 70 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 @@ -53,9 +57,16 @@ export const FocusContext = createContext<FocusContextValue | undefined>(
);

/** The provider for the [[FocusContext]]. */
export function FocusProvider(props: { children: ReactNode }): JSX.Element {
export function FocusProvider({
children,
disallowFocusOnDisabledDates
}: {
children: ReactNode;
disallowFocusOnDisabledDates?: boolean;
}): JSX.Element {
const navigation = useNavigation();
const modifiers = useModifiers();
const { fromDate, toDate } = useDayPicker();

const [focusedDay, setFocusedDay] = useState<Date | undefined>();
const [lastFocused, setLastFocused] = useState<Date | undefined>();
Expand All @@ -79,75 +90,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 All @@ -167,8 +135,6 @@ export function FocusProvider(props: { children: ReactNode }): JSX.Element {
};

return (
<FocusContext.Provider value={value}>
{props.children}
</FocusContext.Provider>
<FocusContext.Provider value={value}>{children}</FocusContext.Provider>
);
}
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
8 changes: 6 additions & 2 deletions packages/react-day-picker/src/contexts/RootProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type RootContext = DayPickerBase & {

/** Provide the value for all the context providers. */
export function RootProvider(props: RootContext): JSX.Element {
const { children, ...initialProps } = props;
const { children, disallowFocusOnDisabledDates, ...initialProps } = props;

return (
<DayPickerProvider initialProps={initialProps}>
Expand All @@ -26,7 +26,11 @@ export function RootProvider(props: RootContext): JSX.Element {
<SelectMultipleProvider initialProps={initialProps}>
<SelectRangeProvider initialProps={initialProps}>
<ModifiersProvider>
<FocusProvider>{children}</FocusProvider>
<FocusProvider
disallowFocusOnDisabledDates={disallowFocusOnDisabledDates}
>
{children}
</FocusProvider>
</ModifiersProvider>
</SelectRangeProvider>
</SelectMultipleProvider>
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 b9e7e09

Please sign in to comment.