mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
## What kind of change does this PR introduce? Feature / a11y polish ## What is the current behavior? Studio and Docs each had their own skip-to-content link (different styling and behaviour). www and design-system had none. ## What is the new behavior? Shared `SkipToContent` in `ui-patterns`, adopted by Studio, Docs, www, and design-system. Documented as a fragment with a short note under Accessibility → Jumping ahead. Tab once to reveal the button (top-left), Enter to jump to a content-only `<main>`. | After | | --- | | <img width="836" height="324" alt="CleanShot 2026-07-24 at 14 08 47@2x" src="https://github.com/user-attachments/assets/6df29452-e53a-4eca-8f64-946f2b9f605d" /> | ## To test Shared steps for every app: enable Tab key navigation if needed, load the preview, press **Tab** once — skip button should slide in top-left. Press **Enter** — focus jumps to main content (no blue ring on `<main>`). Press **Tab** again — first interactive control in the page body, not the sidebar/nav. Hover the skip button — solid fill, clear hover state, no chrome showing through. - **Studio** — [preview](https://studio-staging-git-dnywh-featskip-to-content-supabase.vercel.app) → sign in → any project page - **Docs** — [preview](https://docs-git-dnywh-featskip-to-content-supabase.vercel.app) → any docs page with sidebar - **www** — [preview](https://zone-www-dot-com-git-dnywh-featskip-to-content-supabase.vercel.app) → homepage or any marketing page with the default nav - **Design system** — [preview](https://design-system-git-dnywh-featskip-to-content-supabase.vercel.app) → any docs page (confirm Tab from content does **not** walk the sidebar), plus [Skip to Content fragment](https://design-system-git-dnywh-featskip-to-content-supabase.vercel.app/docs/fragments/skip-to-content) ## Additional context Follow-up to #47694 / #48303 (Studio) and #47515 (Docs). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a reusable “Skip to content” accessibility link across key layouts and pages. - Updated main landmarks to support keyboard focus and skip-link navigation (`id="main"`). - **Accessibility** - Skip links now follow consistent landmark-target conventions and remain hidden until focused. - Improved documentation for skip links/jump shortcuts in persistent chrome layouts. - **Documentation** - Added a dedicated Skip to Content fragment, navigation entry, and expanded accessibility guidance. - Updated button description wording in component docs. - **Tests** - Added component tests for SkipToContent. - **Chores** - Exposed SkipToContent via additional public package entry points. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
import { useBreakpoint, useParams } from 'common'
|
|
import { useRouter } from 'next/router'
|
|
import { PropsWithChildren, useEffect, useState } from 'react'
|
|
import { ResizablePanel, ResizablePanelGroup, SidebarProvider, usePanelRef } from 'ui'
|
|
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
|
|
|
import { BannerStack } from '../ui/BannerStack/BannerStack'
|
|
import { LayoutHeader } from './Navigation/LayoutHeader/LayoutHeader'
|
|
import MobileNavigationBar from './Navigation/NavigationBar/MobileNavigationBar'
|
|
import { MobileSheetProvider } from './Navigation/NavigationBar/MobileSheetContext'
|
|
import { StudioMobileSheetNav } from './Navigation/NavigationBar/StudioMobileSheetNav'
|
|
import { LayoutSidebar } from './ProjectLayout/LayoutSidebar'
|
|
import {
|
|
LayoutSidebarProvider,
|
|
SIDEBAR_KEYS,
|
|
} from './ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
|
import { ProjectContextProvider } from './ProjectLayout/ProjectContext'
|
|
import { AppBannerWrapper } from '@/components/interfaces/App/AppBannerWrapper'
|
|
import { Sidebar } from '@/components/interfaces/Sidebar'
|
|
import { useSyncScopedIntrospection } from '@/data/scoped-introspection'
|
|
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
|
|
import { useCheckLatestDeploy } from '@/hooks/use-check-latest-deploy'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import { useAppStateSnapshot } from '@/state/app-state'
|
|
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
|
|
|
|
export interface DefaultLayoutProps {
|
|
headerTitle?: string
|
|
hideMobileMenu?: boolean
|
|
}
|
|
|
|
/**
|
|
* Base layout for all project pages in the dashboard, rendered as the first child on all page files within a project.
|
|
*
|
|
* A second layout as the child to this is required, and the layout depends on which section of the dashboard the page is on. (e.g Auth - AuthLayout)
|
|
*
|
|
* The base layout handles rendering the following UI components:
|
|
* - App banner (e.g for notices or incidents)
|
|
* - Mobile navigation bar
|
|
* - First level side navigation bar (e.g For navigating to Table Editor, SQL Editor, Database page, etc)
|
|
*/
|
|
export const DefaultLayout = ({
|
|
children,
|
|
headerTitle,
|
|
hideMobileMenu,
|
|
}: PropsWithChildren<DefaultLayoutProps>) => {
|
|
useSyncScopedIntrospection()
|
|
useCheckLatestDeploy()
|
|
|
|
const { ref } = useParams()
|
|
const router = useRouter()
|
|
const panelRef = usePanelRef()
|
|
const isMobile = useBreakpoint('md')
|
|
const appSnap = useAppStateSnapshot()
|
|
const { isMaximised, activeSidebar } = useSidebarManagerSnapshot()
|
|
const { lastVisitedOrganization } = useLastVisitedOrganization()
|
|
|
|
const [isMounted, setIsMounted] = useState(false)
|
|
|
|
const backToDashboardURL = router.pathname.startsWith('/account')
|
|
? appSnap.lastRouteBeforeVisitingAccountPage.length > 0
|
|
? appSnap.lastRouteBeforeVisitingAccountPage
|
|
: IS_PLATFORM && !!lastVisitedOrganization
|
|
? `/org/${lastVisitedOrganization}`
|
|
: IS_PLATFORM
|
|
? '/organizations'
|
|
: '/project/default'
|
|
: undefined
|
|
|
|
const contentMinSizePercentage = 50
|
|
const contentMaxSizePercentage = 70
|
|
|
|
useEffect(() => {
|
|
setIsMounted(true)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!isMounted || !panelRef.current || !activeSidebar || isMobile) return
|
|
if (isMaximised) {
|
|
panelRef.current.collapse()
|
|
} else {
|
|
panelRef.current.resize(`${contentMaxSizePercentage}%`)
|
|
}
|
|
}, [isMounted, isMaximised, panelRef, activeSidebar, isMobile])
|
|
|
|
// This is required to prevent layout shift when rendering resizable panels (they initially render at 50%, then shift
|
|
// to whatever is specified).
|
|
if (!isMounted) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<SidebarProvider defaultOpen={false}>
|
|
<LayoutSidebarProvider>
|
|
<ProjectContextProvider projectRef={ref}>
|
|
<MobileSheetProvider>
|
|
<div className="flex flex-col h-screen w-screen">
|
|
<SkipToContent href="#main" />
|
|
{/* Top Banner */}
|
|
<AppBannerWrapper />
|
|
<div className="shrink-0">
|
|
{isMobile && (
|
|
<MobileNavigationBar
|
|
hideMobileMenu={hideMobileMenu}
|
|
backToDashboardURL={backToDashboardURL}
|
|
/>
|
|
)}
|
|
<LayoutHeader headerTitle={headerTitle} backToDashboardURL={backToDashboardURL} />
|
|
</div>
|
|
{/* Main Content Area */}
|
|
<div className="flex flex-1 w-full overflow-y-hidden">
|
|
{/* Sidebar - Only show for project pages, not account pages */}
|
|
{!router.pathname.startsWith('/account') && <Sidebar />}
|
|
{/* Main Content with Layout Sidebar */}
|
|
<ResizablePanelGroup
|
|
orientation="horizontal"
|
|
className="h-full w-full overflow-x-hidden flex-1 flex flex-row gap-0"
|
|
autoSaveId="default-layout-content"
|
|
>
|
|
<ResizablePanel
|
|
id="panel-content"
|
|
className="w-full"
|
|
panelRef={panelRef}
|
|
collapsible={activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT}
|
|
minSize={`${contentMinSizePercentage}`}
|
|
maxSize={`${contentMaxSizePercentage}`}
|
|
defaultSize={`${contentMaxSizePercentage}`}
|
|
>
|
|
<main id="main" tabIndex={-1} className="h-full overflow-y-auto outline-hidden">
|
|
{children}
|
|
</main>
|
|
</ResizablePanel>
|
|
<LayoutSidebar
|
|
minSize={`${100 - contentMaxSizePercentage}`}
|
|
maxSize="100"
|
|
defaultSize={`${100 - contentMaxSizePercentage}`}
|
|
/>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
</div>
|
|
|
|
<BannerStack />
|
|
<StudioMobileSheetNav />
|
|
</MobileSheetProvider>
|
|
</ProjectContextProvider>
|
|
</LayoutSidebarProvider>
|
|
</SidebarProvider>
|
|
)
|
|
}
|