From 94c92b11be41b4aa20bc8d0345ea8f88c652bd4b Mon Sep 17 00:00:00 2001 From: Giampaolo Bellavite Date: Fri, 31 May 2024 07:07:31 -0500 Subject: [PATCH] fix: today day is auto focused when available (#2174) --- src/components/DayWrapper.tsx | 5 ++-- src/contexts/calendar.tsx | 11 ++++++++ src/contexts/focus.test.tsx | 40 ++++++++++++++++++++++++++++ src/contexts/focus.tsx | 50 ++++++++++++++++++++++++----------- 4 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 src/contexts/focus.test.tsx diff --git a/src/components/DayWrapper.tsx b/src/components/DayWrapper.tsx index a8f4636aa1..2525875760 100644 --- a/src/components/DayWrapper.tsx +++ b/src/components/DayWrapper.tsx @@ -11,6 +11,7 @@ import { import { UI, DayModifier } from "../UI"; import { CalendarDay } from "../classes/CalendarDay"; +import { useCalendar } from "../contexts/calendar"; import { useFocus } from "../contexts/focus"; import { useModifiers } from "../contexts/modifiers"; import { useProps } from "../contexts/props"; @@ -60,8 +61,10 @@ export function DayWrapper(props: { styles = {} } = useProps(); + const { isInteractive } = useCalendar(); const { setSelected } = useSelection(); const { getModifiers } = useModifiers(); + const { autoFocusTarget, focusedDay, @@ -196,8 +199,6 @@ export function DayWrapper(props: { const isAutoFocusTarget = Boolean(autoFocusTarget?.isEqualTo(props.day)); const isFocused = Boolean(focusedDay?.isEqualTo(props.day)); - const isInteractive = mode !== "default" || Boolean(onDayClick); - const style = getStyleForModifiers(modifiers, modifiersStyles, styles); const classNameForModifiers = getClassNamesForModifiers( diff --git a/src/contexts/calendar.tsx b/src/contexts/calendar.tsx index 18b8eac675..1432aa3b8c 100644 --- a/src/contexts/calendar.tsx +++ b/src/contexts/calendar.tsx @@ -83,6 +83,12 @@ export interface CalendarContext { goToDay: (day: CalendarDay) => void; /** Whether the given date is included in the displayed months. */ isDayDisplayed: (day: CalendarDay) => boolean; + + /** + * Whether the calendar is interactive, i.e. DayPicker has a selection `mode` + * set or the `onDayClick` prop is set. + */ + isInteractive: boolean; } const calendarContext = createContext(undefined); @@ -161,6 +167,9 @@ export function CalendarProvider(providerProps: { children?: ReactNode }) { return previousMonth ? goToMonth(previousMonth) : undefined; } + const isInteractive = + props.mode !== "default" || props.onDayClick !== undefined; + const calendar: CalendarContext = { dates, months, @@ -176,6 +185,8 @@ export function CalendarProvider(providerProps: { children?: ReactNode }) { goToNextMonth, goToPreviousMonth, goToDay, + + isInteractive, isDayDisplayed, dropdownOptions: { diff --git a/src/contexts/focus.test.tsx b/src/contexts/focus.test.tsx new file mode 100644 index 0000000000..dcf9084868 --- /dev/null +++ b/src/contexts/focus.test.tsx @@ -0,0 +1,40 @@ +import { gridcell } from "@/test/elements"; +import { renderHook } from "@/test/renderHook"; +import { user } from "@/test/user"; + +import { useFocus } from "./focus"; + +const month = new Date(2020, 0, 1); +const today = new Date(2020, 0, 14); + +describe("autoFocusTarget", () => { + describe("when not in interactive", () => { + test("the auto focus target is undefined", () => { + const { result } = renderHook(useFocus, { month, today }); + expect(result.current.autoFocusTarget).toBeUndefined(); + }); + }); + describe("when in selection mode", () => { + test("the autofocus target should be today", () => { + const { result } = renderHook(useFocus, { + month, + today, + mode: "single" + }); + expect(result.current.autoFocusTarget?.date).toEqual( + new Date(2020, 0, 14) + ); + }); + describe("if today is disabled", () => { + test("the autofocus target should be the first focusable day (the 1st of month)", () => { + const { result } = renderHook(useFocus, { + month, + today, + mode: "multiple", + disabled: [today] + }); + expect(result.current.autoFocusTarget?.date).toEqual(month); + }); + }); + }); +}); diff --git a/src/contexts/focus.tsx b/src/contexts/focus.tsx index b801e9c5d9..f81a8d478d 100644 --- a/src/contexts/focus.tsx +++ b/src/contexts/focus.tsx @@ -6,6 +6,7 @@ import React, { useState } from "react"; +import { DayModifier } from "../UI"; import type { CalendarDay } from "../classes"; import { getNextFocus } from "../helpers/getNextFocus"; @@ -69,23 +70,44 @@ export interface FocusContext { const focusContext = createContext(undefined); /** @private */ -export function FocusProvider(props: { children: ReactNode }): JSX.Element { - const { goToDay, isDayDisplayed } = useCalendar(); +export function FocusProvider({ + children +}: { + children: ReactNode; +}): JSX.Element { + const { goToDay, isDayDisplayed, days, isInteractive } = useCalendar(); - const { autoFocus = false, ...dayPickerProps } = useProps(); - const { calendarModifiers } = useModifiers(); + const { autoFocus = false, ...props } = useProps(); + const { calendarModifiers, getModifiers } = useModifiers(); const [focused, setFocused] = useState(); const [lastFocused, setLastFocused] = useState(); const [initiallyFocused, setInitiallyFocused] = useState(false); - const autoFocusTarget = - focused ?? (lastFocused && isDayDisplayed(lastFocused)) - ? lastFocused - : calendarModifiers.selected[0] ?? // autofocus the first selected day - // TOFIX: possible bug here selecting a today date when disabled - // calendarModifiers.today[0] ?? // autofocus today - calendarModifiers.focusable[0]; // otherwise autofocus the first focusable day; + const today = calendarModifiers.today[0]; + + let autoFocusTarget: CalendarDay | undefined; + + const isValidFocusTarget = (day: CalendarDay) => { + return isDayDisplayed(day) && !getModifiers(day)[DayModifier.disabled]; + }; + + if (isInteractive) { + if (focused) { + autoFocusTarget = focused; + } else if (lastFocused) { + autoFocusTarget = lastFocused; + } else if ( + calendarModifiers.selected[0] && + isValidFocusTarget(calendarModifiers.selected[0]) + ) { + autoFocusTarget = calendarModifiers.selected[0]; + } else if (today && isValidFocusTarget(today)) { + autoFocusTarget = today; + } else if (calendarModifiers.focusable[0]) { + autoFocusTarget = calendarModifiers.focusable[0]; + } + } // Focus the focus target when autoFocus is passed in useEffect(() => { @@ -103,7 +125,7 @@ export function FocusProvider(props: { children: ReactNode }): JSX.Element { function moveFocus(moveBy: MoveFocusBy, moveDir: MoveFocusDir) { if (!focused) return; - const nextFocus = getNextFocus(moveBy, moveDir, focused, dayPickerProps); + const nextFocus = getNextFocus(moveBy, moveDir, focused, props); if (!nextFocus) return; goToDay(nextFocus); @@ -129,9 +151,7 @@ export function FocusProvider(props: { children: ReactNode }): JSX.Element { }; return ( - - {props.children} - + {children} ); }