Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
33c23f3
feat(ui): implement reusable SearchBar component
nancymuyeh Sep 17, 2025
dadff60
feat(ui): implement reusable SearchBar component
nancymuyeh Sep 17, 2025
02fdf91
feat(ui): implement reusable SearchBar component
nancymuyeh Sep 18, 2025
07f3c4a
feat(ui): implement reusable SearchBar component
nancymuyeh Sep 18, 2025
581d387
Merge branch 'main' into 31-design-and-implement-shared-reusable-sear…
nancymuyeh Sep 18, 2025
62a576d
fix(ui): mark SearchBar component prop as read-only
nancymuyeh Sep 18, 2025
0f3de59
Merge branch '31-design-and-implement-shared-reusable-search-bar-comp…
nancymuyeh Sep 18, 2025
d886154
fix(ui): increase test coverage
nancymuyeh Sep 18, 2025
b198f93
Merge branch 'main' into 31-design-and-implement-shared-reusable-sear…
nancymuyeh Sep 18, 2025
190f0b3
fix(ui): increase test coverage
nancymuyeh Sep 18, 2025
a24a3d9
fix(ui): refactor useSearchBar hook with extracted helper functions f…
nancymuyeh Sep 18, 2025
a74f693
fix(ui): refactor useSearchBar hook with extracted helper functions f…
nancymuyeh Sep 18, 2025
390b514
Update packages/ui/src/components/SearchBar/SearchBar.view.tsx
nancymuyeh Sep 18, 2025
7f2e481
fix(ui): refactor useSearchBar component for SonarQube compliance
nancymuyeh Sep 19, 2025
5f5396c
fix(ui): refactor useSearchBar component for SonarQube compliance
nancymuyeh Sep 19, 2025
216d993
fix(ui): refactor SearchBar component for SonarQube compliance
nancymuyeh Sep 19, 2025
e04b8bd
fix(ui): refactor SearchBar component for SonarQube compliance
nancymuyeh Sep 19, 2025
3328a72
chore(ui): merge main
nancymuyeh Sep 19, 2025
89b66c6
fix(ui): refactor SearchBar component for SonarQube compliance
nancymuyeh Sep 19, 2025
1161ec1
fix(ui): refactor SearchBar component with more simplifies functionality
nancymuyeh Sep 19, 2025
cd8acd7
fix(ui): update searchbar variants and styling
nancymuyeh Sep 19, 2025
1e6e858
fix(ui): remove unused dependencies
nancymuyeh Sep 19, 2025
103e6c8
fix(ui): remove unused dependencies
nancymuyeh Sep 22, 2025
0cd4017
fix(ui): update searchbar componenet to use global styling from style…
nancymuyeh Sep 22, 2025
8ba9f4b
Merge branch 'main' into 31-design-and-implement-shared-reusable-sear…
nancymuyeh Sep 22, 2025
e885bf6
fix(ui): fix minor lint issue
nancymuyeh Sep 22, 2025
9d2d22f
fix(ui): merge main
nancymuyeh Sep 22, 2025
632273e
Merge remote-tracking branch 'origin' into 31-design-and-implement-sh…
nancymuyeh Sep 22, 2025
eafd972
Merge branch 'main' into 31-design-and-implement-shared-reusable-sear…
nancymuyeh Sep 22, 2025
0fffcc2
Merge branch '31-design-and-implement-shared-reusable-search-bar-comp…
nancymuyeh Sep 23, 2025
9999bf2
fix(ui): fix minor lint issue
nancymuyeh Sep 23, 2025
f87027b
fix(ui): fix minor lint issue
nancymuyeh Sep 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions docs/shared-components/searchbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Search Bar Component

A versatile search component that provides a consistent search experience across all applications in the Fineract ecosystem. It supports various features like suggestions, async search, keyboard navigation, and multiple visual variants.

## Features

- **Live Search**: Real-time search with debouncing
- **Suggestions**: Both client-side and server-side suggestions support
- **Keyboard Navigation**: Full keyboard support for suggestion navigation
- **Accessibility**: ARIA compliant using Downshift
- **Loading States**: Visual feedback during async operations
- **Clear/Reset**: Optional clear button functionality
- **Multiple Variants**: Simple, with button, and expandable styles
- **Responsive Design**: Adapts to different screen sizes
- **Theme Integration**: Uses semantic color tokens

## Installation

The SearchBar component is part of the `@fineract-apps/ui` package and is automatically available in all frontend applications.

## Basic Usage

```tsx
import { SearchBar } from "@fineract-apps/ui";

// Simple search
<SearchBar
placeholder="Search accounts"
onSearch={(value) => console.log('Searching for:', value)}
/>

// With suggestions
<SearchBar
placeholder="Search accounts"
suggestions={[
{ id: '1', label: 'Account #123' },
{ id: '2', label: 'Account #456' }
]}
onSuggestionSelect={(suggestion) => {
console.log('Selected:', suggestion);
}}
/>

// With async suggestions
<SearchBar
placeholder="Search accounts"
suggestionProvider={async (query, signal) => {
const res = await fetch(`/api/search?q=${query}`, { signal });
const data = await res.json();
return data.map(item => ({
id: item.id,
label: item.displayName
}));
}}
/>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| \`variant\` | \`"default" \| "simple" \| "withButton" \| "expandable"\` | \`"default"\` | The visual style variant |
| \`size\` | \`"sm" \| "md" \| "lg"\` | \`"md"\` | The size of the search bar |
| \`placeholder\` | \`string\` | \`"Search"\` | Placeholder text |
| \`onSearch\` | \`(value: string) => void\` | - | Called when search is triggered |
| \`showClear\` | \`boolean\` | \`true\` | Whether to show the clear button |
| \`onClear\` | \`() => void\` | - | Called when clear button is clicked |
| \`suggestions\` | \`Suggestion[]\` | - | Array of suggestions for client-side filtering |
| \`suggestionProvider\` | \`(query: string, signal?: AbortSignal) => Promise<Suggestion[]>\` | - | Async function to fetch suggestions |
| \`minChars\` | \`number\` | \`2\` | Minimum characters before showing suggestions |
| \`debounceMs\` | \`number\` | \`250\` | Debounce delay for suggestions in ms |
| \`maxSuggestions\` | \`number\` | \`8\` | Maximum number of suggestions to show |
| \`onSuggestionSelect\` | \`(suggestion: Suggestion) => void\` | - | Called when a suggestion is selected |
| \`isLoading\` | \`boolean\` | \`false\` | Shows loading indicator |
| \`showSearchButton\` | \`boolean\` | \`false\` | Shows search button (for withButton variant) |

## Variants

### Default
Standard search bar with icon and optional clear button.
```tsx
<SearchBar placeholder="Search" />
```

### Simple
Compact version for space-constrained areas.
```tsx
<SearchBar
variant="simple"
size="sm"
placeholder="Quick search"
/>
```

### With Button
Includes a search button, useful for explicit search actions.
```tsx
<SearchBar
variant="withButton"
showSearchButton
placeholder="Search with button"
/>
```

### Expandable
Collapses to an icon and expands on focus, great for navigation bars.
```tsx
<SearchBar
variant="expandable"
size="sm"
placeholder="Click to expand"
/>
```

## Accessibility

The SearchBar component follows WAI-ARIA guidelines and includes:

- Proper ARIA roles and attributes (`combobox`, `listbox`, `option`)
- Keyboard navigation:
- `↑/↓`: Navigate through suggestions
- `Enter`: Select suggestion or trigger search
- `Esc`: Close suggestions
- Clear button has proper aria-label
- Loading state announcements
- Focus management

## Examples

### Basic Search
```tsx
<SearchBar
placeholder="Search accounts"
onSearch={(value) => {
console.log('Searching:', value);
}}
/>
```

### With Async Suggestions
```tsx
const [loading, setLoading] = useState(false);

<SearchBar
placeholder="Search accounts"
suggestionProvider={async (query, signal) => {
setLoading(true);
try {
const res = await fetch(`/api/search?q=${query}`, { signal });
const data = await res.json();
return data.map(item => ({
id: item.id,
label: item.name
}));
} finally {
setLoading(false);
}
}}
isLoading={loading}
onSuggestionSelect={(suggestion) => {
navigate(`/accounts/${suggestion.id}`);
}}
/>
```

### Expandable in Navigation
```tsx
<nav className="flex items-center justify-between">
<Logo />
<SearchBar
variant="expandable"
size="sm"
placeholder="Search"
className="max-w-xs"
/>
<UserMenu />
</nav>
```

## Technical Details

The SearchBar uses:
- Downshift for accessibility and keyboard interactions
- class-variance-authority for variant management
- AbortController for cancelling outdated requests
- Debouncing for performance

## Best Practices

1. **Error Handling**: Always handle errors in your suggestion provider:
```tsx
const handleSuggestions = async (query: string, signal?: AbortSignal) => {
try {
const data = await fetchSuggestions(query, signal);
return data.map(toSuggestion);
} catch (err) {
if (err.name === 'AbortError') return [];
console.error('Search error:', err);
return []; // Or show error state
}
};
```

2. **Performance**: Use debouncing and cancellation:
```tsx
<SearchBar
debounceMs={300} // Adjust based on your needs
minChars={2} // Prevent unnecessary searches
maxSuggestions={8} // Limit number of results
/>
```

3. **Responsive Design**: Use appropriate variants:
```tsx
<SearchBar
variant="default"
className="hidden md:flex" // Hide on mobile
/>
<SearchBar
variant="expandable"
className="flex md:hidden" // Show on mobile
/>
```
75 changes: 66 additions & 9 deletions frontend/account-manager-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,78 @@
import { Button, SearchBar } from "@fineract-apps/ui";
import { useState } from "react";
import "@fineract-apps/ui/styles.css";
import { Button } from "@fineract-apps/ui";

const mockSuggestions = [
{ id: "1", label: "John Doe - #123456" },
{ id: "2", label: "Jane Smith - #789012" },
{ id: "3", label: "Bob Wilson - #345678" },
];

function App() {
const [loading, setLoading] = useState(false);

const handleSearch = async (value: string) => {
console.log("Searching for:", value);
};

const mockSuggestionProvider = async (q: string) => {
setLoading(true);
try {
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 500));
return mockSuggestions.filter((s) =>
s.label.toLowerCase().includes(q.toLowerCase()),
);
} finally {
setLoading(false);
}
};

return (
<div style={{ padding: "20px" }}>
<h1>Account Manager App</h1>
<p>
This button is a shared component from the '@fineract-apps/ui' package.
</p>
<Button>Click me!</Button>
<Button>Click me!</Button>

<Button>Click me!</Button>
<h2 className="mt-8 mb-4 text-lg font-semibold">Default SearchBar</h2>
<div style={{ maxWidth: 420 }}>
<SearchBar
placeholder="Search accounts"
onSearch={handleSearch}
suggestionProvider={mockSuggestionProvider}
isLoading={loading}
/>
</div>

<Button>Click me!</Button>
<h2 className="mt-8 mb-4 text-lg font-semibold">Simple Variant</h2>
<div style={{ maxWidth: 320 }}>
<SearchBar
variant="simple"
size="sm"
placeholder="Quick search"
onSearch={handleSearch}
/>
</div>

<h2 className="mt-8 mb-4 text-lg font-semibold">With Button Variant</h2>
<div style={{ maxWidth: 420 }}>
<SearchBar
variant="withButton"
placeholder="Search with button"
onSearch={handleSearch}
showSearchButton
/>
</div>

<Button className="bg-red-500">Click me!</Button>
<h2 className="mt-8 mb-4 text-lg font-semibold">Expandable Variant</h2>
<div className="flex justify-end" style={{ maxWidth: 420 }}>
<SearchBar
variant="expandable"
size="sm"
placeholder="Click to expand"
onSearch={handleSearch}
/>
</div>

<Button>Click me!</Button>
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@tailwindcss/vite": "^4.1.13",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^29.5.12",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"downshift": "^9.0.10",
"lucide-react": "^0.544.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
Expand Down
Loading
Loading