Skip to content

feat(components): freeze tab widths during rapid closes like Chromium - #805

Merged
wibus-wee merged 2 commits into
mainfrom
lody/92fc1fb1-e40
Sep 18, 2026
Merged

wibus-wee merged 2 commits into
mainfrom
lody/92fc1fb1-e40

Conversation

@wibus-wee

Copy link
Copy Markdown
Member

Summary

Implements Chromium's rapid-close mode (in_tab_close_ / override_available_width_for_tabs_) in AdaptiveTabStrip, so closing a tab keeps every survivor at its current width and the next close button lands under the cursor — matching Chrome's behavior during repeated closes.

  • Pointer-armed freeze: a pointerdown inside the strip arms the gesture (≈ CloseTabSource::kFromMouse); keyboard/programmatic closes relayout normally.
  • Chromium exit semantics: the freeze releases when the pointer leaves an expanded MouseWatcher region (40px below the strip, 60px toward the new-tab button), on a tab add, on a viewport shrink below the captured width (growing keeps it, min(override, real)), or when a single tab remains. Closing the last tab while frozen re-spreads survivors over the occupied width, keeping the new last tab's close button under the cursor.
  • Role-based frozen widths: a survivor promoted to active takes the captured active width (size_delta = next_active_tab->width()), and widens in place without the removal slide so it cannot read as unfolding from the emptied slot.
  • 200ms transitions: removals slide closed via margin-inline-start; pure inserts grow from zero (StartInsertTabAnimation); a same-commit remove+add morphs from the removed width (draft→session promotion); a lone tab in an empty strip renders directly.
  • Render-phase freeze decision: the removal diff runs during render (React derived-state adjustment), so a removal commit's first painted frame already carries frozen widths. An earlier useLayoutEffect-driven freeze let the unfrozen allocation commit first and flashed survivors at the fresh width for a frame.
  • Storybook Rapid Close story is now fully interactive (close/select/reorder/new-tab/restore against local state) and selects the same neighbour as production's getSessionTabFallback.

Test plan

  • tests/session-tab-bar.test.tsx rapid-close suite — 24/24 pass: freeze, slop boundaries, consecutive closes, slide margins, trailing-close re-spread, single-survivor release, viewport grow/shrink, unarmed close, active-close promotion, substitution morph, empty-strip insert, and a MutationObserver regression asserting the active item never holds an unfrozen width on the removal commit
  • tests/adaptive-tab-strip.test.ts — 7/7 pass
  • Verified in Storybook (Sessions/SessionTabBar → Rapid Close): freeze, slop region, trailing-close re-spread, margin/width CSSTransitions via document.getAnimations(), and per-mutation style sequences showing no unfrozen width
  • Exercised in a running desktop build

Generated with Devin

A pointer-triggered close freezes every surviving tab at its current
width (Chromium in_tab_close_ / override_available_width_for_tabs_) so
the next close button stays under the cursor. The freeze releases when
the pointer leaves an expanded region (40px below the strip, 60px toward
the new-tab button), on a tab add, on a viewport shrink below the
captured width, or when a single tab remains; closing the last tab while
frozen re-spreads survivors over the occupied width. Widths freeze by
role: a survivor promoted to active takes the captured active width and
widens in place without the removal slide.

Removals slide closed and inserts grow in over a 200ms transition; a
same-commit remove+add morphs from the removed width instead of zero,
and a lone tab in an empty strip renders directly. The removal diff runs
during render (derived-state adjustment), so the first painted frame of
a removal already carries frozen widths — an earlier effect-driven
freeze let the unfrozen allocation commit first and flashed survivors at
the fresh width.

Model: swe-2-max

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: wibus-wee <62133302+wibus-wee@users.noreply.github.com>
@wibus-wee
wibus-wee merged commit 90a05ae into main Sep 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant