Skip to content

feat(scraps): Add Tooltip.Header, Grid, Row and Footer - #121983

Merged
ChrisandraVaz merged 9 commits into
masterfrom
cv/scraps/tooltip-compound
Aug 27, 2026
Merged

feat(scraps): Add Tooltip.Header, Grid, Row and Footer#121983
ChrisandraVaz merged 9 commits into
masterfrom
cv/scraps/tooltip-compound

Conversation

@ChrisandraVaz

@ChrisandraVaz ChrisandraVaz commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Splitting #121854 into three PRs based on the review feedback. This is the first one, and it only adds the tooltip primitives so the RelativeTime card has something to build on.

TooltipContent hardcoded padding: md lg and text-align: center. That's fine for a sentence but wrong for a card, so anything card shaped had to cancel both by hand. We already do that in three places: logsTimeTooltip, breadcrumbsTimeline, and group.tsx. RelativeTime was about to be the fourth.

  • Tooltip.Header / Footer take a label plus optional leadingItems and trailingItems. Sentence case, no bottom border.
  • Tooltip.Grid owns the grid the rows line up against.
  • Tooltip.Row takes leadingItems and trailingItems either side of its main cell, and renders as display: contents so all three become items of the grid itself. That's what keeps columns aligned across rows when one cell is wider than the one above it.
  • No padding prop. The overlay pads itself, and sections pull back out to its edges.

Primitives only here, nothing migrated yet.

Review feedback addressed

  • trailing is trailingItems, and leadingItems exists on Header, Row and Footer.
  • Tooltip.Body is Tooltip.Grid, which is what it actually is.
  • The padding prop is gone. Optional guidance gets forgotten, so the component handles the layout instead of asking the caller to.
  • Done with a negative margin, as suggested. Sections cancel the overlay's padding with a negative inline margin and re-apply it themselves, so a full width row or separator reaches the edges while its own content stays inset. Only the first and last section cancel the block padding, since adjacent negative margins compound and would collapse the space between two sections.
  • max-width needs no adjustment. The overlay's border box stays capped at 225, its content box is that minus the inline padding, and a section adds exactly that padding back, so it spans the overlay's outer width and no wider. Shrink-to-fit balances for the same reason. Worth confirming on the story preview, since jsdom does no layout.
  • display: contents stays on Tooltip.Row. Checked whether subgrid is used elsewhere first: it is, in 31 files, and core/table uses this exact recipe. But display: contents was fine either way, so it stays.

InfoText no longer forwards padding, since it only did so to give TimeSince a way to reach this.

Stack

Bottom of a 3 PR stack:

  1. this PR, the tooltip sections
  2. ref(scraps): Move TimezoneProvider into scraps #122063, the datetime provider — merged
  3. RelativeTime + TimeSince (feat(scraps): Add RelativeTime component #121854), which needs this one

Worth writing down

leadingItems/trailingItems on Tooltip.Row are optional, so a row emitting two cells where its neighbours emit three shifts every following row one track over, and it cascades. Header and Footer guard against that with defined(trailingItems); Row can't, because under display: contents every cell of every row is one placement stream. Subgrid would contain it to the row. Nothing shipping hits this today, since RelativeTime always emits three cells.

  • Story: tooltip.mdx.

@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Story previews

Preview the stories changed in this PR on the Vercel deployment:

Preview deployment: https://sentry-45sc79fr5.sentry.dev

Tooltip content that is a row of labelled values rather than a sentence
currently has to fight the overlay: it hardcodes `padding: md lg` and
`text-align: center`, so a card wanting full-width sections has to cancel
both with negative margins. That pattern is already hand-rolled twice, in
logsTimeTooltip and breadcrumbsTimeline, and RelativeTime was about to be
the third.

Adds a `padding` prop (defaulting to the current `md lg`, so no existing
tooltip changes) and four sections that own their own spacing.
`Tooltip.Body` owns the grid and `Tooltip.Row` renders as
`display: contents`, so a column stays aligned across rows even when one
row's cell is wider than the same cell above it.

Also forwards `padding` through InfoText, which is the path TimeSince
takes to the tooltip.
The padding tests compared emotion class names between two renders,
which proved the default matched an explicit `md lg` but never that
`md lg` resolves to the values the hardcoded rule used to emit. The
comment justifying that also claimed emotion rules are unreadable in
tests, which is wrong: sentry-test/utils exports `getEmotionRules`, and
infoText.spec.tsx already uses it.

Also covers two things that had no test at all: that InfoText forwards
`padding` to the tooltip, which is the whole reason it was added and
what PR 3 depends on, and that a tooltip composed of sections still
associates itself with its trigger for screen readers.
@ChrisandraVaz
ChrisandraVaz marked this pull request as ready for review August 14, 2026 18:59
@ChrisandraVaz
ChrisandraVaz requested a review from a team as a code owner August 14, 2026 18:59
Comment thread static/app/components/core/tooltip/tooltip.tsx Outdated
Comment thread static/app/components/core/tooltip/tooltip.tsx Outdated
Comment thread static/app/components/core/tooltip/tooltip.tsx Outdated
Comment thread static/app/components/core/tooltip/tooltip.tsx
Comment thread static/app/components/core/tooltip/tooltip.tsx
Comment thread static/app/components/core/tooltip/tooltip.tsx Outdated
Addresses the review on #121983.

- `Tooltip.Body` is now `Tooltip.Grid`, which says what it is rather than
  claiming to be the whole body of the tooltip.
- `trailing` is now `trailingItems`, and `leadingItems` joins it on the
  header, footer and row, matching the pattern the rest of the system uses.
- `Tooltip.Row` carries that pattern rather than being a bare
  `display: contents` passthrough, so a three column row is described rather
  than hand-placed. It stays a component because `display: contents` is what
  keeps a column aligned across rows.
- Composing sections into `title` now drops the overlay's content padding on
  its own. The prop stays for what the detection cannot see: a component that
  renders sections internally is a single element from out here, which is the
  case `TimeSince` is about to be in.
Inferring the padding meant walking `title` with `React.Children`, which the
React docs discourage, and it never covered the case that motivated it. The
walk only sees what the tooltip was handed directly, so `title={<RelativeTime />}`
is one opaque element and that component has to pass `padding="0"` regardless.
It only helped when the sections were inlined at the call site, which is the
case that needs the help least.

So `padding` goes back to a plain prop defaulting to `md lg`, and composing
sections means passing `'0'` alongside them. The section renames stay.
@ChrisandraVaz ChrisandraVaz changed the title feat(scraps): Add Tooltip.Header, Body, Row and Footer feat(scraps): Add Tooltip.Header, Grid, Row and Footer Aug 25, 2026
ChrisandraVaz added a commit that referenced this pull request Aug 25, 2026
## Summary

Second of the three PRs from the #121854 review. Dominik's point was
that scraps can't reach into sentry for implicit context, so this
inverts the two things that decide how an absolute time is written.

`@sentry/scraps/datetime` exports `DateTimeProvider`, which takes
`{timezone, clockDisplay}`, plus `useTimezone()` and
`useClockDisplay()`. `SentryDateTimeProvider` stays in sentry, reads
`useUser`, and supplies the value. It's wired into `ScrapsProviders`
next to the tracking and link providers.

The clock lives here rather than in PR 3 because `DateTime` and
`TimeSince` both read it from `useUser` directly, so moving only the
timezone would have left them coupled to sentry and PR 3 unable to move
`TimeSince`.

## Review feedback addressed

- One `DateTimeContext` instead of separate timezone and clock contexts,
so consumers wire up a single thing.
- File is `@sentry/scraps/datetime`.
- `useTimezone()` unchanged, so none of the existing call sites move.
`useClockDisplay()` added alongside it.
- `clock24Hours: boolean` became `clockDisplay: '12' | '24'`, which
reads better than a negated boolean at the call site.

## Heads up on the browser fallback

The fallback reads as configured timezone, then browser, but the browser
half can't currently be reached and the docstring now says so.
`UserSerializer` resolves the option to `SENTRY_DEFAULT_TIME_ZONE`
before it leaves the API, so someone who never opened their settings is
indistinguishable from someone who chose UTC. Auto-detect needs the API
to preserve that state first, which is a backend PR. No org timezone,
per Nate.

## Stack

1. #121983, the tooltip sections
2. **this PR**, the datetime provider
3. RelativeTime + TimeSince (#121854), rebased on both

1 and 2 are independent. 3 needs both.

## Test plan

- `core/datetime.spec.tsx` covers the defaults, the provided value, and
nesting. `scrapsProviders/datetime.spec.tsx` covers resolution from the
user and the 24 hour mapping.
- Three specs were setting the clock through `ConfigStore` and asserting
on components that now read context. Fixed to provide the value.
- `pnpm typecheck`, `lint:js`, `oxfmt`, `knip` clean. 5707 tests across
components, scrapsProviders and profiling utils. Green, on a base
current with master.

`ScrapsProviders` wraps `appRoutes` above `App`, so moving the provider
out of `AppProviders` widens coverage rather than narrowing it, and now
also covers the `processInitQueue` path.

## Open question

`feedbackTimestampsTooltip`, `eventCreatedTooltip`, `auditLogList` and
the release cards still read `clock24Hours` from `useUser` directly.
They don't go through `DateTime` or `TimeSince` so nothing breaks, and I
kept this PR to what PR 3 needs. Happy to sweep them separately.
An optional prop that has to be remembered is the wrong shape for this: the
overlay's padding and the sections' padding are two halves of one decision, so
the API should make it rather than the caller.

The overlay now pads itself always and stands that padding down when it
contains a section, matched with `:has([data-tooltip-section])`. Doing it in
CSS rather than by inspecting children is what makes it cover the case that
motivated the prop. A component that renders the sections internally is one
opaque element as far as the tooltip is concerned, so `React.Children` could
never see into it, but the rendered DOM is the same either way and `:has` reads
that. `:has` is already used across the app, including in compactSelect, slider
and segmentedControl.

`InfoText` no longer forwards `padding` either, since it only forwarded it to
give `TimeSince` a way to reach this.

The specs cover it in two halves, because jsdom resolves neither emotion's
computed styles nor `:has`: one asserts the overlay carries the rule, the
others assert the marker really is in the DOM for a section composed directly,
for a section behind a component boundary, and not for a plain sentence.
Replaces the `:has()` version of this with the negative margin Nate asked for,
so the mechanism is the one he proposed rather than one substituted for it.

The overlay pads itself as before. Sections cancel that padding with a negative
inline margin and re-apply it themselves, so a full width row or separator can
reach the edges while its own content stays inset. Only the first and last
section cancel the block padding: doing it on every one would collapse the
space between two of them, since adjacent negative margins compound.

`max-width` needs no adjustment. The overlay's border box stays capped, its
content box is that minus the inline padding, and a section adds exactly that
padding back, so a section spans the overlay's outer width and no wider. The
shrink-to-fit case balances for the same reason: the section's negative margins
cancel its own padding out of the intrinsic width.

The prop is still gone, which was the point of all of this.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d13fe92. Configure here.

Comment thread static/app/components/core/tooltip/tooltip.tsx
The block-start cancel keyed off `:first-child`, which never matched. `Overlay`
renders `OverlayArrow` ahead of its content whenever `arrowProps` is passed,
and `Tooltip` always passes it, so the first section is the second child and a
structured card kept the overlay's top padding on top of its own.

Selecting on a preceding section instead gets the same result without caring
what else the overlay renders: every section cancels the top padding, and one
that follows another puts it back.

The old tests could not have caught this. They asserted the rules existed in
the generated CSS, which they did, not that anything matched them — jsdom
resolves neither emotion's computed styles nor selectors. The added test
asserts the thing that is checkable and was the actual trap: a section is not
the overlay's first child.

Found by the Cursor bot.
Comment on lines +343 to +353
* </Fragment>
* }
* >
* {trigger}
* </Tooltip>
* ```
*
* That holds wherever the sections are rendered from. A component that renders
* them internally is covered too, because its sections are still the overlay's
* own children in the DOM.
*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The Tooltip component's section styling breaks silently if child sections are wrapped in an element like a <div> instead of a Fragment, due to a direct-child CSS selector.
Severity: LOW

Suggested Fix

To make the API less fragile, either update the CSS to not rely on a direct child selector, or add a runtime development warning if the Tooltip's children are not the expected section components. At a minimum, explicitly document this limitation, warning users not to wrap sections in container elements.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: static/app/components/core/tooltip/tooltip.tsx#L343-L353

Potential issue: The `Tooltip` component's styling for sections like `Header`, `Footer`,
and `Grid` relies on a direct child CSS selector (`> [data-tooltip-section]`) to apply
negative margins. If a developer wraps these sections in a container element like a
`<div>` instead of the documented `Fragment` pattern, the selector will not match. This
causes a silent failure where the negative margins are not applied, resulting in a
visually broken layout with unexpected padding around the sections. The component does
not provide any runtime warnings or errors in this scenario.

Did we get this right? 👍 / 👎 to inform future reviews.

@natemoo-re natemoo-re left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great, thanks for addressing the feedback!

@ChrisandraVaz
ChrisandraVaz merged commit 68e033d into master Aug 27, 2026
79 checks passed
@ChrisandraVaz
ChrisandraVaz deleted the cv/scraps/tooltip-compound branch August 27, 2026 17:37
ChrisandraVaz added a commit that referenced this pull request Aug 28, 2026
adds a `RelativeTime` card to `@sentry/scraps/relativeTime` — relative
time in the header, absolute time in the viewer's timezone and UTC.
Every `TimeSince` renders one, starting with the Last Seen and First
Seen columns on the issue feed.

**Before:**

<img width="255" height="177" alt="image"
src="https://github.com/user-attachments/assets/f98d705e-6961-4f20-8f34-f0538397a1ec"
/>

<img width="255" height="177" alt="image"
src="https://github.com/user-attachments/assets/f1c08b85-d9b3-436a-bc47-5a404f5a73b2"
/>

**After:**

Last seen 
<img width="255" height="177" alt="image"
src="https://github.com/user-attachments/assets/039801fe-d39c-4010-8ea3-885d81215f16"
/>


First seen
<img width="255" height="177" alt="image"
src="https://github.com/user-attachments/assets/7952adf7-dad0-40f4-80ef-9e5ac0c8b5e1"
/>

If only UTC is set (user didnt update their timezone in settings yet):

<img width="255" height="177" alt="image"
src="https://github.com/user-attachments/assets/70b00b36-83ad-4948-8712-cdffa6f5b05d"
/>

## Stack

Top of a 3 PR stack. This one needs both of the others:

1. #121983, the tooltip sections
2. #122063, the datetime provider
3. **this PR**, RelativeTime + TimeSince

## Review feedback addressed

- **Scraps no longer reaches into sentry for the timezone.**
`@sentry/scraps/datetime` owns the provider and sentry supplies the
value, in #122063. Same inversion as the tracking and link providers.
- **`TimeSince` always renders `RelativeTime`.** There's no way to get
one without the other now. `tooltipBody` remains for the callers that
replace the overlay wholesale.
- **Two rows, never three.** Viewer's timezone on top, UTC underneath.

## Open question

**Does `TimeSince` move into scraps in this PR, or the next one?**
Nothing blocks it: the only sentry import left in the file is `t` from
`sentry/locale`, which 35 components already in scraps import,
`RelativeTime` among them. So it's a file move plus rewriting 87
imports. That's ~90 mechanically changed files, which would bury the
part of this PR worth actually reading, so I'd rather be told which
you'd prefer to review.

## Known gap

The provider falls back to the browser's timezone when the viewer hasn't
configured one, but that branch can't be reached today. `UserSerializer`
resolves the option to `SENTRY_DEFAULT_TIME_ZONE` before it leaves the
API, so someone who never opened their settings is indistinguishable
from someone who chose UTC. Auto-detect needs the API to preserve that
state first, which is a backend PR.

No org timezone: neither the `Organization` type nor its serializer
carries one, and per Nate we'd rather not add a property to the org for
this.

story: `relativeTime.mdx`
<!-- [live
story](https://sentry-git-cv-scraps-relative-time.sentry.dev/stories/core/relativeTime)
-->

<!-- Closes DE-1513 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants