Skip to content

Tooltip v4#1812

Open
bendansby wants to merge 2 commits into
NoRedInk:masterfrom
bendansby:tooltip-v4
Open

Tooltip v4#1812
bendansby wants to merge 2 commits into
NoRedInk:masterfrom
bendansby:tooltip-v4

Conversation

@bendansby
Copy link
Copy Markdown
Contributor

@bendansby bendansby commented Apr 27, 2026

Context

Nri.Ui.Tooltip.V3 had grown a large surface area — 16 directional functions (4 sides × 4 breakpoints), 12 alignment functions (3 alignments × 4 breakpoints), four *Css breakpoint helpers, three padding functions, two width functions — and still required callers to pre-decide which side the tooltip should appear on. When tooltips appeared near a viewport edge, they clipped instead of flipping.

V4 is a clean-break rewrite. Same a11y semantics, much smaller API, and tooltips auto-flip when they would otherwise clip the viewport.

V3 is untouched. Existing consumers keep working.

🔧 What changes

New module: Nri.Ui.Tooltip.V4

  • Positioning collapses to four functions. above / below / before / after (logical, RTL-friendly). Auto-flipping is on by default — these are preferred sides, and the tooltip moves to the opposite side only if it would clip.
  • Alignment is one function taking a sum type: align Start | Middle | End. (Plus convenience wrappers alignStart / alignMiddle / alignEnd.) The pixel-offset arg is gone.
  • Breakpoints are one mechanism. forBreakpoint Tooltip.mobile [ Tooltip.below, Tooltip.alignStart ] replaces all 28 of V3's *ForMobile / *ForQuizEngineMobile / *ForNarrowMobile direction and alignment variants and the per-breakpoint *Css helpers.
  • Padding and width are one attribute each: padding (Padding) with Small / Normal / Custom Float, width Int or fitToContent.
  • flip / noFlip to opt out of auto-flipping.
  • offset Float to tune the gap between trigger and tooltip (default 12px, was hardcoded in V3).
  • Same purpose attributes: primaryLabel, auxiliaryDescription, helpfullyDisabled, disclosure.
  • Same behavior attributes: onToggle, onTriggerKeyDown, open.
  • Same escape hatches: css, custom, nriDescription, testId.

Auto-flipping engine

Powered by a new <nri-tooltip-auto> custom element (lib/Tooltip/V4.js):

  • Wraps the trigger + tooltip when flip is enabled (default).
  • Runs a requestAnimationFrame loop while connected; on each frame, reads the trigger's bounding rect and only rewrites data-position / data-align when the rect changed. This is the same approach Floating UI uses for autoUpdate, and catches every kind of position change including ancestor-driven movement (e.g. drag).
  • The tooltip's positioning, alignment, and tail rendering CSS are all keyed off [data-position="…"] / [data-align="…"] attributes via Css.Global rules, so the flip happens without an Elm re-render.

Catalog

  • Examples/Tooltip.elm migrated to V4 (version bumped to 4). Direction/alignment controls flattened from 8 to 2; per-breakpoint multipliers gone.
  • New auto-flip playground: a launcher card that opens a fullscreen overlay with a draggable trigger and a "preferred side" picker. The playground tooltip inherits the customizable example's settings so reviewers can experiment with noFlip / withoutTail / padding / etc. while dragging.
  • Body now has Fonts.baseFont, gray20 color, and line-height: 1.5 set globally so unstyled text on example pages inherits Muli rather than Times New Roman.

Migration

V3 V4
onTop above
onBottom below
onLeft before
onRight after
alignStart 10 alignStart (offset removed)
alignMiddle alignMiddle (or omit — default)
alignEnd 10 alignEnd
onTopForMobile forBreakpoint Tooltip.mobile [ above ]
alignMiddleForMobile forBreakpoint Tooltip.mobile [ alignMiddle ]
mobileCss [...] forBreakpoint Tooltip.mobile [ css [...] ]
exactWidth 320 exactWidth 320 (kept)
fitToContent fitToContent (kept)
smallPadding / normalPadding / customPadding x smallPadding / normalPadding / customPadding x (kept) or padding Small / etc.

Most of the migration is mechanical find-and-replace. The biggest win is callers that today need 6+ attributes to express different behavior at three breakpoints — those collapse to three forBreakpoint calls.

Component completion checklist

  • Documentation: V4 module is fully @docs-ed
  • Component Catalog updated to use V4
  • Customizable example produces sample code
  • Keyboard support unchanged from V3
  • V3 left intact — consumers migrate at their own pace
  • Reviewers
    • Component library owner
    • Designer (visible behavior change — auto-flip)

Notes for reviewers

  • JS dependency: the auto-flip is powered by lib/Tooltip/V4.js, which is required by lib/index.js and so loads automatically wherever the noredink-ui JS is bundled (catalog included). Apps that bundle Elm without noredink-ui/lib would lose auto-flip and fall back to whatever side was passed (no flip), which is graceful but worth flagging.
  • No viewToggleTip yet. The "?" icon helper is V3-only for now; consumers using it should stay on V3 until we port it.
  • Tail rendering is attribute-keyed. That means the four directions × three alignments worth of pseudo-element CSS ships once on the page (via Css.Global.global inside the tooltip), and the JS just toggles attributes. Slightly more CSS in the page than V3, but simpler to reason about and cheaper at flip-time (no re-render).
  • primaryLabel is still the default — keep using auxiliaryDescription / helpfullyDisabled / disclosure consciously.

bendansby and others added 2 commits April 27, 2026 09:52
Clean-break rewrite of Tooltip.V3 with a much smaller surface area:

- Single positioning attribute family: above/below/before/after, plus
  align Start/Middle/End. Replaces 16 V3 functions (4 directions × 4
  breakpoints) and 12 align functions (3 alignments × 4 breakpoints).
- forBreakpoint Mobile/QuizEngineMobile/NarrowMobile [ ... ] replaces
  the *ForMobile / *ForQuizEngineMobile / *ForNarrowMobile variants.
- Auto-flipping is the default: the tooltip moves to the opposite side
  only if the preferred side would clip the viewport. Opt out with
  noFlip.
- Padding and width are single sum-typed attributes rather than three
  separate functions each.

Auto-flipping is powered by a new <nri-tooltip-auto> custom element
that observes the trigger (ResizeObserver + scroll/resize) and writes
data-position / data-align attributes on the tooltip element. The
tooltip's position/align CSS is keyed off those attributes so the flip
happens without an Elm re-render.

V3 is left untouched. Consumers migrate at their own pace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ound.

- Render the V4 tooltip tail with attribute-keyed `::before`/`::after`
  pseudo-elements so the JS auto-flip moves the tail with the bubble
  without an Elm re-render. Honors `withoutTail` via a `data-tail`
  attribute the global rule responds to.
- Switch the custom element's update strategy to a continuous
  `requestAnimationFrame` loop while connected so dragging the trigger
  updates the orientation in real time (ResizeObserver alone misses
  position-only changes via ancestor `left`/`top`).
- Reset `transform`/`left`/`right` on each align so the JS swap from
  `middle` -> `start`/`end` correctly clears the centering transform.
- Bump default offset from 8px to 12px, normal padding to 16px,
  small padding to 8px 12px, font size to 15px, and add Shadows.high.
- Catalog: add an interactive auto-flip playground that takes over the
  page with a draggable trigger, inheriting the customizable example's
  attributes so users can experiment with `noFlip`/`withoutTail`/etc.
- Catalog body: set `Fonts.baseFont` + `Css.color gray20` +
  `line-height: 1.5` globally so unstyled text inherits Muli rather
  than Times New Roman.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant