Skip to content

Scrollbox receives clicks outside its visible bounds (hit-testing ignores clipping) #467

@nyxkrage

Description

@nyxkrage

When a scrollbox contains content taller than its visible area, clicks outside the scrollbox's visible bounds are incorrectly captured by the scrollbox. The content is visually clipped correctly, but hit-testing uses the full unclipped content height.

  • When scrolled to top: clicking below the dialog hits the scrollbox
  • When scrolled to bottom: clicking above the dialog hits the scrollbox

Steps to Reproduce

  1. Create a dialog with an overlay (for click-to-close)
  2. Add a scrollbox inside the dialog with content taller than the visible area (e.g., 50 lines)
  3. Open the dialog
  4. Click directly below the scrollable content (in the overlay area)

Expected: The overlay receives the click and the dialog closes
Actual: The scrollbox receives the click (even though it's visually contained within the dialog)

Minimal Example

import { render, useKeyboard, useTerminalDimensions, useRenderer } from "@opentui/solid"
import { createSignal, Show, For } from "solid-js"

function App() {
  const [showDialog, setShowDialog] = createSignal(false)
  const [lastClick, setLastClick] = createSignal("none")
  const dimensions = useTerminalDimensions()
  const renderer = useRenderer()

  useKeyboard((key) => {
    if (key.name === "q") {
      renderer.destroy()
      process.exit(0)
    }
    if (key.name === "d") setShowDialog(!showDialog())
    if (key.name === "escape") setShowDialog(false)
  })

  const lines = Array.from({ length: 50 }, (_, i) => `Line ${i + 1}: This is some content`)

  return (
    <box flexDirection="column" height={dimensions().height} width={dimensions().width} backgroundColor="#1a1a2e">
      <text>Press 'd' to toggle dialog, 'q' to quit, 'esc' to close</text>
      <text>Last click: {lastClick()}</text>

      <Show when={showDialog()}>
        {/* Overlay - clicking here should close the dialog */}
        <box
          position="absolute"
          top={0}
          left={0}
          width={dimensions().width}
          height={dimensions().height}
          backgroundColor="#ff000033"
          onMouseDown={() => {
            setLastClick("OVERLAY (red)")
            setShowDialog(false)
          }}
        >
          <box
            position="absolute"
            top={Math.floor(dimensions().height / 4)}
            left={Math.floor(dimensions().width / 4)}
            width={Math.floor(dimensions().width / 2)}
            height={Math.floor(dimensions().height / 2)}
            backgroundColor="#0000ff"
            borderStyle="rounded"
            flexDirection="column"
            onMouseDown={(e: { stopPropagation: () => void }) => {
              console.log(">>> DIALOG BOX clicked")
              setLastClick("DIALOG BOX (blue)")
              e.stopPropagation()
            }}
          >
            <scrollbox
              flexGrow={1}
              backgroundColor="#ffff00"
              onMouseDown={(e: { stopPropagation: () => void }) => {
                setLastClick("SCROLLBOX (yellow)")
                e.stopPropagation()
              }}
            >
              <For each={lines}>{(line) => <text>{line}</text>}</For>
            </scrollbox>
          </box>
        </box>
      </Show>
    </box>
  )
}

render(() => <App />)
  • Press d to open the dialog
  • Click in the red area directly below the scrollable content in the dialog box
  • "Last click" shows "SCROLLBOX (yellow)" instead of "OVERLAY (red)" and closing the dialog

Environment

  • @opentui/core: 0.1.68
  • @opentui/solid: 0.1.68
  • bun: 1.3.5
  • OS: Linux x64
  • Terminal: Tested in Ghostty and the Zen integrated terminal

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcoreThis relates to the core package

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions