Skip to content

Commit adf53ea

Browse files
committed
style: use InputGroup with search icon for text filters
1 parent 9031904 commit adf53ea

File tree

2 files changed

+182
-7
lines changed

2 files changed

+182
-7
lines changed

src/components/data-table/data-table-toolbar.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import type { Column, Table } from "@tanstack/react-table";
4-
import { X } from "lucide-react";
4+
import { Search, X } from "lucide-react";
55
import * as React from "react";
66

77
import { DataTableDateFilter } from "@/components/data-table/data-table-date-filter";
@@ -10,6 +10,7 @@ import { DataTableSliderFilter } from "@/components/data-table/data-table-slider
1010
import { DataTableViewOptions } from "@/components/data-table/data-table-view-options";
1111
import { Button } from "@/components/ui/button";
1212
import { Input } from "@/components/ui/input";
13+
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
1314
import { cn } from "@/lib/utils";
1415

1516
interface DataTableToolbarProps<TData> extends React.ComponentProps<"div"> {
@@ -83,12 +84,16 @@ function DataTableToolbarFilter<TData>({
8384
switch (columnMeta.variant) {
8485
case "text":
8586
return (
86-
<Input
87-
placeholder={columnMeta.placeholder ?? columnMeta.label}
88-
value={(column.getFilterValue() as string) ?? ""}
89-
onChange={(event) => column.setFilterValue(event.target.value)}
90-
className="h-8 w-40 lg:w-56"
91-
/>
87+
<InputGroup className="h-8 w-40 lg:w-56">
88+
<InputGroupInput
89+
placeholder={columnMeta.placeholder ?? columnMeta.label}
90+
value={(column.getFilterValue() as string) ?? ""}
91+
onChange={(event) => column.setFilterValue(event.target.value)}
92+
/>
93+
<InputGroupAddon>
94+
<Search />
95+
</InputGroupAddon>
96+
</InputGroup>
9297
);
9398

9499
case "number":

src/components/ui/input-group.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { cva, type VariantProps } from "class-variance-authority";
5+
6+
import { cn } from "@/lib/utils";
7+
import { Button } from "@/components/ui/button";
8+
import { Input } from "@/components/ui/input";
9+
import { Textarea } from "@/components/ui/textarea";
10+
11+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12+
return (
13+
<div
14+
data-slot="input-group"
15+
role="group"
16+
className={cn(
17+
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
18+
"h-9 has-[>textarea]:h-auto",
19+
20+
// Variants based on alignment.
21+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
22+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
23+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
24+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
25+
26+
// Focus state.
27+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
28+
29+
// Error state.
30+
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
31+
32+
className
33+
)}
34+
{...props}
35+
/>
36+
);
37+
}
38+
39+
const inputGroupAddonVariants = cva(
40+
"text-muted-foreground flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
41+
{
42+
variants: {
43+
align: {
44+
"inline-start":
45+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
46+
"inline-end":
47+
"order-last pr-3 has-[>button]:mr-[-0.4rem] has-[>kbd]:mr-[-0.35rem]",
48+
"block-start":
49+
"[.border-b]:pb-3 order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5",
50+
"block-end":
51+
"[.border-t]:pt-3 order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5",
52+
},
53+
},
54+
defaultVariants: {
55+
align: "inline-start",
56+
},
57+
}
58+
);
59+
60+
function InputGroupAddon({
61+
className,
62+
align = "inline-start",
63+
...props
64+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
65+
return (
66+
<div
67+
role="group"
68+
data-slot="input-group-addon"
69+
data-align={align}
70+
className={cn(inputGroupAddonVariants({ align }), className)}
71+
onClick={(e) => {
72+
if ((e.target as HTMLElement).closest("button")) {
73+
return;
74+
}
75+
e.currentTarget.parentElement?.querySelector("input")?.focus();
76+
}}
77+
{...props}
78+
/>
79+
);
80+
}
81+
82+
const inputGroupButtonVariants = cva(
83+
"flex items-center gap-2 text-sm shadow-none",
84+
{
85+
variants: {
86+
size: {
87+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
88+
sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
89+
"icon-xs":
90+
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
91+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
92+
},
93+
},
94+
defaultVariants: {
95+
size: "xs",
96+
},
97+
}
98+
);
99+
100+
function InputGroupButton({
101+
className,
102+
type = "button",
103+
variant = "ghost",
104+
size = "xs",
105+
...props
106+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
107+
VariantProps<typeof inputGroupButtonVariants>) {
108+
return (
109+
<Button
110+
type={type}
111+
data-size={size}
112+
variant={variant}
113+
className={cn(inputGroupButtonVariants({ size }), className)}
114+
{...props}
115+
/>
116+
);
117+
}
118+
119+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
120+
return (
121+
<span
122+
className={cn(
123+
"text-muted-foreground flex items-center gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
124+
className
125+
)}
126+
{...props}
127+
/>
128+
);
129+
}
130+
131+
function InputGroupInput({
132+
className,
133+
...props
134+
}: React.ComponentProps<"input">) {
135+
return (
136+
<Input
137+
data-slot="input-group-control"
138+
className={cn(
139+
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
140+
className
141+
)}
142+
{...props}
143+
/>
144+
);
145+
}
146+
147+
function InputGroupTextarea({
148+
className,
149+
...props
150+
}: React.ComponentProps<"textarea">) {
151+
return (
152+
<Textarea
153+
data-slot="input-group-control"
154+
className={cn(
155+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
156+
className
157+
)}
158+
{...props}
159+
/>
160+
);
161+
}
162+
163+
export {
164+
InputGroup,
165+
InputGroupAddon,
166+
InputGroupButton,
167+
InputGroupText,
168+
InputGroupInput,
169+
InputGroupTextarea,
170+
};

0 commit comments

Comments
 (0)