From 6a4cbefbcf175cd8b7857d53949b447411f23f83 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:44:11 +1000 Subject: [PATCH] feat(ui): align combobox trigger with select styling (#49665) ## What kind of change does this PR introduce? Bug fix and shared UI primitive. ## What is the current behaviour? In **Database > Replication > Add destination**, the searchable **Publication** field uses a double chevron and button styling. It looks inconsistent beside the **Initial sync** select, which uses a single chevron and the standard select surface. The shared multi-select trigger uses the same inconsistent double chevron. These differences are easy to repeat because searchable comboboxes do not have a dedicated trigger that shares the select styling. ## What is the new behaviour? - Adds `ComboboxTrigger` with the same shared styling as `SelectTrigger` - Uses a fixed single down chevron so combobox and select triggers cannot drift - Migrates the Studio **Database > Replication > Add destination > Publication** field to `ComboboxTrigger` - Updates `MultiSelectorTrigger` to replace `ChevronsUpDown` with the same fixed `ChevronDown`, including **Connect > MCP > Feature groups** in Studio - Updates the canonical design-system combobox examples - Leaves `Button` unchanged, so adopting combobox styling is explicit | Before | After | | --- | --- | | CleanShot 2026-09-09 at 10 21
32@2x | CleanShot 2026-09-09 at 10 21
11@2x | | CleanShot 2026-09-09 at 10 58
28@2x | CleanShot 2026-09-09 at 10 57
57@2x | ## Left for incremental migration These existing Studio comboboxes still use bespoke button triggers and will move to `ComboboxTrigger` in focused follow-up changes: - **Database > Backups > Point in time > Timezone** - **Authentication > Third-party auth > AWS region** - **Account > Preferences > Timezone** - Shared schema and function selectors See https://github.com/supabase/supabase/pull/50157. The organisation and project switchers in the top navigation are intentionally out of scope. They are context switchers rather than form-field comboboxes. ## To test ### Studio callsites On the [Studio preview](https://studio-staging-git-dnywh-featcombobox-trigger-supabase.vercel.app): 1. Open **Database > Replication > Add destination**. 2. Compare **Publication** with **Initial sync**. Both should use a single down chevron and the same closed-field surface, border, sizing, and hover treatment. 3. Open **Publication**, search for an option, and select it. The popover should close and the selected publication should be shown. 4. Open **Connect** from the project header, select **MCP**, and check **Feature groups**. Its multi-select trigger should use a single down chevron. ### Design system 1. Check the [combobox examples](https://design-system-git-dnywh-featcombobox-trigger-supabase.vercel.app/design-system/docs/components/combobox). 2. Check the [multi-select examples](https://design-system-git-dnywh-featcombobox-trigger-supabase.vercel.app/design-system/docs/fragments/multi-select). 3. Both trigger types should use a single down chevron. --- .../content/docs/components/combobox.mdx | 23 ++++++------ .../default/example/combobox-demo.tsx | 15 +++----- .../default/example/combobox-form.tsx | 28 ++++++-------- .../DestinationForm/PublicationsComboBox.tsx | 17 ++++----- .../src/multi-select/multi-select.test.tsx | 1 + .../src/multi-select/multi-select.tsx | 5 ++- .../shadcn/ui/combobox-trigger.test.tsx | 22 +++++++++++ .../components/shadcn/ui/combobox-trigger.tsx | 37 +++++++++++++++++++ .../components/shadcn/ui/select-trigger.tsx | 21 +++++++++++ .../ui/src/components/shadcn/ui/select.tsx | 34 +++++------------ 10 files changed, 130 insertions(+), 73 deletions(-) create mode 100644 packages/ui/src/components/shadcn/ui/combobox-trigger.test.tsx create mode 100644 packages/ui/src/components/shadcn/ui/combobox-trigger.tsx create mode 100644 packages/ui/src/components/shadcn/ui/select-trigger.tsx diff --git a/apps/design-system/content/docs/components/combobox.mdx b/apps/design-system/content/docs/components/combobox.mdx index 21d55d94ae8..3ad33b0124e 100644 --- a/apps/design-system/content/docs/components/combobox.mdx +++ b/apps/design-system/content/docs/components/combobox.mdx @@ -19,18 +19,21 @@ See installation instructions for the [Popover](/docs/components/popover#install ```tsx 'use client' -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check } from 'lucide-react' import * as React from 'react' - -import { Button } from '@/components/ui/button' import { + ComboboxTrigger, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, -} from '@/components/ui/command' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + CommandList, + Popover, + PopoverContent, + PopoverTrigger, +} from 'ui' + import { cn } from '@/lib/utils' const frameworks = [ @@ -63,17 +66,15 @@ export function ComboboxDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/combobox-demo.tsx b/apps/design-system/registry/default/example/combobox-demo.tsx index 5216f8b4ada..280d9b772b5 100644 --- a/apps/design-system/registry/default/example/combobox-demo.tsx +++ b/apps/design-system/registry/default/example/combobox-demo.tsx @@ -1,9 +1,9 @@ 'use client' -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check } from 'lucide-react' import * as React from 'react' import { - Button, + ComboboxTrigger, Command, CommandEmpty, CommandGroup, @@ -47,18 +47,15 @@ export default function ComboboxDemo() { return ( - + diff --git a/apps/design-system/registry/default/example/combobox-form.tsx b/apps/design-system/registry/default/example/combobox-form.tsx index a0a71ccd955..44545f313a2 100644 --- a/apps/design-system/registry/default/example/combobox-form.tsx +++ b/apps/design-system/registry/default/example/combobox-form.tsx @@ -1,11 +1,13 @@ 'use client' import { zodResolver } from '@hookform/resolvers/zod' -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check } from 'lucide-react' +import { useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, + ComboboxTrigger, Command, CommandEmpty, CommandGroup, @@ -46,6 +48,7 @@ const FormSchema = z.object({ }) export default function ComboboxForm() { + const [open, setOpen] = useState(false) const form = useForm>({ resolver: zodResolver(FormSchema), }) @@ -69,28 +72,18 @@ export default function ComboboxForm() { render={({ field }) => ( Language - + - + @@ -105,6 +98,7 @@ export default function ComboboxForm() { key={language.value} onSelect={() => { form.setValue('language', language.value) + setOpen(false) }} > - + { const trigger = screen.getByRole('combobox') expect(trigger).toHaveClass('h-[26px]', 'p-0.5') expect(trigger.firstElementChild).toHaveClass('gap-0.5') + expect(trigger.querySelector('.lucide-chevron-down')).toHaveAttribute('aria-hidden', 'true') }) it('renders selected values with a custom label', () => { diff --git a/packages/ui-patterns/src/multi-select/multi-select.tsx b/packages/ui-patterns/src/multi-select/multi-select.tsx index a26425c0220..01de5c3354a 100644 --- a/packages/ui-patterns/src/multi-select/multi-select.tsx +++ b/packages/ui-patterns/src/multi-select/multi-select.tsx @@ -1,7 +1,7 @@ 'use client' import { cva, VariantProps } from 'class-variance-authority' -import { Check, ChevronsUpDown, X as RemoveIcon } from 'lucide-react' +import { Check, ChevronDown, X as RemoveIcon } from 'lucide-react' // @ts-ignore Required to avoid TS error: The inferred type of MultiSelectorContent cannot be named without a reference to @radix-ui import type { Popover as PopoverPrimitive } from 'radix-ui' import React, { Children, useEffect } from 'react' @@ -489,7 +489,8 @@ const MultiSelectorTrigger = React.forwardRef {showIcon && ( -