just_tooltip 0.5.1
just_tooltip: ^0.5.1 copied to clipboard
A lightweight, customizable Flutter tooltip with flexible placement, hover, tap & keyboard triggers, screen-reader support, programmatic control, and RTL support.
0.5.1 #
Added #
-
A keyboard user can show and dismiss a tooltip (#92). Neither was possible: a focused child showed nothing, and no key closed a shown tooltip. Flutter's own
Tooltipdoes neither; this follows WCAG 2.1 SC 1.4.13 (Content on Hover or Focus).enableFocus: trueshows the tooltip while keyboard focus is inside the child — focus that arrived by a key such as Tab, not by a click, a tap orrequestFocus(). Focus may move on into the tooltip's content and keep it open. It behaves as a hover does: it waits forwaitDuration, nested only the innermost tooltip shows, it needs content, and losing focus hides it. A pointer hovering an enclosing tooltip elsewhere wins. A focused tooltip hidden by another one comes back when that one hides. Off by default.- Escape closes the most recently shown tooltip, one per press, however it was opened,
controller.show()included; with nothing shown, it cancels a show still waiting outwaitDuration. A dismissed tooltip stays closed until its trigger leaves and returns — the pointer leaves the child and comes back, or focus does. A tap still opens it. - When focus is inside that tooltip, Escape is consumed, so a dialog behind it closes on the next press. A tooltip shown with focus elsewhere — a hover — closes along with the dialog: Flutter gives a key to both its keyboard handlers and the focused widgets, and only the second can stop it.
showDurationandinteractive: falsenow say in their documentation that they leave the criterion; both defaults satisfy it.
-
A screen reader hears a tooltip (#69). The tooltip's text is put on its child's semantics node, as Flutter's
Tooltipdoes, so TalkBack and VoiceOver read it with the child without opening anything; showing the tooltip also announces it (honoured on Android). Nothing was heard before:TooltipTap.claimonly exposed a tap action, andTooltipTap.observenot even that.- The text is the new
semanticsTooltipwhen given, otherwisemessage. AtooltipBuildertooltip is heard only throughsemanticsTooltip. - A tooltip is heard only when it would show and its text is non-empty: an empty bubble (
hideOnEmptyMessage: false) is shown and not heard, andsemanticsTooltipnever makes a tooltip show. excludeFromSemantics: truekeeps it unheard — for a child that already says what the tooltip says, such as a cell whose tooltip repeats its text.- Nested, only the innermost heard tooltip is heard; an outer one is silent while one inside its child is heard. Left alone, Flutter would keep the outer text and drop the inner one. An inner tooltip a screen reader cannot reach — under
ExcludeSemantics,Offstageor a hiddenIndexedStackchild — silences it too; setexcludeFromSemanticson that one (#94). - A screen reader still cannot open a tooltip. Under
TooltipTap.observethe screen-reader tap goes to the surface around the child, as the pointer tap does; underTooltipTap.claimit opens the tooltip and the surface does not receive it, as with a pointer.
- The text is the new
0.5.0 #
Changed #
- Minimum Flutter is now
3.32.0(Dart3.8.0), up from3.13.0(#83). The tooltip is positioned during layout throughOverlayPortal.overlayChildLayoutBuilder, so a shown tooltip follows a moving child — a scroll, a resize, an insertion above it — in the same frame. It was re-aimed after each frame and trailed by one: measured, a tooltip shown by its controller in a list scrolled 40 px per frame stood 40 px off its child on every scrolling frame.
Added #
tap: TooltipTap.observe— a tap trigger that does not claim the gesture (#66).enableTapputs a tap recognizer in the gesture arena, and being innermost it wins, so a tooltip on part of a tappable surface took the tap from that surface: in a folder view, a tap on a row's label opened the tooltip but never selected the row. Withobservethe tooltip toggles and the ancestor's tap fires.- The tooltip takes part in the gesture arena but never wins it: it toggles when a primary-button press is released within the touch slop (
gestureSettings.touchSlop, else 18 px) and no other gesture has won by then. An ancestor's long press, drag or scroll — including a mouse drag of a few pixels — leaves the tooltip alone. - Gestures decided only after the release still see the tooltip toggle: under an ancestor's double tap the tooltip opens on the first tap and closes on the second, and a tap handler inside the child fires along with the tooltip.
- Screen readers get no tap action in this mode;
claimkeeps the one it had. - Under
TooltipAnchor.pointerthe tooltip anchors at the tap, as it does with a claimed tap. - Nested, the innermost tooltip with content takes the tap, whatever mode each one uses (#68). Every tooltip that saw a tap used to toggle, and the outer one opened last and dismissed the inner one, so an inner
observetooltip lost to its ancestor. An ancestor's own tap handlers are untouched.
- The tooltip takes part in the gesture arena but never wins it: it toggles when a primary-button press is released within the touch slop (
Fixed #
- A tooltip inside another tooltip's content no longer closes its host (#84). Both were on the app-global registry, so showing the inner one dismissed the host, and the inner one went with the host's content — measured: hovering, tapping or touching a tooltip in a shown host's content left neither on screen. A host now gives its content a registry of its own; tooltips there are still one at a time among themselves, and an explicit
registrystill wins. - A non-interactive hover tooltip too large for either side of its target no longer flickers (#65). The clamp laid the body over the target and the pointer; the body took the pointer, the child reported an exit, the tooltip hid, the child was uncovered, and it showed again — measured in a 390-wide view with a 300-wide
left/rightbody: shown on every other 50 ms sample. Withinteractive: falsethe body is now transparent to the pointer, as Flutter'sTooltipis for amessage.- A tap on an
interactive: falsetooltip now reaches the widget beneath it, and clickable content inside such a tooltip can no longer be clicked. Useinteractive: true(the default) for a tooltip with content to reach.
- A tap on an
- A tooltip now follows the
Directionality,Theme,DefaultTextStyleandMediaQueryaround it (#72). It was built in theOverlay's context, so anything set between theOverlayand the tooltip was ignored — measured: an RTLDirectionalityaround the tooltip left atop/startbody unmirrored (350–574 for a target at 350–450), a localThemeand a text scale of 2 never reachedtooltipBuilder. The tooltip is now drawn through anOverlayPortal.- A tooltip under a local
Theme,Directionality,DefaultTextStyleorMediaQuerynow draws with it. If the tooltip should keep the app-level look, set those intooltipBuilderor move the tooltip outside them. - A tooltip inside another tooltip's content still has no ancestor tooltips: it does not take part in nesting suppression.
- A tooltip under a local
- In RTL, the arrow of a
toporbottomtooltip alignedstartorendnow points at the target (#70). The body was mirrored but the arrow was not, so it was placed from the wrong side and pointed past the target's edge — measured on a 100 px target, 200 px tooltip: 62 px beyond it.centerand the*TargetCenteralignments were unaffected. - A positive
crossAxisOffsetnow moves anendTargetCentertooltip inward, as it does forend(#73). It moved it outward — on a 100 px target with a 120 px tooltip,crossAxisOffset: 10put the body's edge 10 px past the target's instead of 10 px inside it. The arrow still pointed at the target's centre. This includesstartTargetCenterin RTL, which resolves toendTargetCenterfortopandbottom.- A value tuned against the old direction now shifts the other way; flip its sign.
Deprecated #
enableTap, replaced bytap: TooltipTap.claim, which behaves identically.tapis one value with no invalid combinations; passing both is an assertion error.- Code still passing
enableTapgets adeprecated_member_useinfo, which failsflutter analyzeby default. ReplaceenableTap: xwithtap: x ? TooltipTap.claim : TooltipTap.none, orTooltipTap.observewhere the tooltip sits inside something tappable.
- Code still passing
0.4.4 #
Fixed #
- A tooltip that can never show no longer suppresses the tooltips around it (#46). With
hideOnEmptyMessage(the default) an emptymessagemeans "draw nothing" — but the suppression was claimed fromMouseRegion.onEnter, before that was known, so hovering such a tooltip nested inside another showed nothing at all: not the inner one, not the outer one.- Reachable from ordinary data. A cell tooltip whose text is
''for "no description", inside a row-level tooltip, silently kills the row's. - Two packages had independently grown the same local guard rather than report it, which is what a trap in the upstream default looks like.
- Reachable from ordinary data. A cell tooltip whose text is
- A tooltip now follows its
messagewhile it is already on screen or under a resting pointer. Content that arrives makes it appear; content that leaves makes it disappear at once, bypassingshowDurationas suppression already did. Previously neither was noticed until the pointer left and returned.- This holds however the tooltip was opened. A tooltip shown by
controller.show()has no hover intent to lose, so an emptiedmessageused to leave it standing — with the overlay now following the widget, that would have drawn an empty bubble in defiance ofhideOnEmptyMessage. Showing is gated on content; hiding now is too.
- This holds however the tooltip was opened. A tooltip shown by
- A shown tooltip now follows its own configuration (#47). Changing
message,tooltipBuilder,theme,directionoralignmentwhile the tooltip is on screen had no effect: the overlay entry renders all of them live, but nothing ever asked it to rebuild. It now rebuilds on the next frame after any widget update.- A
tooltipBuilderthat reads mutable state at call time —(_) => Text(_message)— appeared to work, because the stale closure returned the new value. Capture the value instead and both paths were equally stale. Anyone verifying this should use the capturing form.
- A
Changed #
- Nesting Suppression now reads "the innermost tooltip under the pointer that has something to draw wins". Having nothing to draw is no longer enough to take an ancestor's place.
hideOnEmptyMessage: falseasks for the empty bubble, so such a tooltip draws, and suppresses exactly as before.
0.4.3 #
Fixed #
- An
interactivetooltip no longer dies for good when the cursor returns from the tooltip body to its child (#43). Leaving the tooltip arms the 100 ms Hover Bridge; re-entering the child did not cancel it, so it fired unseen, started a fade-out, and nothing revived it — a pointer that never left the child sends no furtheronEnter. The tooltip vanished about 250 ms after the cursor came home and stayed gone.- The reverse-catch could not help: the show request arrives while the animation is still running forward, and the reverse only begins 100 ms later, when nobody is asking to show.
- Invisible at the moment it happens. The bridge fires while the tooltip is still on screen; it disappears 150 ms afterwards, a quarter second removed from its cause.
TooltipAnchor.pointermakes it easy to hit, since the tooltip is drawn beside the cursor.
0.4.2 #
Changed #
- Minimum Flutter is now
3.13.0(Dart3.1.0), up from3.10.0(#38). The ancestor clip walk below readsRenderObject.parent, which wasAbstractNode?— a type with nodescribeApproximatePaintClip— until Flutter 3.13.
Fixed #
- Behaviour change.
TooltipAnchor.childnow targets the visible part of the child rather than its whole rect (#33). A child wider than the ancestor clipping it — a row inside a horizontal scroll viewport, say — had its tooltip aimed at a centre nobody could see. Measured: a 900px child in a 400px viewport placed its tooltip atx = 450, entirely outside the scroll view, with the cursor atx = 200.screenMarginnever helped: it confines the tooltip to theOverlay, usually the whole app, which an off-screen anchor satisfies.- A viewport reports a clip whenever
clipBehavioris notClip.none, so anyJustTooltipinside aListVieworSingleChildScrollViewmay shift — always toward the visible part. An unclipped child is unaffected. TooltipAnchor.pointerwas never affected, which is why both known downstreams had independently adopted it as a workaround.- A child clipped away entirely — reachable only via
controller.show(), since a hovering pointer proves a visible part exists — anchors at the clip edge the child lies beyond rather than refusing to show. Hiding is reserved for losing a target you had: see the tracking entry below.
- A visible tooltip now tracks its child (#35). The target was resolved once, when the tooltip appeared, so a scroll, a resize, a layout animation, or an insertion above the child left the tooltip pointing where the child used to be. It now re-aims after any frame that moves the child, and hides once a child it was pointing at is clipped away entirely — nothing to point at, no tooltip. Hiding is a transition, not a state:
controller.show()against a child that was already out of sight anchors at the clip edge rather than refusing, keeps tracking, and re-aims onto the child as soon as it scrolls into view. - The target rect now follows the child's paint transform.
Transform.scalebetween the child and theOverlaypreviously produced a target whose origin was transformed but whose size was not, so a2.0-scaled 100×50 child yielded a 100×50 target at the origin and aTooltipDirection.bottomtooltip drawn on top of the child instead of below it.
0.4.1 #
Added #
JustTooltipTheme.bare()(#30). A theme that draws no background, padding, shadow, border, or arrow — for atooltipBuilderwhose widget already draws its own surface, leaving the tooltip to contribute positioning and nothing else. Previously this meant zeroingbackgroundColor,padding, andelevationby hand at every call site, where omitting any one of them silently reintroduced chrome.
0.4.0 #
Added #
JustTooltip.anchorandTooltipAnchor(#21).TooltipAnchor.pointerkeeps the child as the hover region but anchors the tooltip at the cursor — for a child much wider than the pointer's neighbourhood (a table row, a wide card), whose centre is nowhere near where the user is looking. Defaults toTooltipAnchor.child, the existing behaviour.- The anchor is captured when the tooltip is shown and does not follow the pointer, so
interactivetooltips stay reachable. - Tap-triggered tooltips anchor at the tap; a touch fires no hover events, so the tap-down supplies the position.
- A programmatic
controller.show()with no pointer present falls back to the child's rect. - Against a point there are no target edges to align to, so
alignmentsays which of the tooltip's own edges lands on the pointer.
- The anchor is captured when the tooltip is shown and does not follow the pointer, so
Changed #
- Behaviour change for nested tooltips. A
JustTooltipcontaining the pointer now suppresses every enclosingJustTooltip, regardless of registry. If you relied on a nested tooltip and its ancestor being visible together (only reachable by giving them separateTooltipRegistryinstances), that no longer happens. Suppression gates hover only — a programmaticcontroller.show()is unaffected.
Fixed #
-
Nested
JustTooltips now reliably show only the innermost tooltip under the pointer (#22). Previously this held only by coincidence — the "one tooltip at a time" registry dismissed the ancestor after it had shown — and broke in three ways:- an ancestor's
waitDurationtimer would fire while the pointer rested on a nested child, replacing the inner tooltip with the outer one; - moving from a nested child back onto its ancestor showed nothing at all, because the ancestor's hover region was never exited and so never re-entered;
- tooltips scoped to separate
TooltipRegistryinstances showed both at once.
- an ancestor's
-
The tooltip is now positioned against its target in the coordinate space of the
Overlayit is laid out in, rather than the window's (#24). The two coincide for a full-windowMaterialApp, so nothing changes there — but a tooltip under a nestedNavigator, an insetOverlay, or an embedded Flutter view was displaced by the Overlay's offset, and its direction flipping andscreenMarginclamping were measured against the wrong bounds.
Internal #
- Pointer facts (
_pointerInside, and the pointer's last position) are now retained and a hover intent boolean derived from them, coalesced into a microtask and handed toTooltipVisibilityScheduleron transition only. This makes hover behaviour independent of Flutter'sMouseTrackerdispatch order. See ADR-0003. The one collaborator that must observe a hover transition synchronously — the tooltip body'sonEnter, which cancels the hover bridge — flushes the pending recomputation first. TooltipVisibilitySchedulerandTooltipRegistryare unchanged.JustTooltipPositionDelegate.targetRectis documented as being in theOverlay's coordinate space; the child rect and the pointer anchor both arrive in it.- A second example entry point (
example/lib/spike_wide_row.dart) exercises pointer anchoring, nested suppression, an insetOverlayand an interactive tooltip together. - Test coverage: 125 tests. Every test added this release was mutation-checked — reverting the code it covers must make it fail.
0.3.0 #
Breaking #
JustTooltipControlleris no longer aChangeNotifier. It is now an attach-based command source (modeled on Flutter'sOverlayPortalController):show()/hide()/toggle()drive the attached tooltip, which is the single source of truth for visibility.- removed the
shouldShowgetter — useisShowing(reflects the tooltip's live state) instead - removed
addListener/removeListener/dispose(ChangeNotifierAPI) — observe visibility via the widget'sonShow/onHidecallbacks - removed the internal
resetShouldShow()
- removed the
- Narrowed the public API —
JustTooltipPositionDelegate,TooltipShapePainter, andJustTooltipOverlayare no longer exported (they are internal implementation details). The public surface is nowJustTooltip,JustTooltipController,JustTooltipTheme,TooltipRegistry, and the enums.
Added #
- An optional
registryparameter andTooltipRegistryclass to scope the "one tooltip visible at a time" policy — pass a sharedTooltipRegistry()to a group of tooltips to isolate them (or a test); omitting it keeps the app-global default.
Fixed #
controller.show()called before theJustTooltipmounts is now honoured (the queued show is applied once mounted); previously it was ignored.slideandfadeSlideanimations now enter from the auto-flipped (resolved) direction — a tooltip that flips (e.g.top→bottomnear a screen edge) slides in from the side it actually appears on, not the originally-requested one.
Internal #
- Extracted the hover/auto-hide timing into a dedicated
TooltipVisibilityScheduler(timer cancellation collapsed into one place). - Extracted the 7 show/hide animations into a dedicated
TooltipTransitionsmodule. - Moved the single-visible-tooltip policy out of a static
Setinto the injectableTooltipRegistry. - Test coverage raised to ~97% (scheduler, transitions, registry, controller, painter, and hover integration).
0.2.5 #
- feat add
TooltipAlignment.startTargetCenterandendTargetCenteralignments where the arrow dynamically points to the center of the target widget - example add
Top-StartTargetandTop-EndTargetquick presets to playground
0.2.4 #
- feat add
hideOnEmptyMessageparameter to suppress tooltip whenmessageis empty (default:true) - example add
hideOnEmptyMessagetoggle to Content section in playground
0.2.3 #
- fix
borderColornot visible on non-arrow tooltips (showArrow: false) due toMaterialbackground coveringDecoratedBoxborder
0.2.2 #
- fix rename
TooltipPositionDelegate→JustTooltipPositionDelegateto resolve name conflict with Flutter SDK'sTooltipPositionDelegate(introduced in Flutter 3.32)
0.2.1 #
- feat add
TooltipAnimationenum with 7 animation types:none,fade,scale,slide,fadeScale,fadeSlide,rotation - feat add
animationandanimationCurveparameters for animation type and curve selection - feat add
fadeBegin,scaleBegin,slideOffset,rotationBeginparameters for fine-tuning animations - docs add Animation section to README with usage examples and API reference
- example add Animation section with type/curve dropdowns and parameter sliders
0.2.0 #
- BREAKING extract 12 visual styling parameters into
JustTooltipThemeclassbackgroundColor,borderRadius,padding,elevation,boxShadow,borderColor,borderWidth,textStyle,showArrow,arrowBaseWidth,arrowLength,arrowPositionRatioare now accessed viathemeparameter- Migration: wrap style params in
theme: JustTooltipTheme(...)
- feat add
JustTooltipTheme.copyWith()for easy theme derivation - feat
JustTooltipThemeis a reusable data class with==/hashCodesupport
0.1.7 #
- fix controller
show()not working after tooltip was dismissed by hover-out or auto-hide
0.1.6 #
- feat add
showArrowparameter with unified path rendering (arrow integrated into tooltip shape) - feat add
arrowBaseWidthandarrowLengthparameters for arrow size customization - feat add
arrowPositionRatioparameter to control arrow placement along the tooltip edge - feat add
borderColorandborderWidthparameters for tooltip outline (follows arrow shape) - feat arrow auto-flips with tooltip direction when viewport space is insufficient
- fix tooltip not reappearing when re-hovering during fade-out animation
0.1.5 #
- feat add viewport overflow protection with auto direction flip and position clamping
- feat add
screenMarginparameter to control minimum distance from screen edges - docs
tooltipBuildernow documents that content sizing is the caller's responsibility
0.1.4 #
- fix
interactivemode now properly pauses auto-hide timer while cursor is on tooltip
0.1.3 #
- feat add
boxShadowparameter for custom shadow control (color, blur, spread, offset)
0.1.2 #
- feat add
interactiveoption to control whether tooltip stays visible on hover - feat add
waitDurationfor delayed tooltip appearance on hover - feat add
showDurationfor auto-hiding tooltip after a set time with timer reset on re-enter
0.1.1 #
- feat add
crossAxisOffsetparameter for shifting tooltip along the cross-axis
0.1.0 #
- implement JustTooltip core widget with direction (top/bottom/left/right) + alignment (start/center/end)
- implement JustTooltipController for programmatic show/hide/toggle
- implement tooltip position utils with 12-combination anchor mapping and RTL support
- feat hover and tap trigger modes
- feat fade animation with configurable duration
- feat custom tooltip content via
tooltipBuilder - feat single-instance enforcement (only one tooltip visible at a time)
- feat interactive playground example app with theme switching