Skip to content

fix(tooltip): keep Kumo's disabled trigger out of the tab order - #902

Merged
mattrothenberg merged 1 commit into
cloudflare:mainfrom
wking-io:wking/tooltip-disabled-trigger-tab-order
Oct 28, 2026
Merged

mattrothenberg merged 1 commit into
cloudflare:mainfrom
wking-io:wking/tooltip-disabled-trigger-tab-order

Conversation

@wking-io

@wking-io wking-io commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When Tooltip renders its own trigger (no render or asChild), the trigger is a native <button> around non-interactive content. The common case is truncated Text that needs a tooltip only when it overflows:

<Tooltip content={name} disabled={!isTruncated}>
  <Text truncate>{name}</Text>
</Tooltip>

While the tooltip is disabled, the trigger has nothing to reveal, but it stays a tab stop. In a table of names, keyboard users stop on every row even though only the truncated ones show anything. Consumers can't fix this themselves, because Tooltip passes extra props to Tooltip.Root, not the trigger. Their workaround is a custom render trigger with a hand-written tabIndex. But a custom trigger skips Kumo's defensive trigger resets (border-none bg-transparent p-0 …), so in apps without Tailwind preflight it picks up native button styling unless the consumer copies those resets too.

Fix

Kumo's own trigger gets tabIndex={-1} while the tooltip is disabled. Nothing else changes:

  • An enabled tooltip's trigger is focusable as before, so keyboard users can still open it.
  • Custom triggers (render or the deprecated asChild) keep their own focus behavior. They're usually real controls; Sidebar.MenuButton and Menubar both use render, so they're unaffected.
  • No new API.

With this, the snippet above works as-is: truncated names are focusable and show the tooltip, and names that fit aren't tab stops.

Behavior note

Anyone relying on a disabled tooltip's default trigger being keyboard-focusable will see it skipped. That seems unlikely to be intentional, since the trigger does nothing while disabled, but say so if you know of a case. A separate focusable-style option would avoid it, at the cost of new API.

Tests

Three unit tests in tooltip.test.tsx. Each tabs from a button before the tooltip:

  • Disabled, Kumo's trigger: tabindex="-1", and focus skips to the next button. Fails on main, passes with this change.
  • Enabled, Kumo's trigger: no tabindex, and the trigger takes focus. Passes on both.
  • Disabled, custom render trigger: no tabindex, and the trigger takes focus. Passes on both.

Checks:

  • Unit suite: 1315 passed, 0 failed (after codegen:demos and codegen:registry).

  • pnpm typecheck, pnpm lint and Oxfmt are clean.

  • The tooltip visual contract ("keeps a truncating Text trigger visible") renders an enabled tooltip, so this change doesn't reach it. Locally on macOS it fails with an image size mismatch (78×20 vs the stored 148×38), and main fails the same way.

  • Reviews

  • bonk has reviewed the change

  • automated review not possible because: bonk requires repository write access, which this fork's author doesn't have yet; requesting human review

  • Tests

  • Tests included/updated

  • Automated tests not possible - manual testing has been completed as follows:

  • Additional testing not necessary because:

@wking-io
wking-io marked this pull request as ready for review October 28, 2026 20:55
@clanky-the-clanker

Copy link
Copy Markdown

This pull request was not reviewed automatically because its author does not have write access to this repository. A maintainer with write access can start a review by commenting clanky review (or clanker review or @clanky-the-clanker review), optionally followed by instructions (e.g. clanky review focus on the retry logic).

When Kumo renders its own trigger (no render or asChild), it wraps
non-interactive content such as truncated Text that only needs a tooltip
when it overflows. A disabled tooltip there has nothing to reveal, but the
native button trigger was still a tab stop. Set tabIndex -1 on Kumo's
trigger while disabled; custom triggers keep their own focus behavior.

Co-authored-by: Pi <noreply@pi.dev>
Co-authored-by: cloudflare <noreply@unknown.com>
Co-authored-by: claude-opus-5-5 <noreply@unknown.com>
@wking-io
wking-io force-pushed the wking/tooltip-disabled-trigger-tab-order branch from e5932e7 to 186fc21 Compare October 28, 2026 21:33
@pkg-pr-new

pkg-pr-new Bot commented Oct 28, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@cloudflare/kumo@902

commit: 186fc21

@github-actions

Copy link
Copy Markdown
Contributor

Docs Preview

View docs preview

Commit: 186fc21

@github-actions

Copy link
Copy Markdown
Contributor

Visual contracts

Linux Chromium references exercised by the PR's visual-contracts job.

1 curated contract

truncating text trigger

truncating text trigger

@github-actions

Copy link
Copy Markdown
Contributor

📐 Bundle Size

Consumer fixtures bundled against packages/kumo/dist (peers external, minified ESM).

Fixture Raw Gzip Brotli
Button (root) 166.6 KB 51.0 KB 44.3 KB
Button (components/button) 166.5 KB 50.9 KB 44.3 KB
Button + Dialog + Select (root) 293.5 KB 87.6 KB 73.7 KB
Button + Dialog + Select (component subpaths) 293.6 KB 87.7 KB 73.7 KB
Chart (root) 238.5 KB 71.9 KB 61.9 KB
Chart (components/chart) 238.5 KB 71.9 KB 61.9 KB
Badge (components/badge) 36.5 KB 12.7 KB 11.1 KB
Flow (components/flow) 200.7 KB 59.2 KB 51.8 KB
Button (primitives/button) 12.4 KB 4.5 KB 4.0 KB
Primitives barrel 718.4 KB 198.5 KB 154.5 KB
Code highlighting (code) 2.07 MB 469.3 KB 350.5 KB

npm tarball: 565 files, 1.56 MB packed, 7.30 MB unpacked.

⚠️ 25 flagged files in tarball (tests / raw scripts)
  • dist/blocks-source/delete-resource/delete-resource.test.tsx
  • dist/blocks-source/resource-list/resource-list.test.tsx
  • scripts/component-registry/cache.ts
  • scripts/component-registry/discovery.ts
  • scripts/component-registry/example-cleanup.ts
  • scripts/component-registry/index.test.ts
  • scripts/component-registry/index.ts
  • scripts/component-registry/markdown-generator.ts
  • scripts/component-registry/metadata.ts
  • scripts/component-registry/props-filter.ts
  • scripts/component-registry/schema-generator.ts
  • scripts/component-registry/sub-components.ts
  • scripts/component-registry/types.ts
  • scripts/component-registry/utils.ts
  • scripts/component-registry/variant-parser.ts
  • scripts/css-build.ts
  • scripts/format-generated.ts
  • scripts/generate-globe-land-mask.mjs
  • scripts/generate-primitives.ts
  • scripts/theme-generator/config.ts
  • scripts/theme-generator/generate-css.test.ts
  • scripts/theme-generator/generate-css.ts
  • scripts/theme-generator/index.ts
  • scripts/theme-generator/migrate.ts
  • scripts/theme-generator/types.ts

Report-only: no size thresholds are enforced yet.

Commit: 186fc21a

@mattrothenberg
mattrothenberg merged commit 4185bb1 into cloudflare:main Oct 28, 2026
15 checks passed
@github-actions

Copy link
Copy Markdown
Contributor
Visual Regression Report — 0 changed, 6 unchanged

No visual changes detected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants