Files
calendar/packages/coss-ui/migration_guide.md
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
25fa46fd34 fix(coss-ui): convert /settings/my-account/general to coss-ui (#26053)
* fix(coss-ui): convert /settings/my-account/general to coss-ui

* update guide

* fix: add menuPosition fixed to TimezoneSelect in TravelScheduleModal

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: use menuPortalTarget to render TimezoneSelect dropdown above dialog footer

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: use Object.assign for type-safe styles in TimezoneSelect

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* revert comment

* fix: guard document access for SSR in TravelScheduleModal

Addresses Cubic AI review feedback: menuPortalTarget={document.body} will
throw during server rendering where document is undefined. Added typeof
check to guard against SSR.

Co-Authored-By: unknown <>

* change dialog backdrop from blur to dim

* fix: add menuPlacement auto to open dropdown upward on mobile

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: use menuPlacement top on mobile for TimezoneSelect dropdown

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: make timezone button stack vertically on mobile to prevent overflow

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: make timezone select and button 50/50 width on larger viewports

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix date range picker issue on mobile

* prevent travel schedule dialog dismissal with date picker

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-23 11:19:00 +00:00

4.8 KiB
Raw Blame History

title, description
title description
`@coss/ui` Migration Guide Entry point for migrating to `@coss/ui` (Base UI), with troubleshooting, dos and don'ts, and links to component-level guides.

Start Here

This is the entry point for migrating to @coss/ui. It covers shared guidance, pitfalls, and troubleshooting. For component-by-component details and Radix/shadcn patterns, use the general guide:

Quick Checklist

  • Identify all @calcom/ui usages in the scope of the migration.
  • Replace Radix asChild with Base UI render where needed.
  • Update naming differences (*Content*Popup/*Panel).
  • Validate dialog, menu, and popover behavior (portals and focus).
  • Run type checks and targeted UI smoke tests.

Do and Don't

Do

  • Prefer Base UI mental models: render for composability, *Popup/*Panel for content.
  • Use direct component imports instead of barrel imports.
  • Keep diffs small and scoped to a single feature or page.
  • When converting a shared component, audit other pages/components that consume it and either migrate them in the same PR or explicitly test them.

Don't

  • Dont keep lazy-loaded components inside Base UI dialogs without verifying render behavior.
  • Dont rely on Radix-specific props (asChild, type="single", collapsible) without mapping them.
  • Dont assume API parity; check the component guide first.
  • Dont over-migrate in a single change; prioritize targeted conversions and verification.

Knowledge Base

Base UI vs Radix UI mental model

@coss/ui is built on Base UI from the ground up. Many component names are similar, but the APIs are not drop-in compatible. Base UI favors render props for composition and often exposes *Popup or *Panel instead of *Content.

Render vs asChild

Radixs asChild becomes Base UIs render. This affects triggers, close buttons, and other slots that used to render children directly.

Naming conventions

Common renames to expect:

  • DialogContentDialogPopup
  • AlertDialogContentAlertDialogPopup
  • AccordionContentAccordionPanel

Legacy names may remain for compatibility, but new usage should follow Base UI naming.

Troubleshooting

Base UI Dialog closes when a Radix Popover is opened

Symptom: A Base UI Dialog closes when you open a Radix Popover inside it and then click anywhere in the dialog. This is most visible in Chrome.

Root cause: Radix popovers render in a portal outside the dialog DOM tree. Base UI's dialog uses outside-interaction detection to dismiss itself. In Chrome, focus and pointer events from the popover portal can be interpreted as outside presses, so the dialog closes even though the user is still interacting with the dialog.

Solution: Disable pointer dismissal while the Radix popover is open or migrate the popover to Base UI. For the travel schedule modal, we track the Radix popover open state and pass disablePointerDismissal to the dialog while the calendar is open.

// apps/web/components/settings/TravelScheduleModal.tsx
const [isDateRangeOpen, setIsDateRangeOpen] = useState(false);

<Dialog open={open} disablePointerDismissal={isDateRangeOpen} onOpenChange={onOpenChange}>
  <DateRangePicker onPopoverOpenChange={setIsDateRangeOpen} />
</Dialog>

Infinite loop with lazy-loaded components inside Base UI Dialog

Symptom: When opening a coss-ui Dialog (Base UI) containing a lazy-loaded component (via next/dynamic), an infinite render loop occurs:

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.

Root cause: Components wrapped in next/dynamic have an async mounting lifecycle. When combined with Base UI Dialog's portal mounting, this creates a conflict that triggers infinite re-renders.

For example, DateRangePicker from @calcom/ui/components/form is lazy-loaded:

// packages/ui/components/form/date-range-picker/index.ts
export const DateRangePickerLazy = dynamic(() =>
  import("./DateRangePicker").then((mod) => mod.DatePickerWithRange)
);

Solution: Import the component directly without the lazy loading wrapper. Add a direct export path to the package.json if needed:

// packages/ui/package.json - add direct export
"./components/form/date-range-picker/DateRangePicker": "./components/form/date-range-picker/DateRangePicker.tsx"

Then import directly:

// Instead of:
import { DateRangePicker } from "@calcom/ui/components/form";

// Use:
import { DatePickerWithRange as DateRangePicker } from "@calcom/ui/components/form/date-range-picker/DateRangePicker";

Note: Regular Radix components (like SettingsToggle, DatePicker, etc.) work fine inside Base UI Dialog as long as they're not lazy-loaded.