Skip to content

feat(bulma-ui): let a Tooltip be hovered, dismissed with Escape, and opened by a tap - #1033

Merged
allxsmith merged 19 commits into
mainfrom
fix/tooltip-wcag-1413
Oct 12, 2026
Merged

allxsmith merged 19 commits into
mainfrom
fix/tooltip-wcag-1413

Conversation

@allxsmith

@allxsmith allxsmith commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Tooltip didn't meet WCAG's Content on Hover or Focus criterion. You couldn't move the pointer onto it, and nothing dismissed it short of moving the pointer or focus away. This fixes both by putting Tooltip on the same hover hook HoverCard uses, plus a stylesheet change. It also adds one opt-in prop, openOnTap, so touch users have a way to see a tooltip, and fixes HoverCard and Tooltip not opening when the pointer comes into a shadow root straight from the page, which is what made the docs previews look flaky.

A few behaviors change along the way, and two of them can break a test or surprise someone, so they're spelled out under Behavior changes below.

It builds on the useHoverOpen hook that came in with HoverCard (#1030).

What failed

  • Hoverable: .tooltip-content had pointer-events: none, so the pointer went straight through it. There's also a gap between the trigger and the tooltip for the arrow, and closeDelay defaults to 0, so even a tooltip that took pointer events would have closed the moment the pointer left the trigger.
  • Dismissible: nothing listened for Escape.
  • Persistent: mostly fine, since no timer hid a hovered tooltip. But hover and focus shared one flag, so with keyboard focus on the trigger, a pointer passing over and leaving hid a tooltip focus was still holding.

I checked all of that in Chromium on the built Storybook before touching anything, and it failed the way the code says it would.

What changed

Tooltip runs on useHoverOpen. The hand-rolled timers and the single hover flag are gone. The hook already does what 1.4.13 asks: pointer enter and leave (ignoring touch), keyboard focus as the browser reports it through :focus-visible, both delays, a floating element the pointer can be on, and Escape. delay maps to openDelay and closeDelay to closeDelay. The positioning and auto measuring code is untouched.

Escape hides it and carries on. Escape hides a shown tooltip wherever focus is, and the key always goes on to whatever has focus, or to a Modal or Popover around it. A tooltip only adds to its trigger, so it shouldn't stand between a control and its own key: a Dropdown in a Tooltip closes on the first Escape, and the tooltip hides on the same one. The hook gained a consumeEscape option for this, on by default, so HoverCard keeps taking the key when focus is in the card or on its trigger.

The stylesheet. A tooltip takes the pointer only while the pointer, focus or a tap has it open, which Tooltip marks with an is-open class. A hidden tooltip, or one that only active shows, lets the pointer through to whatever is under it, as it always did. While it's open, the wrapper draws a transparent bridge exactly as wide as the trigger across the gap, so the pointer can go straight from the trigger onto the tooltip without leaving. That's why closeDelay={0} still works. Page content beside the trigger stays clickable, and the arrow takes no pointer, so it never covers the trigger's edge. The gap's size lives on .tooltip-content, out of the wrapper's reach, so the bridge takes the same defaults for each size, and a test keeps the two in step.

active. An always-shown tooltip is the page's choice rather than something hover or focus brought, so it's outside 1.4.13. The hook tracks the pointer and focus as they are, and active only adds to what's rendered. So hover, focus and Escape leave an active tooltip shown, Escape over it goes on as it does for any tooltip, and dropping active while the pointer rests on the trigger leaves it shown until the pointer leaves.

openOnTap. By default a touch screen never shows a tooltip: there's no hover, and a tap's focus isn't keyboard focus. openOnTap, off by default like HoverCard's, shows it with a tap on the trigger and hides it on the next tap, a tap anywhere else, or Escape. The tap still does whatever the trigger does, so it's for a trigger with no tap action of its own. The Tooltip page now opens with a note saying a tap shows nothing by default, so anything a touch user needs can't live only in a tooltip.

Pointer coming into a shadow root. React makes onPointerEnter out of the pointerout on the element the pointer left, and skips the pointerover that follows when its relatedTarget is an element React renders. When a page portals content into a shadow root, as the docs site's live examples do through react-shadow, the pointerout the page gets on the way in names only the shadow host, so React's enter stops there, and the pointerover inside names page content React owns, so React skips it. A HoverCard or Tooltip in the preview never heard the pointer arrive straight from the page, only from inside the preview, which is why it looked flaky. The hook now also takes onPointerOver: one whose relatedTarget is outside the holder and the floating element counts as the pointer arriving, when no enter already did, and touch still doesn't. One from inside is the pointer moving about within, so a tooltip or card Escape closed stays closed until the pointer leaves and comes back. Leaving already worked.

The wrapper stands in for the trigger. The hook expects the element that takes its handlers to hold only the trigger and the floating element. Tooltip's wrapper holds just the children and the tooltip, so it passes the wrapper as the trigger. That only works while nothing in the tooltip takes focus, so the content TSDoc now says tooltip content is for reading, with links and buttons in a HoverCard or Popover.

Handlers. HoverCard's little chain helper moved into useHoverOpen.ts so both components share it. That fixes a quiet bug too: an onFocus or onBlur you passed to Tooltip used to replace its own, so focus stopped showing it. Now yours runs alongside, and so do your pointer handlers.

TSDoc for content, delay, closeDelay, active, openOnTap and the component itself is updated, the docs page's Accessibility section walks through the three conditions, there's an "On Touch Screens" example and a TouchOpen story, and the HoverCard page and one HoverCard test title lose an "unlike a tooltip" that stopped being true. No skill describes Tooltip's behavior, so there's nothing to update there. The regenerated props table and MCP index are in their own commits.

One HoverCard docs fix rides along, promised in a review thread on #1030 before it merged. Its Dismissible bullet now says Escape closes the card "without you moving the pointer or focus", which is WCAG's sense of it, and that in controlled use Escape asks to close through onOpenChange, so the card closes when you set open to false. It also says a card Escape closed stays closed until the pointer or focus leaves and comes back, or, with openOnTap, until the next tap on the trigger.

Why reuse the hook rather than patch Tooltip

I looked at whether Tooltip needed a hook option to open on any focus, since that's what Tooltip did. It doesn't, and adding it would have made things worse. With hover and focus tracked separately (which persistence needs), "any focus" means clicking a button leaves its tooltip stuck up after the pointer moves away. Opening on keyboard focus only, the :focus-visible rule the hook already uses, matches what a mouse user saw before and is what keyboard users need.

A direct fix in Tooltip would have been a second copy of the Escape listener, the touch filtering, the delay bookkeeping and now the shadow-root fallback. That's exactly the drift one hook exists to prevent. The hook gained consumeEscape, onPointerOver and the shared chain, and HoverCard's existing tests pass as they were.

Behavior changes

These are the things that act differently afterward.

Tests that only fire mouseEnter stop showing the tooltip. Tooltip listens for pointer events now, not mouse events, so fireEvent.mouseEnter(...) on its own does nothing. Use user-event's hover (and unhover), which sends pointer events along with the mouse ones, or fire pointerOver and pointerOut yourself:

// Before: showed the tooltip. Now: does nothing.
fireEvent.mouseEnter(trigger);

// Now: either of these.
await userEvent.setup().hover(trigger);
fireEvent.pointerOver(trigger);

A test that advances fake timers after hovering is fine as it is. One that checks for is-active right after the event with delay={0} needs to let a zero-length timer run first, since the show now goes through one.

A click or a tap doesn't show a tooltip any more.

  • Click focus: focus that a click brings to the trigger doesn't show it, and neither does focus your code moves after a click. A tooltip still shows while the pointer is over the trigger, so a mouse user sees no change there, but it no longer stays up after the pointer leaves just because the clicked button kept focus. Keyboard focus (Tab, or focus your code moves after a key) still shows it, and now holds it while the pointer comes and goes.
  • Touch: a tap on the trigger doesn't show it. It used to, through the browser's compatibility mouse events, and then it stayed up until you tapped somewhere else. Now tapping the trigger just does what the trigger does, unless you turn on openOnTap. So anything a touch user needs to read shouldn't live only in a tooltip.

Tests see this differently from a browser, though. Tooltip asks the browser whether focus is keyboard focus through :focus-visible, and jsdom matches that on any focus. So in a jsdom test, user.tab(), a plain focus(), user.click() and a touch tap that focuses the trigger all show the tooltip, and a clicked trigger keeps it up after user.unhover(). A test that wants the browser's behavior for a click has to make the trigger's matches(':focus-visible') answer false while it takes focus, which is what Tooltip's own tests do.

The rest you'd only notice by looking:

  • While the pointer, focus or a tap has a tooltip open, it takes the pointer, so a click on the spot it covers lands on the tooltip and you can select its text. An active tooltip nobody is hovering lets clicks through, as before.
  • Escape hides a showing tooltip and still reaches whatever has focus. Inside a Modal or Popover, one Escape hides the tooltip and closes the overlay too.

Not in here

The trigger isn't linked to the tooltip for screen readers. There's no id on the content and no aria-describedby anywhere, so focusing the trigger announces its own name and not the tooltip. Fixing that means Tooltip reaching into children with cloneElement, which only works for a single element child that passes the attribute through to the DOM, and has to merge with an aria-describedby the child already has. That's a design call (HoverCard sidesteps it with a trigger prop), so it's its own issue, #1031. The Accessibility section says it plainly for now, and suggests an aria-label on icon-only triggers.

How it was verified

  • The Tooltip suite moved from mouse to pointer events and covers keyboard focus (shows after the delay, holds while the pointer comes and goes, click focus doesn't hold it), hoverable (straight from the trigger onto the tooltip with no closeDelay, no timer while the pointer is on it, pen), touch (no show from a tap or its focus, a touch pointer leaving doesn't close what the mouse holds, and openOnTap showing it on a tap and hiding it on the next one or a tap elsewhere), Escape (from the trigger with focus kept, with focus in a field elsewhere and the key reaching that field, a child's own Escape handler still running, a Dropdown in the trigger closing on the same key, one Escape closing a Modal or Popover around it too, cancelling a delayed show, and a tooltip inside an open HoverCard hiding before the card), no reopen until the pointer or focus leaves and comes back, a trigger in a shadow root reached straight from the page, active (shown whatever happens, and still shown under the pointer once dropped), auto resolving when hover shows it, overflow correction clearing on Escape, a ConfigProvider prefix sweep, the wrapper's own handlers, nothing held after unmount, and hydration.
  • HoverCard and the hook gained the shadow-root cases: the pointer coming from the page opens after the delay and closes when it goes back, touch from the page doesn't, moving between the trigger's own children after Escape doesn't reopen, and a pointerover inside a portaled card doesn't count as arriving. The hook also has a consumeEscape test.
  • Tooltip.styles.test.tsx compiles the real partial under a class prefix and checks pointer events hidden, active only and open, the arrow out of hit testing, and the bridge on the wrapper in each position, trigger-wide, with its size matching the tooltip's gap for each size.
  • Tooltip.ssr.test.tsx renders on the node environment, hidden and active.
  • I broke each piece on purpose and watched its tests fail: the shadow-root fallback and each of its guards, the pointer events, the arrow rule, the is-open class, the press handlers, the old active turn-down and the handler chaining.
  • HoverCard and useHoverOpen stay fully covered, and full bulma-ui coverage clears the thresholds. Tooltip's lines are all covered, and the one statement left is the same unreachable ref guard that was uncovered before.
  • In real Chromium on the built Storybook: hovering straight onto the tooltip in each position, auto in every spot of the AutoPlacement story plus a short viewport that forces it left and right, multiline, and the dashed text trigger; the gap taking the pointer while open and the gap band beside the trigger staying the page's; the trigger's edge under the arrow staying the trigger's at every size; Escape with the pointer on the trigger and on the tooltip, with keyboard focus, and with focus in the form story's password field, each hiding the tooltip and letting the key through; an active tooltip nobody hovers letting clicks through; keyboard focus holding it after the pointer leaves; a clicked trigger not holding it; and taps with and without openOnTap.
  • On the docs site, built from this branch and driven with Playwright: moving the mouse straight from page text outside a live preview onto the Basic HoverCard trigger, a Tooltip trigger and the 500ms delay example, each opens after its delay and closes when the pointer goes back, and Popover's click still opens it. The same script against docs built from the code before the shadow-root fix fails to open each of them.
  • I ran each test call from Behavior changes against the new Tooltip in jsdom: mouseEnter alone, pointerOver, user-event's hover, unhover, tab and click, a plain focus(), and a user-event touch tap. Each did what that section says.
  • Both typechecks, lint, format, conformance and the generator checks pass.

WCAG 1.4.13 asks hover and focus content to be dismissible, hoverable
and persistent. Tooltip missed the first two: nothing hid it on Escape,
and the pointer couldn't reach it, since the content ignored pointer
events and closed as the pointer crossed the gap. It also tracked hover
and focus as one flag, so a pointer leaving hid a tooltip keyboard focus
still held.

Tooltip now runs on useHoverOpen, the hook HoverCard uses, and the hook
needed no new option. Its delays map to openDelay and closeDelay, and an
`active` tooltip turns the hook's asks to open down, so Escape leaves it
shown and goes on to whatever listens. Opening on keyboard focus only,
rather than any focus, keeps a clicked trigger from holding its tooltip
once the pointer leaves. The handler chaining HoverCard kept to itself
moves into the hook's module so both share it, and the wrapper's own
pointer and focus handlers now run alongside the tooltip's instead of
replacing them.

The stylesheet lets the pointer onto a shown tooltip only, and a
transparent ::after bridges the gap to the trigger in each position, so
closeDelay={0} still reaches it. A hidden tooltip still lets the pointer
through.
…led one closes

WCAG's dismissible condition is about the user not having to move the
pointer or focus, so the Dismissible bullet now says "without you
moving". In controlled use Escape only asks to close through
onOpenChange, so the bullet adds that the card closes when the caller
sets open to false. It also says that with openOnTap, the next tap on
the trigger opens a card Escape closed.
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

This makes Tooltip meet WCAG 1.4.13 by putting it on the useHoverOpen hook HoverCard uses (#1030): it's hoverable (pointer-events while shown, plus a transparent bridge over the arrow gap) and dismissible with Escape under the hook's focus-ownership rule. active tooltips turn down the hook's asks so they stay shown. Review in full: Tooltip.tsx, _tooltip.scss, the chain move into useHoverOpen.ts, the Tooltip tests (including the new styles and SSR files), and the docs. Check the behaviour changes the PR lists, including pointer events instead of mouse events and click or tap focus no longer showing a tooltip, against what the code does. Also check the one HoverCard docs bullet that rides along. Post every finding, advisory included, as its own thread.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1de5e40d-25ce-4c91-af89-bcc5cb2328b3

📥 Commits

Reviewing files that changed from the base of the PR and between c33a894 and f940611.


📒 Files selected for processing (18)
  • bestax-mcp/data/catalog.json
  • bestax-mcp/data/components/HoverCard.json
  • bestax-mcp/data/components/Tooltip.json
  • bestax-mcp/data/skills.json
  • bulma-ui/src/components/HoverCard.tsx
  • bulma-ui/src/components/Tooltip.stories.tsx
  • bulma-ui/src/components/Tooltip.tsx
  • bulma-ui/src/components/__tests__/HoverCard.styles.test.tsx
  • bulma-ui/src/components/__tests__/HoverCard.test.tsx
  • bulma-ui/src/components/__tests__/Tooltip.ssr.test.tsx
  • bulma-ui/src/components/__tests__/Tooltip.styles.test.tsx
  • bulma-ui/src/components/__tests__/Tooltip.test.tsx
  • bulma-ui/src/helpers/__tests__/useHoverOpen.test.tsx
  • bulma-ui/src/helpers/useHoverOpen.ts
  • bulma-ui/src/scss/components/_tooltip.scss
  • docs/docs/api/components/hovercard.md
  • docs/docs/api/components/tooltip.md
  • skills/bestax-theming/references/css-variables.md

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 007b678c-0976-4e21-9042-ac41bfca316a



📥 Commits

Reviewing files that changed from the base of the PR and between 40d44c6 and c33a894.




📒 Files selected for processing (14)
  • bestax-mcp/data/catalog.json
  • bestax-mcp/data/components/HoverCard.json
  • bestax-mcp/data/components/Tooltip.json
  • bulma-ui/src/components/HoverCard.tsx
  • bulma-ui/src/components/Tooltip.stories.tsx
  • bulma-ui/src/components/Tooltip.tsx
  • bulma-ui/src/components/__tests__/HoverCard.test.tsx
  • bulma-ui/src/components/__tests__/Tooltip.styles.test.tsx
  • bulma-ui/src/components/__tests__/Tooltip.test.tsx
  • bulma-ui/src/helpers/__tests__/useHoverOpen.test.tsx
  • bulma-ui/src/helpers/useHoverOpen.ts
  • bulma-ui/src/scss/components/_tooltip.scss
  • docs/docs/api/components/hovercard.md
  • docs/docs/api/components/tooltip.md



🚧 Files skipped from review as they are similar to previous changes (1)
  • bestax-mcp/data/components/HoverCard.json



Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.





Walkthrough

Tooltip now uses shared hover-open handling for pointer and keyboard-focus interactions, delayed visibility, Escape behavior, and active state. It adds optional touch activation and allows pointer interaction with visible content. Tests, examples, and documentation cover these interactions. HoverCard reuses shared event handling and has additional portal interaction tests.

Changes

Tooltip and HoverCard interactions

Layer / File(s) Summary
Shared interaction handling
bulma-ui/src/helpers/useHoverOpen.ts, bulma-ui/src/components/Tooltip.tsx, bulma-ui/src/components/HoverCard.tsx, bulma-ui/src/components/__tests__/Tooltip.test.tsx, bulma-ui/src/components/__tests__/Tooltip.ssr.test.tsx, bulma-ui/src/components/__tests__/HoverCard.test.tsx, bulma-ui/src/helpers/__tests__/useHoverOpen.test.tsx, bulma-ui/src/components/__tests__/HoverCard.styles.test.tsx
Tooltip uses useHoverOpen for pointer and focus handling, delays, and visibility. The new openOnTap option supports touch activation, and Tooltip does not consume Escape. Tests cover pointer, focus, touch, Escape propagation, portals, hydration, and caller handlers. HoverCard uses the shared chain helper and forwards pointer-over events.
Tooltip pointer bridge
bulma-ui/src/scss/components/_tooltip.scss, bulma-ui/src/components/__tests__/Tooltip.styles.test.tsx
Hidden tooltip content and its arrow let pointer events pass through. Visible content accepts pointer events, and the open wrapper bridges the gap to the trigger. Style tests check pointer behavior and bridge dimensions across positions and sizes.
Examples and interaction documentation
bulma-ui/src/components/Tooltip.stories.tsx, bestax-mcp/data/components/Tooltip.json, bestax-mcp/data/components/HoverCard.json, bestax-mcp/data/catalog.json, docs/docs/api/components/tooltip.md, docs/docs/api/components/hovercard.md
Storybook and Tooltip metadata add a touch-activation example. API documentation describes Tooltip interactions and props, and clarifies HoverCard Escape behavior. The catalog records the updated Tooltip prop and example counts.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Trigger
  participant Tooltip
  participant useHoverOpen
  participant TooltipContent
  Trigger->>Tooltip: Send pointer, focus, or tap event
  Tooltip->>useHoverOpen: Call composed interaction handler
  useHoverOpen-->>Tooltip: Request visibility change
  Tooltip->>TooltipContent: Render updated visibility
Loading

Merge Risk: ⚪ Minimal · up to c33a8

The reviewed interaction changes have no established issue that needs resolution before merging.

Pre-merge checks | Passed 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 10 files. (6 skipped: 6…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly summarizes the main Tooltip changes: hover support, Escape dismissal, and tap-based opening.
Description check Passed The description is complete and detailed. It explains the affected bulma-ui and documentation changes, behavior changes, testing, accessibility context, and deferred work. It does not use the reposito…



✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR








🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR





  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://512bd7e7.bestax.pages.dev

Comment thread bulma-ui/src/components/Tooltip.tsx
Comment thread bulma-ui/src/scss/components/_tooltip.scss
Comment thread bulma-ui/src/components/Tooltip.tsx Outdated
Comment thread bulma-ui/src/components/Tooltip.tsx
Comment thread bulma-ui/src/components/Tooltip.tsx
Comment thread bulma-ui/src/scss/components/_tooltip.scss Outdated
…eir shadow root from the page

React makes onPointerEnter out of the pointerout on the element the
pointer left, and skips the pointerover that follows when its
relatedTarget is an element React renders. When a page portals content
into a shadow root, as the docs site's live examples do, the pointerout
the page gets on the way in names only the shadow host, so the enter
stops there, and the pointerover inside names page content React owns,
so React skips it. A HoverCard or Tooltip in the shadow root never heard
the pointer arrive straight from the page. It did from anywhere inside
the preview, which made the docs look flaky.

useHoverOpen now also takes onPointerOver from the element that holds
the trigger. A pointerover whose relatedTarget is outside that element
and the floating element counts as the pointer arriving, when no enter
already did, and touch still doesn't. One from inside them is the
pointer moving about within, so a card Escape closed still stays closed
until the pointer leaves and comes back. HoverCard and Tooltip wire it
alongside their other handlers.

Leaving works already, since the pointerout inside the shadow root names
the page element itself. Two React roots, nested or side by side, and a
second copy of React for the page get their enter as before.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 3 blocking · 3 advisory

# Severity Area Finding Location
1 🟠 Major Correctness Wrapper-as-triggerRef lets the document capture listener stop Escape before React sees it, so a focusable child's own onKeyDown never runs — a Dropdown inside a Tooltip needs two presses to close its menu bulma-ui/src/components/Tooltip.tsx:141
2 🟡 Minor Correctness pointer-events: auto is inherited by the arrow, whose box is 1–3px taller than the bridge, so it captures clicks on the trigger's leading edge bulma-ui/src/scss/components/_tooltip.scss:117
3 🟡 Minor Correctness Dropping active while the pointer is on the trigger leaves the tooltip hidden until the pointer leaves and comes back (the old code showed it) bulma-ui/src/components/Tooltip.tsx:135
4 🔵 Advisory Accessibility "nothing in the tooltip takes focus" is load-bearing for two hook behaviours, but content is arbitrary ReactNode; with a focusable node Escape drops focus to <body> bulma-ui/src/components/Tooltip.tsx:139
5 🔵 Advisory Accessibility Touch users lose the tooltip entirely — a tap used to show it — with no openOnTap-style opt-in bulma-ui/src/components/Tooltip.tsx:72
6 🔵 Advisory Correctness The bridge spans the tooltip's width rather than the trigger's, and active tooltips carry it permanently, so a band of page content becomes unclickable bulma-ui/src/scss/components/_tooltip.scss:149

Overall: The change is sound and unusually well evidenced — reusing useHoverOpen rather than growing a second Escape/touch/delay implementation is the right call, the three 1.4.13 conditions really are met in the code, and the new SCSS and SSR test files extend what jsdom can see rather than restating it. I ran the Tooltip, styles, SSR, HoverCard and useHoverOpen suites: 236 passing, useHoverOpen at 100%, Tooltip.tsx at 100% lines with only the pre-existing ref guard uncovered. The riskiest part isn't the hook wiring but the two places the shortcut "the wrapper stands in for the trigger" leaks — it hands the hook's Escape-stop to every focusable child (finding 1, the one to look at first, since it breaks composition with this library's own Dropdown), and it makes the hook's focus-restore branch unreachable (finding 4). After that, the two CSS hit-area items: the arrow overhang is a one-line fix, the bridge width is a design call.

Residual risk:

  • Hoverable could still fail if the overflow correction shifts the tooltip far enough that the trigger leaves the content's horizontal span — the bridge is anchored to the content's left and right edges, so it would stop covering the path from the trigger. Partly refuted: --tooltip-arrow-offset is set to the negation of the shift, so the arrow, and with it the trigger's centre, stays inside the content while the shift is at most half the content's width. Open beyond that, which needs a wide tooltip on a trigger within roughly half its width of the viewport edge.
  • Hoverable depends on .tooltip-content::after being unclaimed. That pseudo-element had no rule before this PR, so a consumer stylesheet or a tooltipClassName that uses it silently removes the bridge, and a close delay of zero goes back to closing as the pointer crosses the gap. No check here can see that; the arrow has the same exposure through ::before and always has.
  • Dismissible does not apply to active tooltips — deliberate, documented, and tested, including the listener-leak case. 1.4.13 governs content that hover or focus produced, so this is accurate rather than a gap.
  • A tooltip clipped by an overflow: hidden ancestor still can't be hovered, since Tooltip renders in place with no portal. Pre-existing and untouched by this diff.
  • Unmount, hydration, class-prefix and nested-overlay paths — Modal, Popover, HoverCard — each gained a test and all pass; the chain move is a pure relocation and HoverCard's suite is unchanged and green.

🏄 Tooltip finally learned to let you paddle out onto it and to bail out on Escape — clean reuse of the HoverCard hook instead of hand-rolling a second one. Just watch that little arrow nipping at clicks on the trigger, and the fact that Escape now gets eaten before your children hear it, and this one's a keeper.

The Tooltip's wrapper holds its children, so with focus on any of them
the hook's capture listener stopped Escape at the document. A child's
own Escape never ran: a Dropdown in a Tooltip needed a second Escape to
close its menu, and so did any control with its own key handling.

useHoverOpen takes a consumeEscape option, on by default so HoverCard
keeps its own Escape. Tooltip turns it off: Escape still hides the
tooltip, and the key always carries on to whatever has focus, or to a
Modal or Popover around it, since a tooltip only adds to what's there.
The arrow inherited pointer events from a shown tooltip, and its box is
taller than the gap the bridge covers, so its edge sat over the trigger
and caught clicks meant for it. The bridge already covers the gap, so
the arrow takes no pointer now.
…dropped

An active Tooltip turned down the hook's asks to open, so the hook
settled on "held, and it asked" while the tooltip stayed closed in its
eyes. Dropping active with the pointer on the trigger then left the
tooltip hidden until the pointer left and came back.

The hook now tracks the pointer and focus as they are, and active only
adds to what's rendered. An active tooltip still shows whatever the
pointer, focus and Escape do, and Escape over it still goes on, since a
Tooltip never consumes it.
A touch screen has no hover and a tap's focus isn't keyboard focus, so
a phone or tablet never shows a Tooltip. openOnTap, off by default as
on HoverCard, shows it with a tap on the trigger and hides it on the
next tap, a tap anywhere else, or Escape. It's for a trigger with no tap
action of its own, since the tap still does whatever the trigger does.

The Tooltip page says plainly that by default a tap shows nothing, so
anything a touch user needs can't live only in a tooltip, and shows the
prop on a button with no onClick. Tooltip now also passes a caller's
onPointerDown and onPointerUp through alongside its own.
…d only while open

The bridge that lets the pointer cross to a Tooltip spanned the whole
tooltip, which can be far wider than the trigger, so it caught clicks on
page content beside the trigger. It and the tooltip's pointer events
also stayed on for as long as an active tooltip was shown.

The wrapper now draws the bridge, exactly the trigger's width across the
gap, and a Tooltip takes an is-open class while the pointer, focus or a
tap has it open. Only then do the tooltip and the bridge take the
pointer, so a tooltip only active shows lets it through as it used to.
The gap's size lives on .tooltip-content, out of the wrapper's reach, so
the bridge takes the same defaults for each size.
@allxsmith allxsmith changed the title fix(bulma-ui): let a Tooltip be hovered and dismissed with Escape feat(bulma-ui): let a Tooltip be hovered, dismissed with Escape, and opened by a tap Oct 12, 2026
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify

Every open thread has a reply: fixed, with the commit, or refuted with a reason. This settles them on 6d1ddb9.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://59f6ab5c.bestax.pages.dev

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (verify) — 6 resolved · 0 open

# Severity Area Finding Resolution
1 🟠 Major Correctness Wrapper as triggerRef made the Tooltip swallow Escape for every focusable child ✅ Verified fixed — consumeEscape: false (bulma-ui/src/components/Tooltip.tsx:171)
2 🟡 Minor Correctness The arrow ate clicks on the trigger, sticking out past the bridge ✅ Verified fixed — pointer-events: none (bulma-ui/src/scss/components/_tooltip.scss:101)
3 🟡 Minor Correctness Dropping active under the pointer left the tooltip stuck hidden ✅ Verified fixed — turn-down removed (bulma-ui/src/components/Tooltip.tsx:161)
4 🔵 Advisory Accessibility “nothing in the tooltip takes focus” is an invariant the public API does not enforce ✅ Accepted — recorded in the content TSDoc (bulma-ui/src/components/Tooltip.tsx:32-39)
5 🔵 Advisory Accessibility Touch users lost the tooltip entirely, with no opt-in ✅ Verified fixed — openOnTap prop + docs note (bulma-ui/src/components/Tooltip.tsx:80)
6 🔵 Advisory Correctness The bridge was as wide as the tooltip, and an active tooltip kept it forever ✅ Verified fixed — bridge moved to the wrapper, gated on is-open (bulma-ui/src/scss/components/_tooltip.scss:210)

Overall: This pass settled threads and reviewed no commits. All six of my open threads — the three blocking findings and all three advisories — are resolved against 6d1ddb9c: four were fixed in code, one advisory was fixed beyond what it asked for (openOnTap), and one was accepted on a reason that holds, recorded in the public TSDoc. Nothing is left open, so there is nothing for a human to arbitrate from this reviewer. For a review of the five commits pushed since the original pass, a steer starting deep-review: fresh is what asks for one.

Residual risk: scoped to the threads this pass settled, since a verify pass raises nothing new.

  • Escape no longer consumed (thread 1): refuted as a regression source for HoverCard — a grep for consumeEscape across bulma-ui/src/ shows Tooltip.tsx:171 as the only non-test caller, and consumes: () => latestRef.current.consumeEscape !== false (useHoverOpen.ts:343) leaves every other caller on the consuming path. 393 HoverCard/Modal/Popover/Dropdown tests pass unchanged.
  • The accepted cost of thread 1 — one Escape now hides the tooltip and closes a Modal/Popover around it — is a deliberate behaviour choice, stated in the component TSDoc and pinned by the Modal and Popover tests, not an unhandled path.
  • Bridge variable drift (thread 6): open and disclosed by the author. The bridge reads the Sass defaults because --bulma-tooltip-arrow-size and -margin are declared on .tooltip-content, out of the wrapper’s reach, so a theme overriding those two on .tooltip-content moves the gap but not the bridge. Tooltip.styles.test.tsx:176-186 holds the two equal at every size variant, which catches drift in this repo but cannot see a consumer’s override. The alternative — declaring them on the wrapper too — would change their documented home and the generated variables table, which is a reasonable thing to decline.
  • Empirical basis: 153 tests across Tooltip.test.tsx, Tooltip.styles.test.tsx and useHoverOpen.test.tsx, plus 393 in the neighbouring suites, all green. I could not run a mutation check (reverting the two Tooltip.tsx lines to confirm the new tests fail) — writes to the working tree were not permitted in this session — so the load-bearingness of each fix is established by reading the assertions rather than by watching them fail.

🏄 Six threads, six clean closeouts — the gnarliest one even picked the braver half of the trade-off and wrote it on the board instead of burying it. Paddle this one in, it is glassy.

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

Main is merged in to clear a conflict in the generated HoverCard MCP entry after #1032 (portal container) landed, so c33a894 needs a fresh review behind it. Since the last summary, this PR also carries the fix for HoverCard and Tooltip not opening when the pointer reaches their shadow root from light-DOM page content (an onPointerOver fallback in useHoverOpen), the six thread fixes (consumeEscape, the arrow's pointer events, active under the pointer, the content guidance, Tooltip's openOnTap, and the bridge as wide as the trigger under is-open), and the merge with #1032's portalContainer changes to HoverCard's TSDoc. Review HEAD_SHA in full. Judge generated output through the generator. Post every finding, advisory included, as its own thread.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://05900448.bestax.pages.dev

Comment thread bulma-ui/src/components/Tooltip.tsx Outdated
Comment thread bulma-ui/src/scss/components/_tooltip.scss
Comment thread bestax-mcp/data/catalog.json

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 1 blocking · 2 advisory

# Severity Area Finding Location
1 🟡 Minor Accessibility "stays hidden until the pointer or focus leaves and comes back" is false with openOnTap — a tap reopens it at once; shipped in the .d.ts, the docs Dismissible bullet and the MCP summary bulma-ui/src/components/Tooltip.tsx:92
2 🔵 Advisory Correctness The bridge's size is Sass-fixed while the gap reads --bulma-tooltip-arrow-size/-margin, both advertised themeable on .tooltip-content, so a documented override reopens the Hoverable failure bulma-ui/src/scss/components/_tooltip.scss:196
3 🔵 Advisory Correctness PR body: separate React roots of one React copy were affected by the shadow-root bug — the over-path bail keys off the per-copy fiber key, not the root PR body (anchored on bestax-mcp/data/catalog.json:1107)

Overall: Sound, and the fresh pass over c33a8946 holds up everything the six settled threads claimed. The riskiest piece is the new onPointerOver fallback, and it is the one I spent the most time on: I read React 19's EnterLeaveEventPlugin to confirm the bail condition the comment describes (react-dom-client.development.js:21154-21162), confirmed SimpleEventPlugin dispatches onPointerOver before the enter so the s.pointerIn guard is never what keeps it from double-firing (settle()'s held !== s.held is), and confirmed the two hook tests at useHoverOpen.test.tsx:295-311 are a real proof rather than a restatement — both fire a bare pointerover whose relatedTarget React owns, which is precisely the shape React skips, so the open in the first can only come from the fallback and the second pins the guard against reopening. 267 tests pass across the six suites; useHoverOpen.ts and HoverCard.tsx at 100% on every metric, Tooltip.tsx at 100% lines with only the pre-existing ref guard at :192 uncovered; typecheck:tests clean; and the generated MCP index and docs tables match what the generators derive from the TSDoc (propCount 13 to 14, exampleCount 10 to 11, and the overview region correctly stays one sentence, as HoverCard's does). Look at finding 1 first — it is a one-clause deletion, and it is the asymmetry this very PR corrected on HoverCard's side.

Residual risk:

  • relatedTarget === null treated as the pointer arriving (alt-tab back, the pointer re-entering from browser chrome, a hit target removed under a stationary pointer) — refuted: React's bail is gated on relatedTarget being truthy (react-dom-client.development.js:21154-21162), so with a null one React dispatches the enter itself and the outcome is identical with or without the fallback. The fallback neither widens nor narrows that path.
  • A boundary event reopening something Escape just closed, while the pointer never left — refuted both ways. Tooltip's content stays mounted, so the floating-element guard catches a pointerover whose source is the tooltip, and is-open plus the bridge drop together so the new hit target is page content outside the wrapper. HoverCard's card unmounts, but it is anchored off the trigger by marginGap, so the element that comes under the pointer is page content, not the holder — and the in-holder variant is pinned by HoverCard's "stays closed after Escape while the pointer moves between the trigger's own children".
  • A consumer's children portaling elsewhere would make a pointerover in that portal read as the pointer arriving — refuted as new: React's enter/leave already walks the fiber tree through portals, so onPointerEnter fires for that case today; the fallback adds no path there.
  • Hoverable can still fail if the overflow correction shifts the content so far that the trigger leaves its horizontal span, since the bridge is anchored to the trigger and the content is not — open, and unchanged by this diff. Bounded as before: --tooltip-arrow-offset is set to the negation of the shift, keeping the arrow, and with it the trigger's centre, inside the content, and the shift is capped by edgePadding.
  • A stale requestAnimationFrame in the placement effect can apply a shift after the tooltip hid, and compound on the next show — pre-existing; the effect and its deps are byte-identical to main, and the shift is invisible while hidden. Listed only because isVisible now changes on pointer events that did not previously drive it.
  • A mouse entering and leaving a tap-opened tooltip closes it on the hover timer, which UseHoverOpenOptions.openOnTap reads as not happening — pre-existing from #1030, and Tooltip's own prop description says "Mouse and pen work the same either way", so the public contract holds.

🏄 Dude, this one's been through the wash cycle twice and came out cleaner — the hover bridge actually catches you now, Escape stops body-blocking the key, and that shadow-root fix is the kind of gnarly React-internals paddle-out most people would have just shrugged at. One stale sentence about taps to tidy up and it is good to go.

…nTap

Tooltip's summary and its Dismissible bullet said a tooltip Escape hid
stays hidden until the pointer or focus leaves and comes back. With
openOnTap the next tap on the trigger shows it again, so both now end
the way HoverCard's do, and a test pins the tap.
…ross it

The bridge that carries the pointer onto a Tooltip is sized from the
arrow variables' Sass defaults, while the gap reads the themeable
--bulma-tooltip-arrow-size and --bulma-tooltip-arrow-margin. Raising
either opens a band the bridge doesn't cover. The trade-off stays, and
the closeDelay TSDoc, the Tooltip page's CSS & Sass Variables section
and the theming skill now say to give such a tooltip a closeDelay long
enough to cross the wider gap.
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify

Every open thread has a reply: fixed, with the commit, or refuted with a reason. This settles them on f940611.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://338ca616.bestax.pages.dev

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (verify) — 3 resolved · 0 open

# Thread Severity Finding Outcome
1 bulma-ui/src/components/Tooltip.tsx (TSDoc summary) 🟡 Minor · Accessibility "stays hidden until the pointer or focus leaves and comes back" was false with openOnTap ✅ Verified fixed — 62e5281
2 bulma-ui/src/scss/components/_tooltip.scss:196 🔵 Advisory · Correctness A themed arrow gap outruns the compile-time hover bridge ✅ Verified fixed — 41975da
3 bestax-mcp/data/catalog.json:1107 (PR body) 🔵 Advisory · Correctness Body claimed separate React roots were never affected by the shadow-root bug ✅ Verified fixed — PR description
What I checked for each

1 — openOnTap after Escape. Tooltip.tsx:93-94 and tooltip.md:310 now both end
"…or, with openOnTap, until the next tap on the trigger", matching the HoverCard wording
rather than dropping the clause. The regenerated index carries it in three places in
bestax-mcp/data/components/Tooltip.json (summary, the sub-component summary, and
accessibility). The new test shows again on the next tap after Escape hid it pins the
behaviour the sentence now describes; jest --testPathPatterns "Tooltip|useHoverOpen|HoverCard"
is 274 passed, 8 suites.

2 — themed arrow gap vs. the bridge. The trade-off stays, and the caveat now sits in all
three places a reader with a theme in hand would be looking: the closeDelay TSDoc
(Tooltip.tsx:67-73, which lands in the props table at tooltip.md:359, the published
.d.ts, and Tooltip.json:169), a paragraph after the generated CSS and Sass Variables table
(tooltip.md:396-400), and the theming skill Tooltip entry immediately above the instruction
to override those vars on .tooltip-content (css-variables.md:481-483). Each statement
checks out against the code: the gap is
calc(getVar("tooltip-arrow-size") + getVar("tooltip-arrow-margin")) (_tooltip.scss:136),
the bridge is calc($arrow-size + $tooltip-arrow-margin) (:196) — Sass defaults only. That
was the second of the two fixes the thread offered, so it is settled.

3 — PR body. The "Pointer coming into a shadow root" paragraph now stops at "Leaving
already worked." The unsupported clause is gone, so the stated blast radius of the fix no
longer rules out a multi-root app.

Overall: This pass settled threads and reviewed no commits. All three remaining findings
were documentation-accuracy items and all three are genuinely addressed — the two code-adjacent
ones are now pinned by a test and by three consistent prose surfaces that match what the SCSS
and the hook actually do. Nothing is left open; a human reviewing this now only needs to decide
whether the documented theme-override trade-off on the hover bridge (thread 2) is the right
long-term answer, or whether a follow-up should move the arrow vars onto .tooltip.

Residual risk: not re-assessed — a verify pass settles open threads and raises no new
findings. The residual-risk analysis from the earlier full review stands; a reader who wants
the current code reviewed afresh should steer deep-review: fresh.

🏄 Dude, every last thread got paddled in and parked on the sand — three for three, tests
green, and the prose finally tells the same story the code does. Clean set, no dropped waves.
Good to go.

@github-actions github-actions Bot added the review-converged Deep review converged: no blocking findings, all review threads resolved, checks green label Oct 12, 2026
@allxsmith
allxsmith merged commit 52aeeda into main Oct 12, 2026
72 checks passed
@allxsmith
allxsmith deleted the fix/tooltip-wcag-1413 branch October 12, 2026 23:06
bestax-release-bot Bot pushed a commit that referenced this pull request Oct 12, 2026
# [5.30.0](https://github.com/allxsmith/bestax/compare/@allxsmith/bestax-bulma@5.29.0...@allxsmith/bestax-bulma@5.30.0) (2026-10-12)

### Bug Fixes

* **bestax-migrate:** carry size to Buttons, Tags and Message ([#1038](#1038)) ([7549264](7549264))
* **bulma-ui:** give TimeInput's AM/PM column a real name for screen readers ([2b1122f](2b1122f))
* **bulma-ui:** keep a picker label's default when its override is undefined ([aa88e50](aa88e50))

### Features

* **bulma-ui:** add ContextMenu, a menu that opens on right-click or long press ([#1036](#1036)) ([85a3e77](85a3e77))
* **bulma-ui:** add ToggleButton, a button that stays pressed ([#1041](#1041)) ([582aff0](582aff0))
* **bulma-ui:** let a Tooltip be hovered, dismissed with Escape, and opened by a tap ([#1033](#1033)) ([52aeeda](52aeeda))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.30.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

bestax-release-bot Bot pushed a commit that referenced this pull request Oct 12, 2026
## [2.25.3](https://github.com/allxsmith/bestax/compare/bestax-migrate@2.25.2...bestax-migrate@2.25.3) (2026-10-12)

### Bug Fixes

* **bestax-mcp:** release with every bulma-ui release so the published index keeps up ([#1027](#1027)) ([618e483](618e483))
* **bestax-migrate:** carry size to Buttons, Tags and Message ([#1038](#1038)) ([7549264](7549264))
* **bulma-ui:** accept material-symbols 0.47 in the peer range ([539cca2](539cca2))
* **bulma-ui:** drop a Field label's for when nothing it holds takes it ([#1020](#1020)) ([3604c14](3604c14))
* **bulma-ui:** give TimeInput's AM/PM column a real name for screen readers ([2b1122f](2b1122f))
* **bulma-ui:** keep a Notification's text clear of its close button ([#1017](#1017)) ([91ebb63](91ebb63))
* **bulma-ui:** keep a picker label's default when its override is undefined ([aa88e50](aa88e50))
* **bulma-ui:** keep Theme's component-variable check out of production ([537d90d](537d90d))
* **bulma-ui:** let a caller's aria-label or aria-labelledby name both range Slider thumbs ([047ff7c](047ff7c)), closes [#974](#974)
* **bulma-ui:** name a composed DateRangeInputBase's group from a Field label ([75aa6cb](75aa6cb))
* **bulma-ui:** name a range Slider's thumbs from a hand-wired Field label's id ([355065b](355065b))
* **bulma-ui:** name Autocomplete's list from a hand-wired Field label's id ([ad46c06](ad46c06))
* **bulma-ui:** name Autocomplete's suggestion list from a surrounding Field's label ([190183f](190183f)), closes [#998](#998)
* **bulma-ui:** name each range Slider thumb from its label ([6b962e3](6b962e3)), closes [#981](#981)
* **bulma-ui:** name the date and time picker bases from a Field label ([e7ae797](e7ae797))
* **bulma-ui:** say a label placed in Field.Label reaches no thumbs or list ([07c2a99](07c2a99))
* **bulma-ui:** say FieldLabel renders the label column, not a label ([2a6ab10](2a6ab10))
* **bulma-ui:** say the label prop needs wiring to reach thumbs or list in an inner Field ([9b33d8d](9b33d8d))
* **bulma-ui:** say which controls a hand-wired Field label's id reaches ([514127f](514127f))
* **bulma-ui:** warn when Theme is given a variable Bulma sets on the component ([bb8f9b8](bb8f9b8)), closes [#1021](#1021)
* **create-bestax:** announce the starter's notifications through a status region ([91dbfd5](91dbfd5))
* **create-bestax:** give each starter notification a status region of its own ([f1ef4df](f1ef4df))
* **create-bestax:** keep focus on the counter when Reset disables itself ([733e505](733e505))
* **create-bestax:** keep the reason for the stylesheet order in every scaffolded entry file ([f4591e0](f4591e0))
* **create-bestax:** pin material-symbols to the newest range bestax-bulma accepts ([ff4f820](ff4f820))
* **create-bestax:** return focus when the starter's notification closes, and tighten guards ([67e3678](67e3678))
* **create-bestax:** say which helper props render nothing under the no-helpers flavors ([b29985b](b29985b))
* **create-bestax:** stop the no-helpers CLAUDE.md offering helper props as the way out ([a96c203](a96c203))
* **create-bestax:** stop the vite-ts build shadowing its config, and fix the starter page ([1dd964d](1dd964d))
* **docs:** give the mixin note the modifier caveat its lead carries ([1427f08](1427f08))
* **docs:** keep the modifier caveat for mixin variables in the MCP index ([e227663](e227663))
* **docs:** name the one edge of the rule for which variables Theme reaches ([b33ebfa](b33ebfa))
* **docs:** qualify the guide and the theming skill on what Theme reaches ([9c2347b](9c2347b))
* **docs:** read only a component's own mixin as its home ([f40cd2c](f40cd2c))
* **docs:** refuse a CSS-variable scope the page cannot fully word ([c815602](c815602))
* **docs:** say where component-scoped Bulma variables have to be set ([771d5bd](771d5bd)), closes [#1021](#1021)

### Features

* **bulma-ui:** add ContextMenu, a menu that opens on right-click or long press ([#1036](#1036)) ([85a3e77](85a3e77))
* **bulma-ui:** add HoverCard, a card that opens on hover or keyboard focus ([#1030](#1030)) ([0eb41df](0eb41df))
* **bulma-ui:** add ToggleButton, a button that stays pressed ([#1041](#1041)) ([582aff0](582aff0))
* **bulma-ui:** let a Tooltip be hovered, dismissed with Escape, and opened by a tap ([#1033](#1033)) ([52aeeda](52aeeda))
* **bulma-ui:** let ConfigProvider set where portaled content goes ([#1032](#1032)) ([5e928ae](5e928ae))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

bestax-release-bot Bot pushed a commit that referenced this pull request Oct 12, 2026
## [1.14.7](https://github.com/allxsmith/bestax/compare/bestax-mcp@1.14.6...bestax-mcp@1.14.7) (2026-10-12)

### Bug Fixes

* **bestax-migrate:** carry size to Buttons, Tags and Message ([#1038](#1038)) ([7549264](7549264))
* **bulma-ui:** give TimeInput's AM/PM column a real name for screen readers ([2b1122f](2b1122f))
* **bulma-ui:** keep a picker label's default when its override is undefined ([aa88e50](aa88e50))

### Features

* **bulma-ui:** add ContextMenu, a menu that opens on right-click or long press ([#1036](#1036)) ([85a3e77](85a3e77))
* **bulma-ui:** add ToggleButton, a button that stays pressed ([#1041](#1041)) ([582aff0](582aff0))
* **bulma-ui:** let a Tooltip be hovered, dismissed with Escape, and opened by a tap ([#1033](#1033)) ([52aeeda](52aeeda))
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released review-converged Deep review converged: no blocking findings, all review threads resolved, checks green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant