fix(studio): keep Explorer Home tab selected (#50684)

## Problem

Selecting Explorer's Home tab immediately redirects back to the last
visited tab.

## Solution

Clear the saved Explorer destination before navigating Home. Explorer
owns this behavior through a generic `onTabChange` callback on the
shared tabs component.

## Review instructions

1. Open Explorer with the Home tab visible, then open a query, notebook,
or chat.
2. Click Home. Confirm it stays selected and the previous tab remains
open.
3. Switch back to the previous tab, then use Tab and arrow keys to
select Home. Confirm it stays selected.
4. Select a content tab, leave Explorer, and return. Confirm the last
visited tab still restores.

## Validation

Mouse and keyboard regression tests and existing restoration tests pass
(12 tests). Typecheck, targeted lint, and formatting checks pass.

## Checklist

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md).
- Docs authoring checks: not applicable; no docs changes.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Explorer navigation now clears the previously saved Explorer
destination when returning to the home tab.
* Returning to Explorer home works consistently through both mouse and
keyboard navigation.
* **Tests**
* Added coverage for Explorer tab navigation, history cleanup, and
selected-tab state.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saxon Fletcher authored and GitHub committed 2026-09-22 11:34:31 +10:00
1 parent a31ca2bad0
commit 509c797be1
3 files changed
+113

No files matched your search

@@ -0,0 +1,103 @@
import { QueryClient } from '@tanstack/react-query'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LOCAL_STORAGE_KEYS } from 'common'
import mockRouter from 'next-router-mock'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ExplorerLayout } from './ExplorerLayout'
import { createTabsState, TabsStateContext } from '@/state/tabs'
import { customRender } from '@/tests/lib/custom-render'
vi.mock('next/navigation', () => ({
usePathname: () => mockRouter.asPath,
}))
vi.mock('../ProjectLayout', () => ({
ProjectLayoutWithAuth: ({ children }: { children: ReactNode }) => <>{children}</>,
}))
vi.mock('@/components/interfaces/Account/Preferences/useExplorerPreferences', () => ({
useExplorerPreferences: () => ({ home: 'home', hasCompletedOnboarding: true, isReady: true }),
}))
vi.mock('@/components/interfaces/Explorer/ExplorerQueryTabCoordinator', () => ({
ExplorerQueryTabCoordinator: () => null,
}))
vi.mock('@/components/interfaces/Explorer/ExplorerNotebookTabCoordinator', () => ({
ExplorerNotebookTabCoordinator: () => null,
}))
vi.mock('@/components/interfaces/Explorer/hooks', () => ({
useCreateChat: () => ({ createChat: vi.fn() }),
useCreateNotebook: () => ({ createNotebook: vi.fn() }),
useCreateQuery: () => ({ createQuery: vi.fn() }),
}))
describe('Explorer Home tab navigation', () => {
beforeEach(() => {
vi.restoreAllMocks()
localStorage.clear()
mockRouter.setCurrentUrl({
pathname: '/project/[ref]/explorer/query/[id]',
query: { ref: 'default', id: 'query-1' },
})
})
it.each(['mouse', 'keyboard'] as const)(
'clears the saved Explorer destination before navigating Home with the %s',
async (input) => {
const user = userEvent.setup()
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
})
const historyKey = ['localStorage', LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY('default')]
const otherHistory = { editor: 'table-1', sql: 'snippet-1' }
queryClient.setQueryData(historyKey, {
...otherHistory,
explorer: { type: 'query', id: 'query-1' },
})
const tabs = createTabsState('default')
tabs.addTab({
id: 'query-query-1',
type: 'query',
label: 'My query',
metadata: { queryId: 'query-1' },
isPreview: false,
})
let historyAtNavigation: unknown
const push = vi.spyOn(mockRouter, 'push')
push.mockImplementationOnce(async () => {
historyAtNavigation = queryClient.getQueryData(historyKey)
return true
})
customRender(
<TabsStateContext.Provider value={tabs}>
<ExplorerLayout>Explorer content</ExplorerLayout>
</TabsStateContext.Provider>,
{ queryClient }
)
if (input === 'mouse') {
await user.click(screen.getByRole('tab', { name: 'Open Explorer home' }))
} else {
await user.tab()
expect(screen.getByRole('tab', { name: 'My query' })).toHaveFocus()
await user.keyboard('{ArrowLeft}')
}
expect(push).toHaveBeenCalledWith('/project/default/explorer')
expect(historyAtNavigation).toEqual(otherHistory)
expect(queryClient.getQueryData(historyKey)).toEqual(otherHistory)
expect(screen.getByRole('tab', { name: 'Open Explorer home' })).toHaveAttribute(
'aria-selected',
'true'
)
expect(tabs.tabsMap['query-query-1']).toBeDefined()
}
)
})
@@ -28,6 +28,7 @@ import {
useCreateNotebook,
useCreateQuery,
} from '@/components/interfaces/Explorer/hooks'
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
import { useIsTemporarySqlEditorVisit } from '@/hooks/misc/useIsTemporarySqlEditorVisit'
import { useTrack } from '@/lib/telemetry/track'
import {
@@ -46,6 +47,7 @@ export interface ExplorerLayoutProps extends ComponentProps<typeof ProjectLayout
export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayoutProps) => {
const { ref } = useParams()
const tabs = useTabsStateSnapshot()
const { setLastVisitedExplorerTab } = useDashboardHistory()
const { home, hasCompletedOnboarding, isReady } = useExplorerPreferences()
const shouldShowHomeTab = isReady && (!hasCompletedOnboarding || home === 'home')
@@ -68,6 +70,10 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
entity: browserTitle?.entity ?? activeTabLabel,
}
const handleTabChange = (id: string) => {
if (id === EXPLORER_HOME_TAB_ID) setLastVisitedExplorerTab(undefined)
}
return (
<ProjectLayoutWithAuth
product="Explorer"
@@ -99,6 +105,7 @@ export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayout
isCollapseButtonHidden
customTabs={shouldShowHomeTab ? <HomeTabButton /> : undefined}
newTabButton={<NewTabButton />}
onTabChange={handleTabChange}
/>
</div>
<div className="flex-grow min-h-0">{children}</div>
@@ -41,6 +41,7 @@ interface EditorTabsProps {
customTabs?: ReactNode
newTabButton?: ReactNode
isCollapseButtonHidden?: boolean
onTabChange?: (id: string) => void
}
// [Joshen] Will be adjusting this component to support Explorer
@@ -49,6 +50,7 @@ export const EditorTabs = ({
customTabs,
newTabButton,
isCollapseButtonHidden,
onTabChange,
}: EditorTabsProps) => {
const { ref } = useParams()
const router = useRouter()
@@ -168,6 +170,7 @@ export const EditorTabs = ({
}
const handleTabChange = (id: string) => {
onTabChange?.(id)
tabs.handleTabNavigation(id, router)
}