Skip to content

bug(core): border + fixed height + overflow="hidden" removes the last content row's mouse hit zone #1231

Description

@kylesnowschwartz

Note: this bug was diagnosed, reduced, and filed by an AI agent (Claude Code), with human review and approval before submission.

Summary

A box with border + fixed height + overflow="hidden" registers no hit-grid zone for its last content row. The row renders correctly, but onMouseDown on it never fires — clicks pass through to whatever is behind. Removing overflow="hidden" (nothing else) makes every row clickable.

Verified on @opentui/core + @opentui/solid 0.1.90 and 0.1.107 (macOS, bun).

Repro

Self-contained script using testRender — no terminal needed, bun run repro.tsx:

import { testRender } from "@opentui/solid";

const clicks: string[] = [];

function Row(p: { label: string }) {
  return (
    <box flexDirection="column" flexShrink={0} onMouseDown={() => clicks.push(p.label)}>
      <box flexDirection="row">
        <text>{p.label}</text>
      </box>
    </box>
  );
}

function App() {
  return (
    <box flexDirection="column" flexGrow={1}>
      <box flexGrow={1} />
      {/* overflow="hidden" is the trigger: remove it and row3 becomes clickable */}
      <box border borderStyle="rounded" flexShrink={0} height={6} overflow="hidden">
        <box flexDirection="column">
          <box flexDirection="row" paddingLeft={1} onMouseDown={() => clicks.push("name")}>
            <text>session-name</text>
          </box>
          <box flexDirection="column" paddingLeft={1}>
            <Row label="row1" />
            <Row label="row2" />
            <Row label="row3" />
          </box>
        </box>
      </box>
      <box flexGrow={1} />
    </box>
  );
}

const { renderOnce, captureCharFrame, mockMouse, mockInput } = await testRender(() => <App />, {
  width: 30,
  height: 14,
});

await renderOnce();
captureCharFrame().split("\n").forEach((l, i) => console.log(i, JSON.stringify(l.slice(0, 24))));

for (let y = 0; y < 12; y++) {
  clicks.length = 0;
  await mockMouse.click(5, y);
  await renderOnce();
  console.log(`click y=${y} -> ${clicks.length ? clicks.join(",") : "(nothing)"}`);
}

// Same result through the real stdin path (raw SGR, 1-based coords):
for (const [label, sgrY] of [["name", 6], ["row1", 7], ["row2", 8], ["row3", 9]] as const) {
  clicks.length = 0;
  mockInput.pressKey(`\x1b[<0;6;${sgrY}M`);
  mockInput.pressKey(`\x1b[<0;6;${sgrY}m`);
  await renderOnce();
  console.log(`SGR y=${sgrY} (${label}) -> ${clicks.length ? clicks.join(",") : "(nothing)"}`);
}
process.exit(0);

Actual output (0.1.107)

4 "╭───────────────────────"
5 "│ session-name          "
6 "│ row1                  "
7 "│ row2                  "
8 "│ row3                  "
9 "╰───────────────────────"

click y=5 -> name
click y=6 -> row1
click y=7 -> row2
click y=8 -> (nothing)     <- row3 renders here but has no zone

SGR y=6 (name) -> name
SGR y=7 (row1) -> row1
SGR y=8 (row2) -> row2
SGR y=9 (row3) -> (nothing)

Both the mockMouse path and the raw SGR stdin path agree: every zone is correctly aligned with its rendered row, except the last content row, which has no zone at all.

Expected

click y=8 -> row3 — the last row inside the border should be clickable like the others.

Notes

  • The trigger is specifically the combination border + fixed height + overflow="hidden". Dropping overflow="hidden" fixes hit-testing with no visual change (content already fits the fixed height).
  • It always eats exactly the last content row (the one adjacent to the bottom border), regardless of row count.
  • Real-world symptom that led here: the bottom row of a bordered list in a tmux sidebar was rendered but unclickable, which initially looked like an app-level state bug.
  • Workaround we shipped: omit overflow="hidden" and rely on height math for truncation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions