From 28a4eb67c5be91d1a2ca347fbf1711506ab7b54c Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 14 Jul 2026 20:08:43 +0530 Subject: [PATCH 1/3] Add TodayIndicator story to Calendar component with accent dot behavior - Introduced a new story, TodayIndicator, demonstrating the visual representation of the current date with a 6px accent dot. - Enhanced existing tests to verify the dot's presence and dimensions when today is selected or unselected. - Updated comments to clarify the behavior of the today indicator and its interaction with selected states. - Refined styles for the today and disabled states to improve visual consistency and accessibility. --- .../components/calendar/calendar.stories.tsx | 36 +++++++++++++++++++ .../elements/calendar/calendar.stories.tsx | 12 +++++-- .../propel/src/elements/calendar/variants.ts | 12 ++++--- 3 files changed, 54 insertions(+), 6 deletions(-) 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/elements/calendar/calendar.stories.tsx b/packages/propel/src/elements/calendar/calendar.stories.tsx index 5a7f5dff..42eec102 100644 --- a/packages/propel/src/elements/calendar/calendar.stories.tsx +++ b/packages/propel/src/elements/calendar/calendar.stories.tsx @@ -477,13 +477,21 @@ export const StatesCanary: Story = { getComputedStyle(resting).backgroundColor, ); - // `today` recolors the (unselected) day's text to the accent tone. + // `today` recolors the (unselected) day's text to the accent tone and paints the 6px + // current-date dot 4px above the CELL's bottom edge — including on a selected today. const today = canvas.getByRole("button", { name: "Day 3, today" }); await expect(getComputedStyle(today).color).not.toBe(getComputedStyle(resting).color); + const todayDot = getComputedStyle(cellOf(today), "::after"); + await expect(todayDot.width).toBe("6px"); + await expect(todayDot.height).toBe("6px"); + await expect(todayDot.bottom).toBe("4px"); + const selectedToday = canvas.getByRole("button", { name: "Day 5, selected today" }); + await expect(getComputedStyle(cellOf(selectedToday), "::after").width).toBe("6px"); - // `disabled` makes the day button non-interactive. + // `disabled` makes the day button non-interactive and visibly recessed. const disabled = canvas.getByRole("button", { name: "Day 6, disabled" }); await expect(getComputedStyle(disabled).pointerEvents).toBe("none"); + await expect(getComputedStyle(disabled).opacity).toBe("0.6"); // `range-middle` paints the soft in-range fill on the CELL, and its button reset beats // `selected`'s accent fill (the cell carries both classes) so the button stays transparent. diff --git a/packages/propel/src/elements/calendar/variants.ts b/packages/propel/src/elements/calendar/variants.ts index c74464e7..78a15ed7 100644 --- a/packages/propel/src/elements/calendar/variants.ts +++ b/packages/propel/src/elements/calendar/variants.ts @@ -60,10 +60,14 @@ export const calendarClassNames: Partialbutton]:bg-transparent [&>button]:text-primary [&>button:hover]:bg-transparent", // Today (when not selected): accent text so the current date stands out. When - // the cell is also selected, the solid accent button (above) wins. - today: "[&:not([aria-selected])>button]:text-accent-primary [&>button]:font-semibold", - // Disabled days read as muted, non-interactive text. - disabled: "[&>button]:text-disabled [&>button]:pointer-events-none", + // the cell is also selected, the solid accent button (above) wins. A 6px dot + // (icon/accent/subtle) sits centered 4px above the cell's bottom edge in + // every state, marking the current date even inside a selected pill. + today: + "[&:not([aria-selected])>button]:text-accent-primary [&>button]:font-semibold after:absolute after:bottom-1 after:left-1/2 after:-translate-x-1/2 after:size-1.5 after:rounded-full after:bg-(--txt-icon-accent-subtle)", + // Disabled days read as muted, non-interactive text — `opacity-60` on top of the disabled + // tone (the propel disabled convention) so they clearly recede from enabled days. + disabled: "[&>button]:text-disabled [&>button]:opacity-60 [&>button]:pointer-events-none", // Days from the adjacent month: dimmed. outside: "[&>button]:text-tertiary", hidden: "invisible", From 91a99460123fffb48bd59ee7fefad4a60cdf0754 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 14 Jul 2026 20:45:10 +0530 Subject: [PATCH 2/3] Refactor Calendar component to enhance props and styling - Updated `CalendarProps` to omit additional props (`numberOfMonths`, `captionLayout`, `showWeekNumber`) that are not styled by propel tokens, ensuring layout consistency. - Introduced a stable `renderChevron` function to optimize the rendering of navigation chevrons, preventing unnecessary remounts. - Modified calendar button styles to use a shared `navButton` class for consistent disabled state representation using `aria-disabled`. - Updated stories to reflect changes in button accessibility and visual behavior when disabled. --- .../src/components/calendar/calendar.tsx | 37 +++++++++++++------ .../elements/calendar/calendar.stories.tsx | 7 +++- .../propel/src/elements/calendar/variants.ts | 13 ++++--- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/propel/src/components/calendar/calendar.tsx b/packages/propel/src/components/calendar/calendar.tsx index d4bfe525..d771418d 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 @@ -30,12 +50,7 @@ export function Calendar(props: CalendarProps) { classNames={calendarClassNames} {...props} components={{ - Chevron: ({ orientation, className }) => - 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 42eec102..9be4feb8 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 = {