svelte: Add search query input to repo pages (#59185)

This PR adds the search query input to the repo page header. I used a
similar solution as GitHub, which renders a button that looks a bit like
an input and opens a dialog with the real input.
The input is prepopulated with a `repo:` filter for the current input.
It has a tab/focus trap and closes on escape.

Before settling on this solution I tried various other approaches:

- Directly render the input in the header and make it expand on focus.
  That caused strange text selection because mousdown would expand the
  input and mouseup would happen at a different place in the input,
  causing text selection.
- Using the `<dialog>` element. It has some default styles/behavior that
  interfered with what I wanted to do.
- I wanted to use the newly added `openFocus` option in `createDialog`
  to disable melt's autofocus behavior, but that didn't work.

Happy to iterate on the design of the button, or on the behavior in
general.

I decided against introducing keyboard shortcuts in this PR, I think
that warrants it's own PR.
This commit is contained in:
Felix Kling 2023-12-22 12:53:02 +01:00 committed by GitHub
parent 17cff22ecf
commit 7e2e51a079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 6 deletions

View File

@ -63,7 +63,7 @@
},
"type": "module",
"dependencies": {
"@melt-ui/svelte": "^0.34.3",
"@melt-ui/svelte": "^0.66.2",
"@popperjs/core": "^2.11.8",
"@remix-run/router": "~1.3.3",
"@sourcegraph/branded": "workspace:*",

View File

@ -150,7 +150,7 @@ export function getQueryURL(
queryState.searchMode
)
return 'search?' + searchQueryParameter
return '/search?' + searchQueryParameter
}
export function submitSearch(

View File

@ -18,6 +18,7 @@
import { computeFit } from '$lib/dom'
import { writable } from 'svelte/store'
import { getButtonClassName } from '@sourcegraph/wildcard'
import RepoSearchInput from './RepoSearchInput.svelte'
export let data: LayoutData
@ -100,6 +101,7 @@
</MenuLink>
{/each}
</DropdownMenu>
<RepoSearchInput repoName={data.repoName} />
</nav>
<slot />

View File

@ -0,0 +1,80 @@
<script lang="ts">
import { createDialog } from '@melt-ui/svelte'
import Icon from '$lib/Icon.svelte'
import SearchInput from '$lib/search/input/SearchInput.svelte'
import { queryStateStore } from '$lib/search/state'
import { settings } from '$lib/stores'
import { mdiMagnify } from '@mdi/js'
import { tick } from 'svelte'
export let repoName: string
const {
elements: { trigger, overlay, content },
states: { open },
} = createDialog()
let searchInput: SearchInput | undefined
let queryState = queryStateStore({ query: `repo:${repoName} ` }, $settings)
$: if ($open) {
// @melt-ui automatically focuses the search input but that positions the cursor at the
// start of the input. We can move the cursor to the end by calling focus(), but we need
// to wait for the next tick to ensure it happens after @melt-ui has updated the DOM.
tick().then(() => searchInput?.focus())
}
</script>
{#if $open}
<div class="wrapper">
<div {...$overlay} use:overlay class="overlay" />
<div {...$content} use:content>
<SearchInput bind:this={searchInput} {queryState} />
</div>
</div>
{:else}
<button {...$trigger} use:trigger>
<Icon svgPath={mdiMagnify} inline aria-hidden="true" />
Search
</button>
{/if}
<style lang="scss">
.wrapper {
flex: 1;
position: absolute;
left: 1rem;
right: 1rem;
// This seems needed to prevent the file headers (which are position: sticky) from overlaying
// the search input. Alternatively we could portal the search input with melt, but then
// it would be more difficult to position it over the repo header.
z-index: 2;
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
}
}
button {
background-color: transparent;
margin: 0;
padding: 0;
border: 1px solid var(--input-border-color);
border-radius: 4px;
padding: 0 0.25rem;
min-height: 32px;
width: 10rem;
text-align: left;
color: var(--text-muted);
white-space: nowrap;
&:focus {
border-color: var(--input-focus-border-color);
}
}
</style>

View File

@ -1385,8 +1385,8 @@ importers:
client/web-sveltekit:
dependencies:
'@melt-ui/svelte':
specifier: ^0.34.3
version: 0.34.3(svelte@4.1.1)
specifier: ^0.66.2
version: 0.66.2(svelte@4.1.1)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@ -5090,6 +5090,12 @@ packages:
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
dev: true
/@internationalized/date@3.5.1:
resolution: {integrity: sha512-LUQIfwU9e+Fmutc/DpRTGXSdgYZLBegi4wygCWDSVmUdLTaMHsQyASDiJtREwanwKuQLq0hY76fCJ9J/9I2xOQ==}
dependencies:
'@swc/helpers': 0.5.3
dev: false
/@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -5371,13 +5377,15 @@ packages:
react: 18.2.0
dev: true
/@melt-ui/svelte@0.34.3(svelte@4.1.1):
resolution: {integrity: sha512-qJE0+7+8Q8h1UhdqpAWWSJx3vXSjlgAKkfjLC5wn6dtVY70Jkb6Hiu726isZ2yNbPilBmbaLrOzfoMTRurjKXw==}
/@melt-ui/svelte@0.66.2(svelte@4.1.1):
resolution: {integrity: sha512-ufIhgOYP11A/G3AvW+2Qw74UGudMBJQ2wK+sETpU51VkC63/5D2sctKgXzGl0OUEJBlPHku6LRSry9rIeAVkNw==}
peerDependencies:
svelte: '>=3 <5'
dependencies:
'@floating-ui/core': 1.4.1
'@floating-ui/dom': 1.5.1
'@internationalized/date': 3.5.1
dequal: 2.0.3
focus-trap: 7.5.2
nanoid: 4.0.2
svelte: 4.1.1
@ -10579,6 +10587,12 @@ packages:
- supports-color
dev: true
/@swc/helpers@0.5.3:
resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==}
dependencies:
tslib: 2.1.0
dev: false
/@szmarczak/http-timer@1.1.2:
resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==}
engines: {node: '>=6'}