diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts index 31427c9fe..74d624c60 100644 --- a/e2e/day-view.spec.ts +++ b/e2e/day-view.spec.ts @@ -224,6 +224,17 @@ test.describe("the day view", () => { // The keyboard: Enter opens, Escape closes the calendar only. await page.keyboard.press("Enter"); await expect(picker).toBeVisible(); + // The calendar has the keyboard before Escape is pressed. + await expect + .poll(() => + page.evaluate( + () => + document.activeElement?.closest( + '[role="dialog"]:not([data-slot="day-panel"])', + ) != null, + ), + ) + .toBe(true); await page.keyboard.press("Escape"); await expect(picker).toHaveCount(0); await expect(panel(page)).toBeVisible(); diff --git a/src/components/day/day-date-picker.tsx b/src/components/day/day-date-picker.tsx index a85fef543..eda77a736 100644 --- a/src/components/day/day-date-picker.tsx +++ b/src/components/day/day-date-picker.tsx @@ -1,9 +1,10 @@ "use client"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Calendar, CalendarDayButton } from "@/components/ui/calendar"; +import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { resolveDateFnsLocale } from "@/components/ui/date-field"; import { Popover, @@ -25,9 +26,10 @@ import { useDayIndex } from "./use-day"; * * No icon and no field: the header keeps its one line, and the arrows * beside it stay the way to the neighbouring days. Radix owns the keyboard - * (Enter or Space opens, Escape closes, focus returns to the date), and - * the same popover serves the docked column and both sheets, with touch- - * sized cells wherever the pointer is coarse. + * (Enter or Space opens, Escape closes, focus returns to the date). A + * popover beside the date serves the docked column and the side sheet; in + * the phone's bottom sheet the calendar rises as a sheet of its own, with + * touch-sized cells wherever the pointer is coarse. */ /** `YYYY-MM-DD` → a local `Date` at midnight, the calendar's own unit. */ @@ -75,6 +77,7 @@ export function DayDatePicker({ today, label, onPick, + variant = "popover", className, }: { date: DateKey; @@ -82,33 +85,91 @@ export function DayDatePicker({ /** The date as the header writes it: the button's text. */ label: React.ReactNode; onPick: (date: DateKey) => void; + /** + * `sheet` in the phone's bottom sheet: a month of finger-sized cells + * does not fit beside a header that sits halfway down the screen, so the + * calendar rises as its own bottom sheet there. + */ + variant?: "popover" | "sheet"; className?: string; }) { const { t } = useTranslations(); const [open, setOpen] = useState(false); + const buttonRef = useRef(null); const [month, setMonth] = useState(() => keyToLocalDate(date)); const range = monthWindow(month, today); const index = useDayIndex(range.from, range.to, open); - return ( - { - // Each opening starts on the month of the day on screen. - if (next) setMonth(keyToLocalDate(date)); - setOpen(next); + const onOpenChange = (next: boolean) => { + // Each opening starts on the month of the day on screen. + if (next) setMonth(keyToLocalDate(date)); + setOpen(next); + }; + const buttonClass = cn( + "hover:bg-muted/60 focus-visible:ring-ring/50 data-[state=open]:bg-muted/60 -mx-1 block max-w-full truncate rounded-md px-1 text-left transition-colors focus-visible:ring-[3px] focus-visible:outline-none", + className, + ); + const calendar = ( + { + setOpen(false); + const day = pickedDay(next, date, today); + if (day !== null) onPick(day); }} - > + /> + ); + + if (variant === "sheet") { + return ( + <> + + { + event.preventDefault(); + buttonRef.current?.focus({ preventScroll: true }); + }} + > +
+ {calendar} +
+
+ + ); + } + + return ( + @@ -119,18 +180,7 @@ export function DayDatePicker({ aria-label={t("day.pickerTitle")} className="w-auto p-0" > - { - setOpen(false); - const day = pickedDay(next, date, today); - if (day !== null) onPick(day); - }} - /> + {calendar} ); diff --git a/src/components/day/day-view.tsx b/src/components/day/day-view.tsx index e8d4b145c..831653891 100644 --- a/src/components/day/day-view.tsx +++ b/src/components/day/day-view.tsx @@ -238,6 +238,7 @@ export function DayView({ date={date} today={today} onPick={onPick} + variant={compact ? "sheet" : "popover"} label={ compact ? ( <> diff --git a/src/components/insights/coach-panel/conversations-panel.tsx b/src/components/insights/coach-panel/conversations-panel.tsx index 92fab16cd..41bd0d228 100644 --- a/src/components/insights/coach-panel/conversations-panel.tsx +++ b/src/components/insights/coach-panel/conversations-panel.tsx @@ -318,32 +318,29 @@ export function ConversationsPanel({ )} > {inSheet ? null : ( - - - - - - {t("insights.coach.frame.hidePanel")} - - + // A title, not a tooltip: focus lands here when the panel opens + // from its edge, and a tooltip opened by that focus would take the + // next Escape for itself. + )} {inSheet ? ( @@ -475,30 +472,24 @@ export function ConversationsPanel({ "border-sidebar-border flex shrink-0 items-center justify-center", )} > - - - - - - {t("insights.coach.frame.showPanel")} - - + {/* The rest of the edge takes a click too and names the list; the keyboard has the button above. */}