diff --git a/packages/propel/src/components/calendar/calendar.stories.tsx b/packages/propel/src/components/calendar/calendar.stories.tsx index 5434b148..cb3eb79b 100644 --- a/packages/propel/src/components/calendar/calendar.stories.tsx +++ b/packages/propel/src/components/calendar/calendar.stories.tsx @@ -75,6 +75,42 @@ export const WithDisabledDays: Story = { }, }; +/** + * The current date carries a 6px accent dot, centered 4px above the bottom edge of its 40px cell. + * The dot persists when today is selected (inside the solid pill) or mid-range. `today` is pinned + * to Jan 6 so the story renders deterministically. + */ +export const TodayIndicator: Story = { + render: function Render() { + const [selected, setSelected] = React.useState(); + return ( + + ); + }, + play: async ({ canvas, userEvent }) => { + const todayButton = canvas.getByRole("button", { name: /^Today, .*January 6th, 2025/ }); + const todayCell = todayButton.closest("td") as HTMLTableCellElement; + const dot = getComputedStyle(todayCell, "::after"); + await expect(dot.width).toBe("6px"); + await expect(dot.height).toBe("6px"); + await expect(dot.bottom).toBe("4px"); + + // The dot survives selection: select today, the pseudo-element is still there. + await userEvent.click(todayButton); + await expect(canvas.getByRole("gridcell", { selected: true })).toHaveAttribute( + "data-day", + "2025-01-06", + ); + await expect(getComputedStyle(todayCell, "::after").width).toBe("6px"); + }, +}; + /** * Behavior tests: the month grid renders day buttons, clicking a day selects it, and a disabled day * stays unselectable. Tagged out of the sidebar/docs/manifest but still runs under `test`. A fixed diff --git a/packages/propel/src/components/calendar/calendar.tsx b/packages/propel/src/components/calendar/calendar.tsx index d4bfe525..cec9eeea 100644 --- a/packages/propel/src/components/calendar/calendar.tsx +++ b/packages/propel/src/components/calendar/calendar.tsx @@ -4,16 +4,36 @@ import { DayPicker, type DayPickerProps } from "react-day-picker"; import { calendarClassNames } from "../../elements/calendar"; /** - * Props for {@link Calendar}. Forwards every react-day-picker `DayPickerProps` (`mode`, `selected`, - * `onSelect`, `disabled`, `month`, `defaultMonth`, …) but drops `className`/`style`/`classNames` — - * styling is owned by propel tokens (the `elements/calendar` `calendarClassNames` contract, grafted - * below). + * Props for {@link Calendar}. Forwards react-day-picker's `DayPickerProps` (`mode`, `selected`, + * `onSelect`, `disabled`, `month`, `defaultMonth`, …). `className`/`style`/`classNames` are dropped + * because styling is owned by propel tokens (the `elements/calendar` `calendarClassNames` contract, + * grafted below). `numberOfMonths`/`captionLayout`/`showWeekNumber` are dropped because + * `calendarClassNames` does not style those layout modes — they would render with + * react-day-picker's unstyled default classes (we never load its stylesheet), so we omit them + * rather than accept a prop that silently breaks the layout. Add the `classNames` keys + stories + * first if a mode is needed. */ // `DayPickerProps` is a discriminated union over `mode`; a plain `Omit` collapses // it and de-correlates `selected`/`onSelect`. Omit distributively so each union // member (and its mode↔selected↔onSelect relationship) is preserved. type DistributiveOmit = T extends unknown ? Omit : never; -export type CalendarProps = DistributiveOmit; +export type CalendarProps = DistributiveOmit< + DayPickerProps, + "className" | "style" | "classNames" | "numberOfMonths" | "captionLayout" | "showWeekNumber" +>; + +// Hoisted so its identity is stable across renders — react-day-picker keys its component registry +// by identity, so an inline definition would remount the chevron subtree on every render. It +// captures nothing, so module scope is safe. +const renderChevron: NonNullable["Chevron"] = ({ + orientation, + className, +}) => + orientation === "left" ? ( + + ) : ( + + ); /** * The ready-made date picker: react-day-picker's `DayPicker` behavior grafted onto propel's @@ -22,20 +42,16 @@ export type CalendarProps = DistributiveOmit - orientation === "left" ? ( - - ) : ( - - ), + Chevron: renderChevron, ...props.components, }} /> diff --git a/packages/propel/src/elements/calendar/calendar.stories.tsx b/packages/propel/src/elements/calendar/calendar.stories.tsx index 5a7f5dff..79410ac3 100644 --- a/packages/propel/src/elements/calendar/calendar.stories.tsx +++ b/packages/propel/src/elements/calendar/calendar.stories.tsx @@ -243,7 +243,7 @@ export const States: Story = {