Skip to content

Commit 1b09245

Browse files
feat: search v1 (#229)
* feat: search v1 * chore: remove comment for consistency * docs: add docs * docs: add routes * fix: css * docs: update states * feat: add aria properties * docs: update docs * fix: missing import * chore: replace elementRef with ComponentRef * revert * chore: simplify css * chore: upgrade to react 19 * feat: add cva for size variant
1 parent 99600c8 commit 1b09245

File tree

7 files changed

+397
-6
lines changed

7 files changed

+397
-6
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: Search
3+
description: A search input component with built-in search icon and optional clear button.
4+
---
5+
6+
## Preview
7+
8+
<Preview>
9+
<Flex style={{ gap: '24px', flexWrap: 'wrap' }}>
10+
<Search
11+
placeholder="Default search..."
12+
/>
13+
14+
<Search
15+
placeholder="With clear button..."
16+
showClearButton
17+
value="Searchable text"
18+
/>
19+
20+
<Search
21+
placeholder="Small size..."
22+
size="small"
23+
/>
24+
25+
<Search
26+
disabled
27+
placeholder="Disabled state"
28+
/>
29+
30+
</Flex>
31+
</Preview>
32+
33+
## Usage
34+
35+
The Search component provides a consistent search input experience with a built-in search icon and optional clear button functionality.
36+
37+
## Installation
38+
39+
Install the component from your command line.
40+
41+
<LiveProvider>
42+
<LiveEditor code={`npm install @raystack/apsara`} border language="shell" />
43+
</LiveProvider>
44+
45+
<LiveProvider>
46+
<LiveEditor code={`import { Search } from '@raystack/apsara/v1'
47+
48+
<Search
49+
placeholder="Search items..."
50+
showClearButton
51+
value={searchValue}
52+
onChange={(e) => setSearchValue(e.target.value)}
53+
onClear={() => setSearchValue("")}
54+
/>`} border/>
55+
</LiveProvider>
56+
57+
## Search Props
58+
59+
The `Search` component accepts the following props:
60+
61+
- `size`: Size variant of the search input (`"small"` | `"large"`, default: "large")
62+
- `placeholder`: Placeholder text for the input
63+
- `disabled`: Whether the search input is disabled
64+
- `showClearButton`: Shows a clear button when the input has a value
65+
- `value`: The controlled value of the input
66+
- `onChange`: Callback when input value changes
67+
- `onClear`: Callback when clear button is clicked
68+
- `className`: Additional CSS class names
69+
70+
## Size
71+
72+
The Search component comes in two sizes to fit different design contexts.
73+
74+
<Playground
75+
scope={{ Search }}
76+
tabs={[
77+
{
78+
name: "Large",
79+
code: `<Search placeholder="Large size search..." />`,
80+
},
81+
{
82+
name: "Small",
83+
code: `<Search size="small" placeholder="Small size search..." />`,
84+
},
85+
]}
86+
/>
87+
88+
## States
89+
90+
The Search component supports various states to provide visual feedback.
91+
92+
<Playground
93+
scope={{ Search }}
94+
tabs={[
95+
{
96+
name: "Default",
97+
code: `<Search placeholder="Default state..." />`,
98+
},
99+
{
100+
name: "Hover",
101+
code: `<Search placeholder="Hover state..." />`,
102+
},
103+
{
104+
name: "Focus",
105+
code: `<Search placeholder="Focus state..." />`,
106+
},
107+
{
108+
name: "Disabled",
109+
code: `<Search disabled placeholder="Disabled state..." />`,
110+
},
111+
]}
112+
/>
113+
114+
## Clear Button
115+
116+
The Search component can include a clear button that appears when there is input value.
117+
118+
<Playground
119+
scope={{ Search }}
120+
tabs={[
121+
{
122+
name: "With Clear Button",
123+
code: `<Search placeholder="Type to search..." value="Searchable text" showClearButton />`,
124+
},
125+
{
126+
name: "Without Clear Button",
127+
code: `<Search placeholder="Basic search..." />`,
128+
},
129+
]}
130+
/>
131+
132+
## Accessibility
133+
134+
The Search component is built with accessibility in mind, following ARIA best practices:
135+
136+
- Container has `role="search"` to identify it as a search landmark
137+
- Input has `type="search"` for semantic HTML
138+
- Search icon is marked as decorative with `aria-hidden="true"`
139+
- Clear button has appropriate `aria-label` for screen readers
140+
- Keyboard navigation support for the clear button
141+
- Input inherits `aria-label` from placeholder text
142+
143+
Example with accessibility features:
144+
145+
<Playground
146+
scope={{ Search }}
147+
tabs={[
148+
{
149+
name: "Default",
150+
code: `
151+
<Search
152+
placeholder="Search items..."
153+
showClearButton
154+
value="Searchable text"
155+
aria-label="Search items"
156+
/>
157+
`,
158+
},
159+
]}
160+
/>
161+
162+
The component supports keyboard navigation:
163+
164+
- Tab to focus on the search input
165+
- Tab again to focus on the clear button (when visible)
166+
- Enter or Space to trigger the clear button

apps/www/examples/shield-ts/assets.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import React, { useState, useCallback, useEffect } from "react";
22
import dayjs from "dayjs";
3-
import { HomeIcon, Cross1Icon, PlusIcon, CheckIcon, CaretLeftIcon, InfoCircledIcon } from "@radix-ui/react-icons";
3+
import { HomeIcon } from "@radix-ui/react-icons";
44
import {
55
DataTable,
66
Title,
77
useTable
88
} from "@raystack/apsara";
99

10-
11-
import { toast, ToastContainer, Avatar, AvatarGroup, Button, Spinner, DropdownMenu, Breadcrumb, Chip, Flex, Text, Checkbox, InputField, Badge, Radio, Tabs } from "@raystack/apsara/v1";
10+
import { toast, ToastContainer, Avatar, AvatarGroup, Button, Spinner, DropdownMenu, Breadcrumb, Chip, Flex, Text, Checkbox, InputField, Badge, Radio, Search } from "@raystack/apsara/v1";
1211

1312
import { getData, Payment } from "./data";
1413
import { ApsaraColumnDef } from "@raystack/apsara/table/datatables.types";
@@ -228,6 +227,7 @@ export const Assets = () => {
228227
const AssetsHeader = () => {
229228
const { filteredColumns } = useTable();
230229
const [checked, setChecked] = useState<boolean | 'indeterminate'>('indeterminate');
230+
const [searchValue, setSearchValue] = useState("");
231231
const handleCheckedChange = (newChecked: boolean | 'indeterminate') => {
232232
if (newChecked !== 'indeterminate') {
233233
setChecked(newChecked);
@@ -256,6 +256,13 @@ const AssetsHeader = () => {
256256
style={{ width: "100%", padding: "4px", paddingTop: "48px" }}
257257
>
258258
<Flex gap="extra-large" align="center" style={{ width: "100%" }}>
259+
<Search
260+
placeholder="Search assets..."
261+
value={searchValue}
262+
onChange={(e) => setSearchValue(e.target.value)}
263+
showClearButton
264+
onClear={() => setSearchValue("")}
265+
/>
259266
{/* <Tabs.Root defaultValue="general">
260267
<Tabs.List>
261268
<Tabs.Trigger value="general" icon={<HomeIcon />}>
@@ -351,6 +358,7 @@ const AssetsHeader = () => {
351358

352359
{/* Add Chip examples */}
353360
<Flex gap="small" align="center">
361+
354362
{/* <Chip isDismissible variant="filled" size="small" style="accent" leadingIcon={<HomeIcon />} trailingIcon={<CheckIcon />}>Default</Chip> */}
355363
{/* <Radio.Root defaultValue="1" aria-label="View options">
356364
<Flex gap="small" align="center" style={{ minWidth: '200px' }}>
@@ -411,12 +419,12 @@ const AssetsHeader = () => {
411419
</Flex>*/}
412420

413421
</Flex>
414-
{/* <Flex gap="small">
422+
<Flex gap="small">
415423
<AssetsFooter />
416424
{isFiltered ? <DataTable.ClearFilter /> : <DataTable.FilterOptions />}
417425
<DataTable.ViewOptions />
418-
<DataTable.GloabalSearch placeholder="Search assets..." />
419-
</Flex> */}
426+
427+
</Flex>
420428
</Flex>
421429
);
422430
};

apps/www/utils/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const primitivesRoutes = [
8383
{ title: "Popover", slug: "docs/primitives/components/popover" },
8484
{ title: "Radio", slug: "docs/primitives/components/radio", newBadge: true },
8585
{ title: "Select", slug: "docs/primitives/components/select" },
86+
{ title: "Search", slug: "docs/primitives/components/search", newBadge: true },
8687
{ title: "Separator", slug: "docs/primitives/components/separator" },
8788
{ title: "Sheet", slug: "docs/primitives/components/sheet" },
8889
{
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Search } from "./search";
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
width: 100%;
5+
}
6+
7+
.inputWrapper {
8+
position: relative;
9+
display: flex;
10+
align-items: center;
11+
gap: var(--rs-space-3);
12+
align-self: stretch;
13+
border-radius: var(--rs-radius-2);
14+
border: 1px solid var(--rs-color-border-base-tertiary);
15+
background: var(--rs-color-background-base-primary);
16+
}
17+
18+
.inputWrapper:hover {
19+
border: 1px solid var(--rs-color-border-base-tertiary-hover);
20+
background: var(--rs-color-background-base-primary-hover);
21+
}
22+
23+
.inputWrapper:focus-within {
24+
border: 1px solid var(--rs-color-border-accent-emphasis);
25+
background: var(--rs-color-background-base-primary);
26+
}
27+
28+
.inputWrapper:focus-within:hover {
29+
border: 1px solid var(--rs-color-border-accent-emphasis);
30+
background: var(--rs-color-background-base-primary);
31+
}
32+
33+
.inputWrapper.disabled,
34+
.inputWrapper.disabled:hover {
35+
opacity: 0.5;
36+
cursor: not-allowed;
37+
background: var(--rs-color-background-base-primary);
38+
border: 1px solid var(--rs-color-border-base-tertiary);
39+
}
40+
41+
.searchField {
42+
width: 100%;
43+
border-radius: var(--rs-radius-2);
44+
border: none;
45+
background: transparent;
46+
font-family: inherit;
47+
outline: none;
48+
transition: all 0.2s ease;
49+
box-sizing: border-box;
50+
color: var(--rs-color-text-base-primary);
51+
padding-left: var(--rs-space-8);
52+
padding-right: var(--rs-space-3);
53+
}
54+
55+
.searchField::placeholder {
56+
color: var(--rs-color-text-base-tertiary);
57+
font-size: 12px;
58+
font-style: normal;
59+
font-weight: 400;
60+
line-height: 16px;
61+
letter-spacing: 0.4px;
62+
}
63+
64+
.searchField:hover {
65+
background: transparent;
66+
}
67+
68+
.searchField:focus {
69+
border-color: var(--rs-color-border-accent-emphasis);
70+
background: var(--rs-color-background-base-primary);
71+
}
72+
73+
.search-small {
74+
height: 24px;
75+
font-size: 12px;
76+
}
77+
78+
.search-large {
79+
height: 32px;
80+
font-size: 12px;
81+
}
82+
83+
.search-disabled {
84+
opacity: 0.5;
85+
cursor: not-allowed;
86+
background: var(--rs-color-background-base-secondary);
87+
}
88+
89+
.search-disabled:hover {
90+
border-color: var(--rs-color-border-base-primary);
91+
background: var(--rs-color-background-base-secondary);
92+
}
93+
94+
.leadingIcon {
95+
position: absolute;
96+
left: var(--rs-space-3);
97+
top: 50%;
98+
transform: translateY(-50%);
99+
display: flex;
100+
align-items: center;
101+
color: var(--rs-color-text-base-secondary);
102+
pointer-events: none;
103+
}
104+
105+
.leadingIcon svg {
106+
width: 16px;
107+
height: 16px;
108+
}
109+
110+
.clearButton {
111+
position: absolute;
112+
right: var(--rs-space-3);
113+
top: 50%;
114+
transform: translateY(-50%);
115+
display: flex;
116+
align-items: center;
117+
justify-content: center;
118+
color: var(--rs-color-text-base-secondary);
119+
background: none;
120+
border: none;
121+
padding: var(--rs-space-1);
122+
cursor: pointer;
123+
}
124+
125+
.clearButton svg {
126+
width: 16px;
127+
height: 16px;
128+
}
129+
130+
.clearButton:disabled {
131+
cursor: not-allowed;
132+
opacity: 0.5;
133+
}

0 commit comments

Comments
 (0)