Fix tab bar scroll to keep active tab fully visible after rename

When a tab is renamed (manually or via auto-generation), its width may
change. Added activeTabName as a dependency to the scroll-into-view
effect so the tab bar auto-scrolls to keep the active tab (including
close button) fully visible after name changes.
This commit is contained in:
Pedram Amini
2026-02-05 00:54:38 -06:00
parent d01e2a01e3
commit a14b5b770b
2 changed files with 59 additions and 2 deletions

View File

@@ -1581,6 +1581,56 @@ describe('TabBar', () => {
rafSpy.mockRestore(); rafSpy.mockRestore();
}); });
it('scrolls active tab into view when its name changes (e.g., after auto-generation)', async () => {
// Mock requestAnimationFrame
const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
cb(0);
return 0;
});
const tabs = [
createTab({ id: 'tab-1', name: null }), // Tab without name initially
createTab({ id: 'tab-2', name: 'Tab 2' }),
];
const { rerender, container } = render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockTheme}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
/>
);
// Simulate the active tab's name being updated (e.g., auto-generated name)
// This should trigger a scroll to ensure the now-wider tab is still visible
const updatedTabs = [
createTab({ id: 'tab-1', name: 'A Much Longer Auto-Generated Tab Name' }),
createTab({ id: 'tab-2', name: 'Tab 2' }),
];
rerender(
<TabBar
tabs={updatedTabs}
activeTabId="tab-1"
theme={mockTheme}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
/>
);
// The scroll behavior uses getBoundingClientRect which returns 0s in JSDOM,
// so we just verify the effect runs without error and the tab renders with new name
const activeTab = container.querySelector('[data-tab-id="tab-1"]');
expect(activeTab).toBeTruthy();
expect(screen.getByText('A Much Longer Auto-Generated Tab Name')).toBeTruthy();
rafSpy.mockRestore();
});
}); });
describe('styling', () => { describe('styling', () => {

View File

@@ -1599,7 +1599,14 @@ function TabBarInner({
const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map()); const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const [isOverflowing, setIsOverflowing] = useState(false); const [isOverflowing, setIsOverflowing] = useState(false);
// Ensure the active tab is fully visible (including close button) when activeTabId or activeFileTabId changes, or filter is toggled // Get active tab's name to trigger scroll when it changes (e.g., after auto-generated name)
const activeTab = tabs.find((t) => t.id === activeTabId);
const activeTabName = activeTab?.name ?? null;
// Ensure the active tab is fully visible (including close button) when:
// - activeTabId or activeFileTabId changes (new tab selected)
// - activeTabName changes (tab renamed, so width may have changed)
// - filter is toggled
useEffect(() => { useEffect(() => {
// Double requestAnimationFrame ensures the DOM has fully updated after React's state changes // Double requestAnimationFrame ensures the DOM has fully updated after React's state changes
// First rAF: React has committed changes but browser hasn't painted yet // First rAF: React has committed changes but browser hasn't painted yet
@@ -1634,7 +1641,7 @@ function TabBarInner({
} }
}); });
}); });
}, [activeTabId, activeFileTabId, showUnreadOnly]); }, [activeTabId, activeFileTabId, activeTabName, showUnreadOnly]);
// Can always close tabs - closing the last one creates a fresh new tab // Can always close tabs - closing the last one creates a fresh new tab
const canClose = true; const canClose = true;