From 2ea7b0ac207fac101992cc25b695f5d65b691874 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 2 Jun 2026 22:51:45 +0530 Subject: [PATCH 1/2] fix: calendar issues --- .../calendar/__tests__/date-picker.test.tsx | 24 +++++++++- .../raystack/components/calendar/calendar.tsx | 44 +++++++++---------- .../components/calendar/date-picker.tsx | 15 +------ .../components/calendar/use-picker-popover.ts | 19 ++++++-- 4 files changed, 62 insertions(+), 40 deletions(-) diff --git a/packages/raystack/components/calendar/__tests__/date-picker.test.tsx b/packages/raystack/components/calendar/__tests__/date-picker.test.tsx index dbc539924..0bf7ff635 100644 --- a/packages/raystack/components/calendar/__tests__/date-picker.test.tsx +++ b/packages/raystack/components/calendar/__tests__/date-picker.test.tsx @@ -1,4 +1,5 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import dayjs from 'dayjs'; import type { ReactElement } from 'react'; import { describe, expect, it, vi } from 'vitest'; @@ -589,4 +590,25 @@ describe('DatePicker', () => { }).not.toThrow(); }); }); + + describe('open/close on trigger click', () => { + /* + * Regression: Base UI's `Popover.Trigger` toggles open on every trigger + * click. The input's `onFocus` opens the picker, so the same click's + * trigger-press toggled it straight back closed — the popover flickered + * shut on the first click and only stuck open on the second. The hook's + * `onOpenChange` now ignores trigger-press *closes* (see use-picker-popover). + */ + it('opens and stays open on the first click of the input', async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByPlaceholderText('Select date')); + await act(async () => { + await new Promise(r => setTimeout(r, 0)); + }); + + expect(screen.queryByRole('dialog')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/raystack/components/calendar/calendar.tsx b/packages/raystack/components/calendar/calendar.tsx index 24ed5442e..4d81b4040 100644 --- a/packages/raystack/components/calendar/calendar.tsx +++ b/packages/raystack/components/calendar/calendar.tsx @@ -123,28 +123,28 @@ export const Calendar = function ({ showOutsideDays={showOutsideDays} timeZone={timeZone} components={{ - Chevron: props => { - const icon = - props.orientation === 'left' ? ( - - ) : ( - - ); - - return ( - - {icon} - - ); - }, + PreviousMonthButton: ({ children, ...props }) => ( + + + + ), + NextMonthButton: ({ children, ...props }) => ( + + + + ), Dropdown: (props: DropdownProps) => ( { + onOpenChange={(open, eventDetails) => { if (isDisabled) return; - popover.onOpenChange(open); + popover.onOpenChange(open, eventDetails?.reason); }} > ; handleInputFocus: () => void; handleInputBlur: (event: React.FocusEvent) => void; - onOpenChange: (open?: boolean) => void; + onOpenChange: (open?: boolean, reason?: string) => void; /* * Pass as Calendar's `onDropdownOpen` so the year/month dropdown isn't * treated as an outside click. @@ -138,18 +138,29 @@ export function usePickerPopover({ [isElementOutside, engage] ); - const onOpenChange = useCallback((open?: boolean) => { + const onOpenChange = useCallback((open?: boolean, reason?: string) => { // Year/month dropdown opening inside the popover triggers an open-change // we don't want; swallow it and consume the flag. if (isDropdownOpenRef.current) { isDropdownOpenRef.current = false; return; } + /* + * Base UI's `Popover.Trigger` wires `useClick`, which *toggles* the popover + * on every trigger click. The input's `onFocus` already opens the picker, so + * a single click both opens (focus) and then toggles back closed + * (trigger-press) — the popover flickers shut on the first click and only + * sticks open on the second. Ignore trigger-press *closes*: opening stays + * owned by focus (and trigger-press open), while closing is owned by our + * outside-click / blur / Enter / day-select logic — plus Base UI's own + * Escape/outside-press, which still flow through below. + */ + if (reason === 'trigger-press' && open === false) return; /* * Suppress only redundant *re-open* events fired by focus/click handlers * while the picker is already engaged + open. Explicit close requests - * (Escape key, trigger toggle, programmatic) must always go through, or - * users get stuck with no way to close. + * (Escape key, programmatic) must always go through, or users get stuck + * with no way to close. */ if (open === true && isEngagedRef.current && isOpenRef.current) return; setIsOpen(Boolean(open)); From 7911a411f15f2d27f986e5b729cefb55ee25ea68 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Wed, 3 Jun 2026 10:23:03 +0530 Subject: [PATCH 2/2] fix: disabled logic --- packages/raystack/components/calendar/calendar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/raystack/components/calendar/calendar.tsx b/packages/raystack/components/calendar/calendar.tsx index 4d81b4040..2cc0c8bc8 100644 --- a/packages/raystack/components/calendar/calendar.tsx +++ b/packages/raystack/components/calendar/calendar.tsx @@ -126,7 +126,7 @@ export const Calendar = function ({ PreviousMonthButton: ({ children, ...props }) => ( (