feat(lab): open InfoTip on tap - #4408
Open
AKnassa wants to merge 1 commit into
Open
Conversation
InfoTip's own RFC accessibility section requires "tap toggles the tooltip", but the shipped component wired only keydown, blur and mouseleave. Core does not fill the gap either — `useTooltip` deliberately bails out of hover-open under `(hover: none)` and gates focus-open on `:focus-visible`, which a tap does not satisfy — so on a touch device the InfoTip could not be opened at all. Replace the two-state dismissed flag with `auto` / `pinned` / `dismissed`, driving the controlled `isOpen` channel the component already owns. Clicking pins unconditionally rather than toggling on perceived visibility, so the gesture does not depend on whether the 200ms hover delay has elapsed; a pinned tip survives pointer-leave and closes on a second click, Escape, or focus leaving. Closing must route through `dismissed` rather than back to `auto`, since `auto` only stops overriding Tooltip and cannot close an open layer. Outside-click dismissal is deliberately not included: an outside tap fires both pointerdown and blur, and if those batch the net state is `auto` — stuck open, the worst outcome on the very devices this fixes. Refs facebook#3349
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
PR Analysis ReportNo new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | View full report |
AKnassa
marked this pull request as ready for review
July 30, 2026 05:14
AKnassa
requested review from
cixzhang,
ejhammond and
imdreamrunner
as code owners
July 30, 2026 05:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
InfoTipcan now be opened by tapping it. Before this, on a touch device it could not be opened at all.Why
The RFC's own accessibility section requires "tap toggles the tooltip". The component merged without it, wiring only keyboard, blur and mouse-leave.
Core doesn't fill the gap either — and it's worse than "hover just doesn't fire".
useTooltipdeliberately bails out of hover-open under(hover: none), and focus-open is gated on:focus-visible, which a tap does not satisfy. So on a phone the info icon was inert.What changed
The two-state dismissed flag becomes
auto/pinned/dismissed, driving the controlledisOpenchannel the component already owns.mouseenterhas already fired it wouldn't re-open. Pinning also makes the gesture independent of whether the 200ms hover delay had elapsed.dismissed, never back toauto—autoonly stops overriding Tooltip and cannot close an open layer, so returning to it would leave the tip stuck open. That's the sharpest edge in the change and is commented in-file.Scope question for a reviewer
A reviewer arguing this belongs in
Tooltipinstead has a strong case, and I'd rather say so than not. Core already owns the touch policy — it's what suppresses hover under(hover: none)— and it left no escape hatch, so every future icon-button-plus-Tooltip pairing inherits the same dead end and will re-invent this state machine.Against that:
Tooltipis core and consumed everywhere, a click path there could collide with triggers that have their own click semantics, and it needs an API decision adjacent to this RFC's unresolved Option A/B question. So the lab-scoped version is the right first move and is cheap to delete if redirected — but treat it as a candidate for graduation toTooltip, not the final home.Deliberately not included
Outside-click dismissal. An outside tap fires both
pointerdownandblur; if those batch, the net state lands onauto— stuck open, which is the worst possible outcome on exactly the devices this fixes. Tap-to-toggle alone satisfies the RFC and keeps one handler per event.The RFC's open questions (Option A vs B, toggletip semantics, lab→core graduation) are untouched.
How to check it
pnpm vitest run packages/lab/src/InfoTip/ packages/core/src/Tooltip/— 29 tests, green, no collateral on Tooltip. Full repo suite run alone matches baseline exactly.The tests were mutation-tested, and that caught a fake: the original "keeps pinned" test passed even when
handleMouseLeavewas mutated to unconditionally reset, becauseautomaps toisOpen: undefinedand core no-ops on that. An extra test now kills that mutant; all four mutants die.Not verified on a real device — jsdom does no layout and dispatches no real touch events, so
fireEvent.clickis synthetic. Every touch claim here is reasoned from core's source, not measured. One known limit: while pinned, core's document-level Escape listener is disabled, so Escape relies on the trigger having focus — Safari and Firefox on macOS don't focus buttons on click, so there a second click closes it but Escape won't. A document-level listener was deliberately not added, since one keypress would then close both the tip and any enclosing dialog.No changeset:
packages/labis private, and the changeset check rejects private packages outright.Refs #3349