Files
frontend-svelte/src/shared/ui/SearchBar/SearchBar.svelte

99 lines
3.2 KiB
Svelte
Raw Normal View History

<!--
Component: SearchBar
Search input with popover dropdown for results/suggestions
- Features keyboard navigation (ArrowDown/Up/Enter) and auto-focus prevention on popover open.
- The input field serves as the popover trigger.
-->
<script lang="ts">
import { Input } from '$shared/shadcn/ui/input';
import { Label } from '$shared/shadcn/ui/label';
import {
Content as PopoverContent,
Root as PopoverRoot,
Trigger as PopoverTrigger,
} from '$shared/shadcn/ui/popover';
import { useId } from 'bits-ui';
import type { Snippet } from 'svelte';
interface Props {
/** Unique identifier for the input element */
id?: string;
/** Current search value (bindable) */
value: string;
/** Additional CSS classes for the container */
class?: string;
/** Placeholder text for the input */
placeholder?: string;
/** Optional label displayed above the input */
label?: string;
/** Content to render inside the popover (receives unique content ID) */
children: Snippet<[{ id: string }]> | undefined;
}
let {
id = 'search-bar',
value = $bindable(),
class: className,
placeholder,
label,
children,
}: Props = $props();
let open = $state(false);
2026-01-13 20:09:30 +03:00
let triggerRef = $state<HTMLInputElement>(null!);
2026-01-14 15:27:41 +03:00
// svelte-ignore state_referenced_locally
const contentId = useId(id);
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
event.preventDefault();
}
}
function handleInputClick() {
open = true;
}
</script>
<PopoverRoot bind:open>
<PopoverTrigger bind:ref={triggerRef}>
{#snippet child({ props })}
{@const { onclick, ...rest } = props}
<div {...rest} class="flex flex-row flex-1 w-full">
{#if label}
<Label for={id}>{label}</Label>
{/if}
<Input
id={id}
placeholder={placeholder}
bind:value={value}
onkeydown={handleKeyDown}
onclick={handleInputClick}
class="
h-20 w-full md:text-2xl backdrop-blur-sm bg-white/60 dark:bg-slate-900/40
2026-01-24 15:38:01 +03:00
ring-2 ring-slate-200/50
active:ring-indigo-500/50
focus-visible:border-indigo-500/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50
hover:bg-white/70 dark:hover:bg-slate-900/50 text-slate-900 dark:text-slate-100
placeholder:text-slate-400 px-6 py-4 rounded-2xl transition-all duration-300
font-medium
"
/>
</div>
{/snippet}
</PopoverTrigger>
<PopoverContent
onOpenAutoFocus={e => e.preventDefault()}
onInteractOutside={(e => {
if (e.target === triggerRef) {
e.preventDefault();
}
})}
class="w-(--bits-popover-anchor-width) min-w-(--bits-popover-anchor-width) md:rounded-2xl"
>
{@render children?.({ id: contentId })}
</PopoverContent>
</PopoverRoot>