Files
supabase/apps/www/components/SolutionsStickyNav.tsx
Danny White 3bca21b3f8 chore(a11y): convert leftover focus recipes to focus-ring (#48219)
## What kind of change does this PR introduce?

Accessibility cleanup (DEPR-628).

## What is the current behavior?

Leftover call sites still use ad-hoc focus recipes
(`ring-foreground-muted`, `outline-brand`, Dialog/Sheet `focus:` rings,
etc.) instead of the shared utilities from #41575.

## What is the new behavior?

Converts those leftovers across `packages/ui`, Studio, www, docs, and
design-system to `focus-ring`, preferring `focus-visible`. Keeps
documented exceptions (`group-focus-visible`, InputGroup `:has()`).

## To test

Tab through controls (keyboard only). Expect a consistent offset ring on
`:focus-visible`, not a green/brand/custom stack, and no ring animation.

### www (marketing)

Preview:
https://zone-www-dot-com-git-danny-depr-628-focus-ring-fbccf9-supabase.vercel.app

- Global nav on `/`: Product, Developers, Solutions dropdowns; logo;
hamburger + mobile menu
- `/features`: view toggles and feature cards
- `/company`: card links
- `/changelog`: timeline / entry links
- `/partners/catalog`: grid/list toggle and partner cards
- `/pricing`: compute section expand control
- Product / Modules / Solutions sticky navs on product pages (e.g.
`/database`, `/storage`)
- `/state-of-startups`: TwoOptionToggle if present

### docs

Preview:
https://docs-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app

- Any guide page: top nav dropdowns and items
- Narrow viewport: hamburger, then mobile menu links + close
- Guide with PromptPanel / tabs: tab to prompt actions and tab list

### studio (dashboard)

Preview:
https://studio-staging-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app

- Project home: Connect section tiles; drag-handle focus on sortable
sections
- Integrations marketplace (`/project/<ref>/integrations`): featured
cards, list/grid toggle, list rows
- Auth (`/project/<ref>/auth/oauth-apps`,
`/project/<ref>/auth/providers`): open create/edit sheet, tab to close
(X)
- Database policies (`/project/<ref>/database/policies`): open policy
editor sheet, tab to close
- Storage policies (`/project/<ref>/storage/files/policies`): bucket
section links; policy modal close
- Query performance (`/project/<ref>/observability/query-performance`):
info icon buttons on metrics
- Replication pipeline detail (if available): slot lag / status info
icons
- Support (`/support/new`): attachment add/remove controls
- Table editor: spreadsheet import preview checkboxes; row text/JSON
editor TwoOptionToggle
- Any Dialog/Sheet/toast close (X): ring on keyboard focus only, not
mouse click

### design-system

Preview:
https://design-system-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app

- Colour palette swatches (keyboard focus)
- Form patterns sidepanel example: avatar / focusable control in the
example

## Additional context

- Linear: [DEPR-628](https://linear.app/supabase/issue/DEPR-628)
- Follow-ups: form-group CSS (DEPR-629), Storage columns selection
(DEPR-630), ESLint rule (DEPR-632)

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

* **Accessibility & Usability**
* Standardized keyboard focus indicators across navigation, dialogs,
forms, buttons, toggles, links, and tooltips using a consolidated focus
style.
* Improved toggle controls to use proper button semantics (instead of
clickable text), including `aria-pressed`/disabled handling and better
keyboard navigation.

* **Visual Updates**
* Harmonized hover/focus ring visuals across the design system, Studio,
documentation, and marketing pages while preserving existing layout and
interaction behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 08:52:22 +10:00

128 lines
4.2 KiB
TypeScript

import SectionContainer from 'components/Layouts/SectionContainer'
import {
appTypeSolutions,
migrationSolutions,
skillBasedSolutions,
SolutionTypes,
useCaseSolutions,
} from 'data/Solutions'
import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
interface Props {
activeItem: SolutionTypes
className?: string
type?: 'skill-based' | 'use-case' | 'migration' | 'app-type'
}
function SolutionsStickyNav({ type, activeItem, className }: Props) {
const router = useRouter()
const solutions =
type === 'skill-based'
? skillBasedSolutions.solutions
: type === 'use-case'
? useCaseSolutions.solutions
: type === 'app-type'
? appTypeSolutions.solutions
: migrationSolutions.solutions
const items = solutions.map((solution: any) => ({
id: solution.id,
name: solution.text,
href: solution.url,
icon: solution.icon,
}))
const activeSolution = items.find((item) => item.id === activeItem)
return (
<>
<div className="absolute inset-0 z-30 h-full w-full pointer-events-none">
<nav
className={cn(
'sticky z-30 flex items-center bg-background/90 w-full border-b backdrop-blur-xs pointer-events-auto top-[65px]',
className
)}
>
{/* mobile */}
<SectionContainer className="p-2! flex items-start md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="text"
iconRight={<ChevronDown />}
className="w-full min-w-[200px] flex justify-between items-center py-2"
>
{activeSolution?.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="start">
{items.map((item) => (
<DropdownMenuItem
key={`item-${item}`}
onClick={() => router.push(item.href)}
className={cn(item.id === activeItem ? 'text-brand-600' : '')}
>
{item.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SectionContainer>
{/* desktop */}
<SectionContainer className="w-full py-0! hidden md:flex gap-3 items-center">
{items.map((item: any) => {
const isActive = item.id === activeItem
return (
<Link
key={item.name}
className={cn(
'flex items-center gap-1.5 px-2 first:-ml-2 py-4 border-b border-transparent text-sm text-foreground-lighter hover:text-foreground',
'focus-ring focus-visible:text-foreground',
isActive && 'border-foreground-light text-foreground'
)}
href={item.href}
>
{item.icon &&
(typeof item.icon === 'string' ? (
<svg
className="h-4 w-4 group-hover/menu-item:text-foreground group-focus-visible/menu-item:text-foreground"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 18 18"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1"
d={item.icon}
stroke="currentColor"
/>
</svg>
) : (
<item.icon className="h-4 w-4" />
))}
<p>{item.name}</p>
</Link>
)
})}
</SectionContainer>
</nav>
</div>
<div className="h-[53px] not-sr-only" />
</>
)
}
export default SolutionsStickyNav