Skip to content

Commit 5622272

Browse files
authored
Merge pull request #829 from RooVetGit/cte/shadcn-combobox
Add `@shadcn/ui` components required for the combobox
2 parents 1d85424 + f05dd88 commit 5622272

File tree

8 files changed

+1233
-65
lines changed

8 files changed

+1233
-65
lines changed

webview-ui/package-lock.json

Lines changed: 862 additions & 65 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
"build-storybook": "storybook build"
1515
},
1616
"dependencies": {
17+
"@radix-ui/react-dialog": "^1.1.6",
1718
"@radix-ui/react-dropdown-menu": "^2.1.5",
1819
"@radix-ui/react-icons": "^1.3.2",
20+
"@radix-ui/react-popover": "^1.1.6",
1921
"@radix-ui/react-slot": "^1.1.1",
2022
"@tailwindcss/vite": "^4.0.0",
2123
"@vscode/webview-ui-toolkit": "^1.4.0",
2224
"class-variance-authority": "^0.7.1",
2325
"clsx": "^2.1.1",
26+
"cmdk": "^1.0.0",
2427
"debounce": "^2.1.1",
2528
"fast-deep-equal": "^3.1.3",
2629
"fzf": "^0.5.2",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as React from "react"
2+
import { type DialogProps } from "@radix-ui/react-dialog"
3+
import { Command as CommandPrimitive } from "cmdk"
4+
import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
import { Dialog, DialogContent } from "@/components/ui/dialog"
9+
10+
const Command = React.forwardRef<
11+
React.ElementRef<typeof CommandPrimitive>,
12+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
13+
>(({ className, ...props }, ref) => (
14+
<CommandPrimitive
15+
ref={ref}
16+
className={cn(
17+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
18+
className,
19+
)}
20+
{...props}
21+
/>
22+
))
23+
Command.displayName = CommandPrimitive.displayName
24+
25+
const CommandDialog = ({ children, ...props }: DialogProps) => {
26+
return (
27+
<Dialog {...props}>
28+
<DialogContent className="overflow-hidden p-0">
29+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
30+
{children}
31+
</Command>
32+
</DialogContent>
33+
</Dialog>
34+
)
35+
}
36+
37+
const CommandInput = React.forwardRef<
38+
React.ElementRef<typeof CommandPrimitive.Input>,
39+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
40+
>(({ className, ...props }, ref) => (
41+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
42+
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
43+
<CommandPrimitive.Input
44+
ref={ref}
45+
className={cn(
46+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
47+
className,
48+
)}
49+
{...props}
50+
/>
51+
</div>
52+
))
53+
54+
CommandInput.displayName = CommandPrimitive.Input.displayName
55+
56+
const CommandList = React.forwardRef<
57+
React.ElementRef<typeof CommandPrimitive.List>,
58+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
59+
>(({ className, ...props }, ref) => (
60+
<CommandPrimitive.List
61+
ref={ref}
62+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
63+
{...props}
64+
/>
65+
))
66+
67+
CommandList.displayName = CommandPrimitive.List.displayName
68+
69+
const CommandEmpty = React.forwardRef<
70+
React.ElementRef<typeof CommandPrimitive.Empty>,
71+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
72+
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
73+
74+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
75+
76+
const CommandGroup = React.forwardRef<
77+
React.ElementRef<typeof CommandPrimitive.Group>,
78+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
79+
>(({ className, ...props }, ref) => (
80+
<CommandPrimitive.Group
81+
ref={ref}
82+
className={cn(
83+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
84+
className,
85+
)}
86+
{...props}
87+
/>
88+
))
89+
90+
CommandGroup.displayName = CommandPrimitive.Group.displayName
91+
92+
const CommandSeparator = React.forwardRef<
93+
React.ElementRef<typeof CommandPrimitive.Separator>,
94+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
95+
>(({ className, ...props }, ref) => (
96+
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
97+
))
98+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
99+
100+
const CommandItem = React.forwardRef<
101+
React.ElementRef<typeof CommandPrimitive.Item>,
102+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
103+
>(({ className, ...props }, ref) => (
104+
<CommandPrimitive.Item
105+
ref={ref}
106+
className={cn(
107+
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
108+
className,
109+
)}
110+
{...props}
111+
/>
112+
))
113+
114+
CommandItem.displayName = CommandPrimitive.Item.displayName
115+
116+
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
117+
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
118+
}
119+
CommandShortcut.displayName = "CommandShortcut"
120+
121+
export {
122+
Command,
123+
CommandDialog,
124+
CommandInput,
125+
CommandList,
126+
CommandEmpty,
127+
CommandGroup,
128+
CommandItem,
129+
CommandShortcut,
130+
CommandSeparator,
131+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as DialogPrimitive from "@radix-ui/react-dialog"
5+
import { Cross2Icon } from "@radix-ui/react-icons"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
const Dialog = DialogPrimitive.Root
10+
11+
const DialogTrigger = DialogPrimitive.Trigger
12+
13+
const DialogPortal = DialogPrimitive.Portal
14+
15+
const DialogClose = DialogPrimitive.Close
16+
17+
const DialogOverlay = React.forwardRef<
18+
React.ElementRef<typeof DialogPrimitive.Overlay>,
19+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20+
>(({ className, ...props }, ref) => (
21+
<DialogPrimitive.Overlay
22+
ref={ref}
23+
className={cn(
24+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25+
className,
26+
)}
27+
{...props}
28+
/>
29+
))
30+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31+
32+
const DialogContent = React.forwardRef<
33+
React.ElementRef<typeof DialogPrimitive.Content>,
34+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35+
>(({ className, children, ...props }, ref) => (
36+
<DialogPortal>
37+
<DialogOverlay />
38+
<DialogPrimitive.Content
39+
ref={ref}
40+
className={cn(
41+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42+
className,
43+
)}
44+
{...props}>
45+
{children}
46+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
47+
<Cross2Icon className="h-4 w-4" />
48+
<span className="sr-only">Close</span>
49+
</DialogPrimitive.Close>
50+
</DialogPrimitive.Content>
51+
</DialogPortal>
52+
))
53+
DialogContent.displayName = DialogPrimitive.Content.displayName
54+
55+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
56+
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
57+
)
58+
DialogHeader.displayName = "DialogHeader"
59+
60+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
61+
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
62+
)
63+
DialogFooter.displayName = "DialogFooter"
64+
65+
const DialogTitle = React.forwardRef<
66+
React.ElementRef<typeof DialogPrimitive.Title>,
67+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
68+
>(({ className, ...props }, ref) => (
69+
<DialogPrimitive.Title
70+
ref={ref}
71+
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
72+
{...props}
73+
/>
74+
))
75+
DialogTitle.displayName = DialogPrimitive.Title.displayName
76+
77+
const DialogDescription = React.forwardRef<
78+
React.ElementRef<typeof DialogPrimitive.Description>,
79+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
80+
>(({ className, ...props }, ref) => (
81+
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
82+
))
83+
DialogDescription.displayName = DialogPrimitive.Description.displayName
84+
85+
export {
86+
Dialog,
87+
DialogPortal,
88+
DialogOverlay,
89+
DialogTrigger,
90+
DialogClose,
91+
DialogContent,
92+
DialogHeader,
93+
DialogFooter,
94+
DialogTitle,
95+
DialogDescription,
96+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export * from "./button"
2+
export * from "./command"
3+
export * from "./dialog"
24
export * from "./dropdown-menu"
5+
export * from "./popover"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from "react"
2+
import * as PopoverPrimitive from "@radix-ui/react-popover"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const Popover = PopoverPrimitive.Root
7+
8+
const PopoverTrigger = PopoverPrimitive.Trigger
9+
10+
const PopoverAnchor = PopoverPrimitive.Anchor
11+
12+
const PopoverContent = React.forwardRef<
13+
React.ElementRef<typeof PopoverPrimitive.Content>,
14+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16+
<PopoverPrimitive.Portal>
17+
<PopoverPrimitive.Content
18+
ref={ref}
19+
align={align}
20+
sideOffset={sideOffset}
21+
className={cn(
22+
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23+
className,
24+
)}
25+
{...props}
26+
/>
27+
</PopoverPrimitive.Portal>
28+
))
29+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
30+
31+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

webview-ui/src/index.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,11 @@ vscode-dropdown::part(listbox) {
289289
.vscrui-checkbox__listbox > ul {
290290
max-height: unset !important;
291291
}
292+
293+
/**
294+
* @shadcn/ui Overrides / Hacks
295+
*/
296+
297+
input[cmdk-input]:focus {
298+
outline: none;
299+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { useState } from "react"
2+
import type { Meta, StoryObj } from "@storybook/react"
3+
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
4+
5+
import { cn } from "@/lib/utils"
6+
import {
7+
Button,
8+
Command,
9+
CommandEmpty,
10+
CommandGroup,
11+
CommandInput,
12+
CommandItem,
13+
CommandList,
14+
Popover,
15+
PopoverContent,
16+
PopoverTrigger,
17+
} from "@/components/ui"
18+
19+
const meta = {
20+
title: "@shadcn/Combobox",
21+
component: Combobox,
22+
parameters: { layout: "centered" },
23+
tags: ["autodocs"],
24+
} satisfies Meta<typeof Combobox>
25+
26+
export default meta
27+
28+
type Story = StoryObj<typeof meta>
29+
30+
export const Default: Story = {
31+
name: "Combobox",
32+
render: () => <Combobox />,
33+
}
34+
35+
const frameworks = [
36+
{
37+
value: "next.js",
38+
label: "Next.js",
39+
},
40+
{
41+
value: "sveltekit",
42+
label: "SvelteKit",
43+
},
44+
{
45+
value: "nuxt.js",
46+
label: "Nuxt.js",
47+
},
48+
{
49+
value: "remix",
50+
label: "Remix",
51+
},
52+
{
53+
value: "astro",
54+
label: "Astro",
55+
},
56+
]
57+
58+
function Combobox() {
59+
const [open, setOpen] = useState(false)
60+
const [value, setValue] = useState("")
61+
62+
return (
63+
<Popover open={open} onOpenChange={setOpen}>
64+
<PopoverTrigger asChild>
65+
<Button variant="secondary" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
66+
{value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."}
67+
<CaretSortIcon className="opacity-50" />
68+
</Button>
69+
</PopoverTrigger>
70+
<PopoverContent className="w-[200px] p-0">
71+
<Command>
72+
<CommandInput placeholder="Search framework..." className="h-9" />
73+
<CommandList>
74+
<CommandEmpty>No framework found.</CommandEmpty>
75+
<CommandGroup>
76+
{frameworks.map((framework) => (
77+
<CommandItem
78+
key={framework.value}
79+
value={framework.value}
80+
onSelect={(currentValue) => {
81+
setValue(currentValue === value ? "" : currentValue)
82+
setOpen(false)
83+
}}>
84+
{framework.label}
85+
<CheckIcon
86+
className={cn(
87+
"ml-auto",
88+
value === framework.value ? "opacity-100" : "opacity-0",
89+
)}
90+
/>
91+
</CommandItem>
92+
))}
93+
</CommandGroup>
94+
</CommandList>
95+
</Command>
96+
</PopoverContent>
97+
</Popover>
98+
)
99+
}

0 commit comments

Comments
 (0)