Browse Source

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

Adam 2 weeks ago
parent
commit
55119559b3
1 changed files with 88 additions and 82 deletions
  1. 88 82
      packages/ui/src/components/code.tsx

+ 88 - 82
packages/ui/src/components/code.tsx

@@ -1,5 +1,6 @@
 import { type FileContents, File, FileOptions, LineAnnotation, type SelectedLineRange } from "@pierre/diffs"
 import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
+import { Portal } from "solid-js/web"
 import { createDefaultOptions, styleVariables } from "../pierre"
 import { getWorkerPool } from "../pierre/worker"
 import { Icon } from "./icon"
@@ -125,11 +126,9 @@ export function Code<T>(props: CodeProps<T>) {
   let wrapper!: HTMLDivElement
   let container!: HTMLDivElement
   let findInput: HTMLInputElement | undefined
-  let findBar: HTMLDivElement | undefined
   let findOverlay!: HTMLDivElement
   let findOverlayFrame: number | undefined
   let findOverlayScroll: HTMLElement[] = []
-  let findScroll: HTMLElement | undefined
   let observer: MutationObserver | undefined
   let renderToken = 0
   let selectionFrame: number | undefined
@@ -159,6 +158,8 @@ export function Code<T>(props: CodeProps<T>) {
   let findMode: "highlights" | "overlay" = "overlay"
   let findHits: Range[] = []
 
+  const [findPos, setFindPos] = createSignal<{ top: number; right: number }>({ top: 8, right: 8 })
+
   const file = createMemo(
     () =>
       new File<T>(
@@ -291,23 +292,26 @@ export function Code<T>(props: CodeProps<T>) {
     setFindIndex(0)
   }
 
-  const getScrollParent = (el: HTMLElement): HTMLElement | null => {
+  const getScrollParent = (el: HTMLElement): HTMLElement | undefined => {
     let parent = el.parentElement
     while (parent) {
       const style = getComputedStyle(parent)
       if (style.overflowY === "auto" || style.overflowY === "scroll") return parent
       parent = parent.parentElement
     }
-    return null
   }
 
   const positionFindBar = () => {
-    if (!findBar || !wrapper) return
-    const scrollTop = findScroll ? findScroll.scrollTop : window.scrollY
-    findBar.style.position = "absolute"
-    findBar.style.top = `${scrollTop + 8}px`
-    findBar.style.right = "8px"
-    findBar.style.left = ""
+    if (typeof window === "undefined") return
+
+    const root = getScrollParent(wrapper) ?? wrapper
+    const rect = root.getBoundingClientRect()
+    const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
+    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) => {
@@ -426,7 +430,6 @@ export function Code<T>(props: CodeProps<T>) {
       }
       if (opts?.scroll && active) {
         scrollToRange(active)
-        positionFindBar()
       }
       return
     }
@@ -435,7 +438,6 @@ export function Code<T>(props: CodeProps<T>) {
     syncOverlayScroll()
     if (opts?.scroll && active) {
       scrollToRange(active)
-      positionFindBar()
     }
     scheduleOverlay()
   }
@@ -464,14 +466,12 @@ export function Code<T>(props: CodeProps<T>) {
         return
       }
       scrollToRange(active)
-      positionFindBar()
       return
     }
 
     clearHighlightFind()
     syncOverlayScroll()
     scrollToRange(active)
-    positionFindBar()
     scheduleOverlay()
   }
 
@@ -484,11 +484,9 @@ export function Code<T>(props: CodeProps<T>) {
       findCurrent = host
       findTarget = host
 
-      findScroll = getScrollParent(wrapper) ?? undefined
       if (!findOpen()) setFindOpen(true)
       requestAnimationFrame(() => {
         applyFind({ scroll: true })
-        positionFindBar()
         findInput?.focus()
         findInput?.select()
       })
@@ -514,18 +512,18 @@ export function Code<T>(props: CodeProps<T>) {
 
   createEffect(() => {
     if (!findOpen()) return
-    findScroll = getScrollParent(wrapper) ?? undefined
-    const target = findScroll ?? window
 
-    const handler = () => positionFindBar()
-    target.addEventListener("scroll", handler, { passive: true })
-    window.addEventListener("resize", handler, { passive: true })
-    handler()
+    const update = () => positionFindBar()
+    requestAnimationFrame(update)
+    window.addEventListener("resize", update, { passive: true })
+
+    const root = getScrollParent(wrapper) ?? wrapper
+    const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(() => update())
+    observer?.observe(root)
 
     onCleanup(() => {
-      target.removeEventListener("scroll", handler)
-      window.removeEventListener("resize", handler)
-      findScroll = undefined
+      window.removeEventListener("resize", update)
+      observer?.disconnect()
     })
   })
 
@@ -916,6 +914,64 @@ export function Code<T>(props: CodeProps<T>) {
     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 (
     <div
       data-component="code"
@@ -936,65 +992,15 @@ export function Code<T>(props: CodeProps<T>) {
       }}
     >
       <Show when={findOpen()}>
-        <div
-          ref={findBar}
-          class="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()}
-        >
-          <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)
+        <Portal>
+          <FindBar
+            class="fixed z-50 flex h-8 items-center gap-2 rounded-md border border-border-base bg-background-base px-3 shadow-md"
+            style={{
+              top: `${findPos().top}px`,
+              right: `${findPos().right}px`,
             }}
           />
-          <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>
+        </Portal>
       </Show>
       <div ref={container} />
       <div ref={findOverlay} class="pointer-events-none absolute inset-0 z-0" />