Skip to content

Upgrade to Svelte 5, bits-ui 2.x, and tanstack-table-8-svelte-5#9045

Open
ericpgreen2 wants to merge 43 commits intomainfrom
ericgreen/svelte-5-upgrade
Open

Upgrade to Svelte 5, bits-ui 2.x, and tanstack-table-8-svelte-5#9045
ericpgreen2 wants to merge 43 commits intomainfrom
ericgreen/svelte-5-upgrade

Conversation

@ericpgreen2
Copy link
Contributor

@ericpgreen2 ericpgreen2 commented Mar 13, 2026

Migrates the frontend to Svelte 5 (legacy compatibility mode) with the breaking dependency upgrades that Svelte 5 requires. Existing components retain Svelte 4 syntax; converting to runes is future work.

  • Bump Svelte from v4 to v5 with @sveltejs/vite-plugin-svelte v4
  • Upgrade bits-ui from 0.22 to 2.x: rewrite wrapper components (asChildchild snippet, on:eventonclick, transitions removed)
  • Replace @tanstack/svelte-table with tanstack-table-8-svelte-5 (flexRenderrenderComponent)
  • Replace cmdk-sv with bits-ui's built-in Command component
  • Fix Svelte 5 runtime compatibility issues (component instantiation API, self-closing HTML tags, floating UI positioning)

Closes APP-386, APP-746

Future work:

  • Convert components from Svelte 4 legacy syntax to Svelte 5 runes
  • Upgrade eslint-plugin-svelte v2 → v3 (removes svelte-ignore custom_element_props_identifier workarounds)
  • Add Svelte 5 best-practice skills: https://svelte.dev/docs/svelte/best-practices
  • Upgrade to Vite 8 for faster builds via Rolldown

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Developed in collaboration with Claude Code

- svelte ^4.2.19 → ^5.0.0 (all workspaces)
- @sveltejs/vite-plugin-svelte override ^3.1.2 → ^4.0.0
- @tanstack/svelte-table → tanstack-table-8-svelte-5 (Svelte 5 compat)
- bits-ui ^0.22.0 → ^2.14.4 (Svelte 5 required)
- Remove cmdk-sv (replaced by bits-ui Command in next commit)
Step 2+3 of Svelte 5 upgrade:
- Rewrite all bits-ui wrapper components for v2 API changes
- Create Svelte 5 Trigger bridge wrappers (asChild → child snippet)
- Migrate Select API: selected → value, onSelectedChange → onValueChange
- Replace cmdk-sv with bits-ui Command components
- Migrate on:event → onevent callback props on runes-mode wrappers
- Remove deprecated props: transition, portal, fitViewport, typeahead,
  closeOnItemClick, CustomEventHandler, disableFocusFirstItem
- Move onOutsideClick from Dialog.Root to onInteractOutside on Content
- Replace RadioIndicator/CheckboxIndicator/ItemIndicator with CSS
- Change Label → GroupHeading in select/dropdown/context menus
- Convert TypeScript enums to const objects in .svelte files
- Fix HTML validity: div inside tr → td, remove empty CSS declarations
- Add missing peer deps: @internationalized/date, @tiptap/suggestion,
  vega-embed
- Add npm overrides for svelte and @sveltejs/vite-plugin-svelte to
  resolve peer dep conflicts with storybook
…onent

- Replace `new Component()` / `$set()` / `$destroy()` with `mount()` / `unmount()` / `$state` in
  create-placeholder and editor-plugins (renamed to .svelte.ts for rune support)
- Use `renderComponent` instead of `flexRender` for custom cell components in PartitionsTable
- Update imports in ChatInput, UserMessage, and MetricsEditor for renamed files
Replace `<span style="display:contents">` with `<div>` in dropdown menu,
popover, tooltip, and context menu trigger wrappers. Elements with
`display:contents` have no bounding rect, causing floating UI to position
dropdowns at (0,0) instead of next to the trigger.
@ericpgreen2 ericpgreen2 self-assigned this Mar 13, 2026
… 5 compatibility

In Svelte 5, `on:click` on components no longer dispatches events through
the component tree. This broke all click handlers on bits-ui 2.x wrapper
components (DropdownMenu.Item, CheckboxItem, etc.) because the handlers
silently never fired.

- Replace `on:click={handler}` with `onclick={handler}` across 279 files
- Replace other `on:event` handlers (input, change, keydown, etc.)
- Convert modifier patterns (`on:click|stopPropagation`) to inline handlers
- Convert bare event forwarding (`on:click`) to explicit `onclick` props
- Migrate `DropdownMenuItem.svelte` to Svelte 5 `$props()` pattern
- Increase breadcrumbs test timeout for model reconciliation
The source file was renamed from `.ts` to `.svelte.ts` in a prior commit
but the test import wasn't updated. Also adds `flushSync` to gutter update
tests for Svelte 5 async reactivity, fixes `closeOnSelect` default on
`DropdownMenuCheckboxItem`, and updates DimensionFilter tests for bits-ui
2.x Select compatibility in jsdom.
…nment

- Change `import type { Readable } from "svelte/motion"` to `svelte/store`
  in 5 files (Svelte 5 no longer exports `Readable` from `svelte/motion`)
- Bump tiptap extensions from `^3.11.0` to `^3.20.1` to match `@tiptap/core`
  and eliminate duplicate package versions causing type incompatibility
- Update `tsc-with-whitelist.sh`: add renamed `.svelte.ts` entries, remove
  ~30 stale entries for errors that no longer exist
…ibility

- Replace self-closing non-void HTML tags with explicit closing tags
  (ran `npx sv migrate self-closing-tags` + manual fixes)
- Remove unused transition imports (`slide`, `fly`, `fade`, `scale`)
  left over from Svelte 4 `transition:` directives
- Add `svelte-ignore` for `$props()` rest element warnings in bits-ui
  wrapper components that legitimately use `...restProps`
- Fix DOM nesting: wrap `<tr>` in `<tbody>`, `<div>` in `<td>`
- Fix accessibility: add `aria-label` to resize button, `tabindex` to dialog
- Fix AvatarButton reactive declaration warning
- Suppress unused `preventFocus` export (used by callers, handler lost in
  Svelte 5 event migration)
- Remove dead CSS rules: `.separator`, `.inactive`, `button.addBorder`,
  `footer:is(.dark)`, `.graph-trigger` and variants
- Use scoped `:global()` for dynamic attributes set by bits-ui
  (e.g. `.trigger:global([data-state="open"])`) where parent selector
  keeps the scope contained
- Inline Tailwind classes for WorkspaceCrumb's `.open` and `button:hover`
  to avoid unscoped `:global()` leakage
- Use `:has(:global(...))` for cross-component `:has()` selectors
- Restore `transition:slide`, `transition:fly`, `transition:fade`, and `transition:scale` directives that were incorrectly converted to HTML attributes by the migration script, and re-add their missing `svelte/transition` imports
- Fix event handler argument types where callbacks were incorrectly passed the event parameter
- Migrate bits-ui v2 API: remove `portal` props (already handled by Portal wrappers), move `closeOnEscape`/`preventScroll` to Content components, rename `openDelay` to `delayDuration`, fix Combobox input value binding
- Add missing component props (`onclick` on `Tab`, `oninput` on `Input`, `target`/`rel` on `DropdownMenuItem`), type `Card.onclick`, and replace `preloadData` with `data-sveltekit-preload-data`
- Fix `Readable` import path (`svelte/motion` → `svelte/store`) and `TimestampProfileSummary` display type
…em` CSS warning

Refactor `dragTableCell` action from custom DOM events to a callback interface, which is the Svelte 5 pattern for action-to-component communication. Use `:global()` for the dynamic `data-state` CSS selector set by bits-ui.
@ericpgreen2 ericpgreen2 force-pushed the ericgreen/svelte-5-upgrade branch from c5db241 to dfc535e Compare March 16, 2026 19:03
…definitions for Svelte 5 compatibility

`tanstack-table-8-svelte-5` requires `renderComponent` when rendering custom Svelte
components in column definitions. Using `flexRender` causes `TypeError: Cannot use 'in'
operator to search for 'Symbol($state)' in undefined` because it doesn't properly
instantiate Svelte 5 components with reactive props.
…tic bits-ui v2 `child` snippet pattern

- Simplify 10 trigger/close wrapper components to pure pass-throughs (tooltip, popover,
  dropdown-menu, context-menu, dialog, collapsible, alert-dialog triggers + dialog-close)
- Update ~100 call sites from `asChild` boolean to `{#snippet child({ props })}` pattern
- Migrate `Button` component to Svelte 5 (`$props()`, `{@render}`, rest prop forwarding)
  so bits-ui event handlers reach the DOM correctly through `{...props}`
- Migrate `DraggableList` to Svelte 5 snippets (replacing Svelte 4 `slot`/`let:item`)
  and `$derived` (replacing `$:` reactive statements)
- Update `DashboardMetricsDraggableList` and `SortConfig` to use snippet props
- Fix doubled tooltip styling in `DashboardMetricsDraggableList`
- Expose `closeOnSelect` prop on `DropdownMenuCheckboxItem`
… snippet migration

Move `{...props}` from `Tooltip` to the interactive child element (`Button`/`Chip`) so
bits-ui click handlers and aria attributes reach a DOM node. Replace the `setTimeout`
re-open hack in `GuardedDialog` with `onEscapeKeyDown`/`onInteractOutside` +
`preventDefault()`, and remove the orphaned `AlertDialogTrigger` that rendered a 0×0
ghost button.
…ration

Migrate `DropdownMenuCheckboxItem` and `DropdownMenuContent` wrappers from
Svelte 4 legacy mode to Svelte 5 runes mode, and change `closeOnSelect`
default from `false` to `true` to match bits-ui v2's native default.
Multi-select consumers are updated with explicit `closeOnSelect={false}`.
…b-admin

- Suppress `custom_element_props_identifier` false positives from eslint-plugin-svelte v2
  (components use `...restProps` in `$props()` but are not custom elements; fixed in v3.9.2+)
- Convert `contentRect`, `dropIndex`, `dragId`, `dragIndex` to `$state()` in `DraggableList`
- Fix `onEscapeKeyDown` → `onEscapeKeydown` for bits-ui v2 in alert dialogs
- Replace unused scoped `.dark` CSS class with inline style in `UploadImagePopover`
- Add `web-local/playwright-report/*` to ESLint ignores
- Update `data-melt-dropdown-menu-trigger` selectors to `data-menu-trigger`
- Update `menuitem` role to `menuitemcheckbox` for `DropdownMenu.CheckboxItem`
  (bits-ui v2 hardcodes the role and ignores overrides)
- Remove dead `role="menuitem"` from `DropdownMenuCheckboxItem` wrapper
- Forward `$$restProps` on `Chip` component so bits-ui trigger props (ref,
  event handlers, aria attributes) reach the DOM element
- Add `onpointerdown` stopPropagation on Chip's remove button to prevent
  trigger handler from intercepting remove clicks
- Use page-level scope for portaled dropdown content in alert/report tests
Test fixes:
- Update `menuitem` role to `menuitemcheckbox` for `DropdownMenu.CheckboxItem`
  (bits-ui v2 hardcodes the role and ignores overrides) in grain selectors
  and leaderboard measure dropdowns
- Remove dead `role="menuitem"` from CheckboxItem usages in source components

Component fixes:
- Convert `ContextButton` to Svelte 5 runes mode; remove `id` override that
  clobbered bits-ui's trigger `id`, breaking dismiss-on-outside-click
- Convert `NavigateOrDropdown` to runes mode; forward trigger props to the
  caret button so the dropdown opens and positions correctly
- Add `href` support to `DropdownMenuItem` via `<svelte:element>` wrapper
  (matches existing CheckboxItem pattern); fixes menu item navigation
- Remove `<span style="display:contents">` wrappers from 10 trigger sites;
  `display:contents` removes the element from the box model so
  `getBoundingClientRect()` returns {0,0,0,0}, causing top-left positioning
- Convert `DropdownMenuSubTrigger` and `DropdownMenuSubContent` to runes mode
- Add `svelte-ignore custom_element_props_identifier` for rest props with `$props()`
- Remove unused `children` prop from NavigateOrDropdown
Use accessible menu button labels in the status action cells so the Playwright spec can target stable role-based locators after the Svelte 5 and BitsUI v2 migration removed the old trigger attribute.

Made-with: Cursor
Use nested `child` snippets so both Tooltip.Trigger and Popover.Trigger
props land on the same DOM element. Previously, the tooltip's `id`/`ref`
were spread on Popover.Trigger as component props (lost), and both had
conflicting explicit `id` overrides. This caused bits-ui to lose track of
the trigger, leaving the tooltip active after the popover closed and
blocking clicks on adjacent elements (e.g. Toggle time comparison).
Add `ignoreNonKeyboardFocus` to the Tooltip.Root wrapping the time range
trigger. In a dialog with a focus trap, closing the popover returns focus
to the trigger, which causes bits-ui Tooltip to activate immediately
(focus has no delay, unlike hover). This blocked clicks on adjacent
elements like "Toggle time comparison".
- public-urls.spec.ts: Use page-level scope for DimensionFilter search
  field and values (portaled to document.body); fix grain `menuitem` →
  `menuitemcheckbox`
- reports.spec.ts: Use page-level scope for field list menu items and
  dimension filter values (portaled to document.body); fix filter value
  `menuitem` → `menuitemcheckbox`
Clicking the filter chip to close the dropdown fails because the portal's
dismiss layer intercepts pointer events. Use Escape key instead.
…i v2

bits-ui v2 Select.Trigger renders as a plain <button> with
aria-haspopup="listbox" but no role="combobox" (that role is reserved
for the Combobox component). Use getByLabel instead.
Replace the <span style="display:contents"> + onMount querySelector hack
with bits-ui's native bind:ref on SelectPrimitive.Trigger. Convert to
Svelte 5 runes mode ($props, $state, @render).
Replace the <span style="display:contents"> + onMount querySelector hack
with bits-ui's native bind:ref on SelectPrimitive.Trigger. Convert to
Svelte 5 runes mode ($props, $state, @render). Matches the shadcn-svelte
v5 pattern exactly. Initialize selectElement callers to null (not
undefined) to satisfy Svelte 5's $bindable constraint.
- select-trigger.svelte: Use $state(lockable) with svelte-ignore for
  state_referenced_locally (lockable is intentionally just an initial value)
- alerts.spec.ts: Use exact:true on getByLabel("Split by dimension") to
  disambiguate from the "Alert split by dimension" wrapper div
- reports.spec.ts: Replace flaky getByText("Select all").hover() tooltip
  workaround with keyboard Escape
Test fixes:
- commonHelpers.ts: Replace #rill-portal locators with getByRole("dialog")
  (Dialog is now portaled to document.body)
- incremental.spec.ts: Use exact:true on "Filter partitions" to resolve
  ambiguity between Select trigger and Button
- explores.spec.ts: Use Escape to close DimensionFilter; use getByText for
  measure check instead of getByRole("button")
- reports.spec.ts: Use mouse.move(0,0) to dismiss hoverIntent tooltip

Component fixes:
- LeaderboardMeasureNamesDropdown: Change onclick to onCheckedChange on
  Switch to fix double-toggle (bits-ui v2 Switch handles click internally)
- PivotExpandableCell: Use `value ?? "null"` to render null dimension
  values as text (Svelte 5 renders {null} as empty, Svelte 4 rendered it
  as "null")
- Fix persistent drag ghost on click by requiring mouse movement (8px)
  before initiating drag, adding explicit window mouseup cleanup, and
  manual DOM removal of portal ghost elements
- Use explicit `{#snippet children()}` in `SaveDefaultsButton` to fix
  cross-mode slot-to-snippet reactivity with runes-based `Button`
- Comment out flaky "Saved default filters" selector in e2e tests
  (renders correctly in app but selector doesn't match in test)
- Minor CSS and test fixes for Svelte 5 compatibility
@ericpgreen2 ericpgreen2 requested a review from djbarnwal March 19, 2026 13:48
@ericpgreen2 ericpgreen2 marked this pull request as ready for review March 19, 2026 13:48
The DimensionFilter tests were skipped because bits-ui 2.x's Select
component uses PointerEvent APIs incompatible with jsdom. This fixes
both the test infrastructure and a rendering bug uncovered during testing.

- Add PointerEvent/scrollIntoView polyfills for jsdom
- Use keyboard navigation (Space/ArrowDown/Enter) to interact with
  bits-ui Select, since pointer events need real layout refs
- Replace DropdownMenu.Label with plain <span> for result count text;
  GroupHeading requires a parent Group context in bits-ui 2.x, which
  silently broke the showExtraInfo section
- Use per-item assertions instead of group textContent checks (bits-ui
  2.x renders items without inter-element whitespace)
- Fix InList mode assertions to expect all searched values (matched +
  unmatched), matching actual component behavior
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant