From dcd8b15d5a83ce45c6127129f839d073bd76964b Mon Sep 17 00:00:00 2001 From: Renato Date: Wed, 5 Aug 2026 10:58:20 +0200 Subject: [PATCH] fix(calendar): keep keyboard navigation alive after picking a date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four `components` overrides were arrow functions written inside `Calendar`'s render, which is where shadcn's generator puts them. That makes them a new component type on every pass, and DayPicker keys its subtree on those identities: any re-render unmounted and remounted the whole grid instead of updating it. `CalendarDayButton` holds a ref and an effect that focuses the day matching `modifiers.focused`, so the remount threw away the focus that effect had just placed. Selecting a start date re-renders the range picker, which meant that after the first click the arrow keys did nothing at all. Confirmed against a running dashboard — click Aug 2, press Right twice, and the focus ring stays on 2. With the components hoisted it moves to 4, which is what it always should have done. Hoisting three of them is a straight move; they closed over nothing. The DayButton wrapper existed only to inject `locale`, so it now reads the same value back out of DayPicker's own context instead of closing over `Calendar`'s prop. This also clears the ten oxlint warnings the file was emitting on every CI run, four no-unstable-nested-components and six no-shadow, but those were the symptom rather than the reason. --- apps/web/app/shared/ui/calendar.tsx | 116 ++++++++++++++++++---------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/apps/web/app/shared/ui/calendar.tsx b/apps/web/app/shared/ui/calendar.tsx index a7b6f4a..0a19a55 100644 --- a/apps/web/app/shared/ui/calendar.tsx +++ b/apps/web/app/shared/ui/calendar.tsx @@ -2,6 +2,8 @@ import * as React from "react"; import { DayPicker, getDefaultClassNames, + useDayPicker, + type CustomComponents, type DayButton, type Locale, } from "react-day-picker"; @@ -14,6 +16,74 @@ import { ChevronDownIcon, } from "lucide-react"; +/** + * The `components` overrides live out here rather than inline in `Calendar`'s + * render, which is where shadcn's generator puts them. + * + * An arrow function written inside the render body is a new component type on + * every pass, and DayPicker keys its subtree on these identities: re-rendering + * the calendar unmounts and remounts the whole grid rather than updating it. + * That is not only wasted work — `CalendarDayButton` holds a ref and an effect + * that focuses the day matching `modifiers.focused`, and a remount drops the + * focus it had just moved. The range picker re-renders on every click, so this + * ran on the path a keyboard user is actually using. + */ +const CalendarRoot: CustomComponents["Root"] = ({ + className, + rootRef, + ...props +}) => { + return ( +
+ ); +}; + +const CalendarChevron: CustomComponents["Chevron"] = ({ + className, + orientation, + ...props +}) => { + if (orientation === "left") { + return ; + } + + if (orientation === "right") { + return ; + } + + return ; +}; + +/** + * Reads the locale back out of DayPicker's own context instead of closing over + * `Calendar`'s prop, which is what forced this one to be defined inline. It is + * the same value: `Calendar` hands `locale` to `DayPicker`, and this renders + * inside it. + */ +const CalendarDayButtonSlot: CustomComponents["DayButton"] = (props) => { + const { dayPickerProps } = useDayPicker(); + + return ; +}; + +const CalendarWeekNumber: CustomComponents["WeekNumber"] = ({ + children, + ...props +}) => { + return ( + +
+ {children} +
+ + ); +}; + function Calendar({ className, classNames, @@ -136,48 +206,10 @@ function Calendar({ ...classNames, }} components={{ - Root: ({ className, rootRef, ...props }) => { - return ( -
- ); - }, - Chevron: ({ className, orientation, ...props }) => { - if (orientation === "left") { - return ( - - ); - } - - if (orientation === "right") { - return ( - - ); - } - - return ( - - ); - }, - DayButton: ({ ...props }) => ( - - ), - WeekNumber: ({ children, ...props }) => { - return ( - -
- {children} -
- - ); - }, + Root: CalendarRoot, + Chevron: CalendarChevron, + DayButton: CalendarDayButtonSlot, + WeekNumber: CalendarWeekNumber, ...components, }} {...props}