Repository: gpbl/react-day-picker
Pull request #2174 (2024-05-31 00:08:06)

fix: today day is auto focused when available


Patch:
diff --git a/src/components/DayWrapper.tsx b/src/components/DayWrapper.tsx
index a8f4636aa..252587576 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 18b8eac67..1432aa3b8 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<CalendarContext | undefined>(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.tsx b/src/contexts/focus.tsx
index b801e9c5d..f81a8d478 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<FocusContext | undefined>(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<CalendarDay | undefined>();
   const [lastFocused, setLastFocused] = useState<CalendarDay | undefined>();
   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 (
-    <focusContext.Provider value={value}>
-      {props.children}
-    </focusContext.Provider>
+    <focusContext.Provider value={value}>{children}</focusContext.Provider>
   );
 }
 


Test patch:
diff --git a/src/contexts/focus.test.tsx b/src/contexts/focus.test.tsx
new file mode 100644
index 000000000..dcf908486
--- /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);
+      });
+    });
+  });
+});