Skip to content

Commit 270d547

Browse files
enkoclaude
andcommitted
feat(frontend): Open first result on Enter in list search boxes
On the friends, encounters, and collectives list pages, pressing Enter while focused in the search/filter box now navigates to the first item in the currently filtered list. The handler lives on each input directly because the global shortcut layer suppresses keys while an input is focused. Also tag the encounters search input with data-search-input so the `/` shortcut focuses it like the other lists. Collectives opens the first client-side sorted item to match what is rendered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d9cce1d commit 270d547

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

apps/frontend/src/lib/components/collectives/collective-list.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
88
import Plus from 'svelte-heros-v2/Plus.svelte';
99
import Users from 'svelte-heros-v2/Users.svelte';
1010
import XMark from 'svelte-heros-v2/XMark.svelte';
11+
import { goto } from '$app/navigation';
1112
import type { CollectiveListParams } from '$lib/api/collectives';
1213
import { createI18n } from '$lib/i18n/index.js';
1314
import { collectives, collectivesList, collectiveTypes } from '$lib/stores/collectives';
@@ -143,6 +144,13 @@ let sortedItems = $derived.by(() => {
143144
});
144145
return items;
145146
});
147+
148+
// Open the first result when pressing Enter in the search box
149+
function openFirstResult() {
150+
const first = sortedItems[0];
151+
if (!first) return;
152+
goto(`/collectives/${first.id}`);
153+
}
146154
</script>
147155

148156
<div class="space-y-4">
@@ -154,6 +162,7 @@ let sortedItems = $derived.by(() => {
154162
type="text"
155163
value={searchQuery}
156164
oninput={(e) => handleSearchInput(e.currentTarget.value)}
165+
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
157166
placeholder={$i18n.t('collectives.searchPlaceholder')}
158167
class="w-full pl-12 pr-12 py-3 text-base font-body text-gray-900 placeholder-gray-400 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-forest focus:border-transparent"
159168
autocomplete="off"

apps/frontend/src/lib/components/encounters/encounter-list.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import ChevronLeft from 'svelte-heros-v2/ChevronLeft.svelte';
55
import ChevronRight from 'svelte-heros-v2/ChevronRight.svelte';
66
import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
77
import Plus from 'svelte-heros-v2/Plus.svelte';
8+
import { goto } from '$app/navigation';
89
import type { EncounterListParams } from '$lib/api/encounters';
910
import { createI18n } from '$lib/i18n/index.js';
1011
import { encounters, encountersList } from '$lib/stores/encounters';
1112
import { visibleEncounterIds } from '$lib/stores/ui';
1213
import { ENCOUNTER_TYPES, type EncounterType } from '$shared';
13-
import { encounterTypeLabel } from './encounter-display';
1414
import EncounterCard from './encounter-card.svelte';
15+
import { encounterTypeLabel } from './encounter-display';
1516
1617
const i18n = createI18n();
1718
@@ -42,6 +43,13 @@ $effect(() => {
4243
visibleEncounterIds.set(ids);
4344
});
4445
46+
// Open the first result when pressing Enter in the search box
47+
function openFirstResult() {
48+
const first = encounterItems[0];
49+
if (!first) return;
50+
goto(`/encounters/${first.id}`);
51+
}
52+
4553
// Load encounters on mount (not in $effect to avoid infinite loop)
4654
onMount(() => {
4755
loadEncounters();
@@ -116,8 +124,10 @@ function goToPage(page: number) {
116124
type="text"
117125
value={searchQuery}
118126
oninput={(e) => handleSearchInput(e.currentTarget.value)}
127+
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
119128
placeholder={$i18n.t('encounters.searchPlaceholder')}
120129
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-forest focus:border-transparent font-body text-sm"
130+
data-search-input
121131
/>
122132
</div>
123133
</div>

apps/frontend/src/lib/components/friends/friend-list.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import MagnifyingGlass from 'svelte-heros-v2/MagnifyingGlass.svelte';
99
import Plus from 'svelte-heros-v2/Plus.svelte';
1010
import Users from 'svelte-heros-v2/Users.svelte';
1111
import XMark from 'svelte-heros-v2/XMark.svelte';
12+
import { goto } from '$app/navigation';
1213
import { page } from '$app/stores';
1314
import * as friendsApi from '$lib/api/friends';
1415
import { createI18n } from '$lib/i18n/index.js';
@@ -390,6 +391,16 @@ let gridItems = $derived.by<FriendGridItem[]>(() => {
390391
return $friendList.map(toFriendGridItem);
391392
}
392393
});
394+
395+
// Open the first result when pressing Enter in the search box
396+
function openFirstResult() {
397+
const first = gridItems[0];
398+
if (!first) return;
399+
const url = returnUrl
400+
? `/friends/${first.id}?from=${encodeURIComponent(returnUrl)}`
401+
: `/friends/${first.id}`;
402+
goto(url);
403+
}
393404
</script>
394405

395406
<div class="space-y-4">
@@ -401,6 +412,7 @@ let gridItems = $derived.by<FriendGridItem[]>(() => {
401412
type="text"
402413
value={searchQuery}
403414
oninput={(e) => handleSearchInput(e.currentTarget.value)}
415+
onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openFirstResult(); } }}
404416
placeholder={$i18n.t('friendList.searchPlaceholder')}
405417
class="w-full pl-12 pr-12 py-3 text-base font-body text-gray-900 placeholder-gray-400 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-forest focus:border-transparent"
406418
autocomplete="off"

0 commit comments

Comments
 (0)