fix(app): don't scroll code search input

This commit is contained in:
Adam
2026-02-10 14:01:14 -06:00
committed by opencode
parent fd5531316f
commit 55119559b3

View File

@@ -1,5 +1,6 @@
import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs" import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
import { Portal } from "solid-js/web"
import { createDefaultOptions, styleVariables } from "../pierre" import { createDefaultOptions, styleVariables } from "../pierre"
import { getWorkerPool } from "../pierre/worker" import { getWorkerPool } from "../pierre/worker"
import { Icon } from "./icon" import { Icon } from "./icon"
@@ -125,11 +126,9 @@ export function Code<T>(props: CodeProps<T>) {
let wrapper!: HTMLDivElement let wrapper!: HTMLDivElement
let container!: HTMLDivElement let container!: HTMLDivElement
let findInput: HTMLInputElement | undefined let findInput: HTMLInputElement | undefined
let findBar: HTMLDivElement | undefined
let findOverlay!: HTMLDivElement let findOverlay!: HTMLDivElement
let findOverlayFrame: number | undefined let findOverlayFrame: number | undefined
let findOverlayScroll: HTMLElement[] = [] let findOverlayScroll: HTMLElement[] = []
let findScroll: HTMLElement | undefined
let observer: MutationObserver | undefined let observer: MutationObserver | undefined
let renderToken = 0 let renderToken = 0
let selectionFrame: number | undefined let selectionFrame: number | undefined
@@ -159,6 +158,8 @@ export function Code<T>(props: CodeProps<T>) {
let findMode: "highlights" | "overlay" = "overlay" let findMode: "highlights" | "overlay" = "overlay"
let findHits: Range[] = [] let findHits: Range[] = []
const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 })
const file = createMemo( const file = createMemo(
() => () =>
new File<T>( new File<T>(
@@ -291,23 +292,26 @@ export function Code<T>(props: CodeProps<T>) {
setFindIndex(0) setFindIndex(0)
} }
const getScrollParent = (el: HTMLElement): HTMLElement | null => { const getScrollParent = (el: HTMLElement): HTMLElement | undefined => {
let parent = el.parentElement let parent = el.parentElement
while (parent) { while (parent) {
const style = getComputedStyle(parent) const style = getComputedStyle(parent)
if (style.overflowY === "auto" || style.overflowY === "scroll") return parent if (style.overflowY === "auto" || style.overflowY === "scroll") return parent
parent = parent.parentElement parent = parent.parentElement
} }
return null
} }
const positionFindBar = () => { const positionFindBar = () => {
if (!findBar || !wrapper) return if (typeof window === "undefined") return
const scrollTop = findScroll ? findScroll.scrollTop : window.scrollY
findBar.style.position = "absolute" const root = getScrollParent(wrapper) ?? wrapper
findBar.style.top = `${scrollTop + 8}px` const rect = root.getBoundingClientRect()
findBar.style.right = "8px" const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
findBar.style.left = "" const header = Number.isNaN(title) ? 0 : title
setFindPos({
top: Math.round(rect.top) + header - 4,
right: Math.round(window.innerWidth - rect.right) + 8,
})
} }
const scanFind = (root: ShadowRoot, query: string) => { const scanFind = (root: ShadowRoot, query: string) => {
@@ -426,7 +430,6 @@ export function Code<T>(props: CodeProps<T>) {
} }
if (opts?.scroll && active) { if (opts?.scroll && active) {
scrollToRange(active) scrollToRange(active)
positionFindBar()
} }
return return
} }
@@ -435,7 +438,6 @@ export function Code<T>(props: CodeProps<T>) {
syncOverlayScroll() syncOverlayScroll()
if (opts?.scroll && active) { if (opts?.scroll && active) {
scrollToRange(active) scrollToRange(active)
positionFindBar()
} }
scheduleOverlay() scheduleOverlay()
} }
@@ -464,14 +466,12 @@ export function Code<T>(props: CodeProps<T>) {
return return
} }
scrollToRange(active) scrollToRange(active)
positionFindBar()
return return
} }
clearHighlightFind() clearHighlightFind()
syncOverlayScroll() syncOverlayScroll()
scrollToRange(active) scrollToRange(active)
positionFindBar()
scheduleOverlay() scheduleOverlay()
} }
@@ -484,11 +484,9 @@ export function Code<T>(props: CodeProps<T>) {
findCurrent = host findCurrent = host
findTarget = host findTarget = host
findScroll = getScrollParent(wrapper) ?? undefined
if (!findOpen()) setFindOpen(true) if (!findOpen()) setFindOpen(true)
requestAnimationFrame(() => { requestAnimationFrame(() => {
applyFind({ scroll: true }) applyFind({ scroll: true })
positionFindBar()
findInput?.focus() findInput?.focus()
findInput?.select() findInput?.select()
}) })
@@ -514,18 +512,18 @@ export function Code<T>(props: CodeProps<T>) {
createEffect(() => { createEffect(() => {
if (!findOpen()) return if (!findOpen()) return
findScroll = getScrollParent(wrapper) ?? undefined
const target = findScroll ?? window
const handler = () => positionFindBar() const update = () => positionFindBar()
target.addEventListener("scroll", handler, { passive: true }) requestAnimationFrame(update)
window.addEventListener("resize", handler, { passive: true }) window.addEventListener("resize", update, { passive: true })
handler()
const root = getScrollParent(wrapper) ?? wrapper
const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update())
observer?.observe(root)
onCleanup(() => { onCleanup(() => {
target.removeEventListener("scroll", handler) window.removeEventListener("resize", update)
window.removeEventListener("resize", handler) observer?.disconnect()
findScroll = undefined
}) })
}) })
@@ -916,6 +914,64 @@ export function Code<T>(props: CodeProps<T>) {
pendingSelectionEnd = false pendingSelectionEnd = false
}) })
const FindBar = (barProps: { class: string; style?: ComponentProps<"div">["style"] }) => (
<div class={barProps.class} style={barProps.style} onPointerDown={(e) => e.stopPropagation()}>
<Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" />
<input
ref={findInput}
placeholder="Find"
value={findQuery()}
class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak"
onInput={(e) => {
setFindQuery(e.currentTarget.value)
setFindIndex(0)
applyFind({ reset: true, scroll: true })
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault()
closeFind()
return
}
if (e.key !== "Enter") return
e.preventDefault()
stepFind(e.shiftKey ? -1 : 1)
}}
/>
<div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}>
{findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"}
</div>
<div class="flex items-center">
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
disabled={findCount() === 0}
aria-label="Previous match"
onClick={() => stepFind(-1)}
>
<Icon name="chevron-down" size="small" class="rotate-180" />
</button>
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
disabled={findCount() === 0}
aria-label="Next match"
onClick={() => stepFind(1)}
>
<Icon name="chevron-down" size="small" />
</button>
</div>
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong"
aria-label="Close search"
onClick={closeFind}
>
<Icon name="close-small" size="small" />
</button>
</div>
)
return ( return (
<div <div
data-component="code" data-component="code"
@@ -936,65 +992,15 @@ export function Code<T>(props: CodeProps<T>) {
}} }}
> >
<Show when={findOpen()}> <Show when={findOpen()}>
<div <Portal>
ref={findBar} <FindBar
class="z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md" class="fixed z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md"
onPointerDown={(e) => e.stopPropagation()} style={{
> top: `${findPos().top}px`,
<Icon name="magnifying-glass" size="small" class="text-text-weak shrink-0" /> right: `${findPos().right}px`,
<input
ref={findInput}
placeholder="Find"
value={findQuery()}
class="w-40 bg-transparent outline-none text-14-regular text-text-strong placeholder:text-text-weak"
onInput={(e) => {
setFindQuery(e.currentTarget.value)
setFindIndex(0)
applyFind({ reset: true, scroll: true })
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault()
closeFind()
return
}
if (e.key !== "Enter") return
e.preventDefault()
stepFind(e.shiftKey ? -1 : 1)
}} }}
/> />
<div class="shrink-0 text-12-regular text-text-weak tabular-nums text-right" style={{ width: "10ch" }}> </Portal>
{findCount() ? `${findIndex() + 1}/${findCount()}` : "0/0"}
</div>
<div class="flex items-center">
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
disabled={findCount() === 0}
aria-label="Previous match"
onClick={() => stepFind(-1)}
>
<Icon name="chevron-down" size="small" class="rotate-180" />
</button>
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong disabled:opacity-40 disabled:pointer-events-none"
disabled={findCount() === 0}
aria-label="Next match"
onClick={() => stepFind(1)}
>
<Icon name="chevron-down" size="small" />
</button>
</div>
<button
type="button"
class="size-6 grid place-items-center rounded text-text-weak hover:bg-surface-base-hover hover:text-text-strong"
aria-label="Close search"
onClick={closeFind}
>
<Icon name="close-small" size="small" />
</button>
</div>
</Show> </Show>
<div ref={container} /> <div ref={container} />
<div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" /> <div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" />