Files
supabase/apps/studio/components/interfaces/LocalDropdown.test.tsx
T
Danny White 6f6badae51 fix(eslint): promote require-explicit-tabindex to error (#48170)
## What kind of change does this PR introduce?

Accessibility / lint hardening (Safari keyboard focus).

## What is the current behavior?

`supabase/require-explicit-tabindex` is `'warn'`. Studio’s ratchet was
at 0 but the rule was still ratcheted; www / docs / design-system still
had raw `<button>` / `role="button"` call sites without an explicit
`tabIndex`.

[DEPR-627](https://linear.app/supabase/issue/DEPR-627) · follow-up to
#47984 / #48040

## What is the new behavior?

- Shared config: `'supabase/require-explicit-tabindex': 'error'`
- Swept www / docs / design-system (+ Studio test fixtures the ratchet
skipped)
- Removed the rule from the Studio ratchet + baselines

## To test

Prefer **Safari**. This PR only adds explicit `tabIndex` to raw
`<button>` / `role="button"` call sites — not links, and not controls
that already go through `Button` from `ui`.

### Marketing (`www`) ([staging
link](https://zone-www-dot-com-git-danny-depr-627-promote-req-7ae43c-supabase.vercel.app/))

- [x] Homepage frameworks / dashboard feature tabs — Tab through each
tab button
- [x] Product pages (e.g. `/auth`, `/database`) — section tab switchers
- [x] Narrow viewport — open the hamburger; Tab through menu buttons
- [x] `/partners/catalog` — filter / view controls
- [x] Blog view toggle (list ↔ grid)

### Docs ([staging
link](https://docs-git-danny-depr-627-promote-require-explici-25e46d-supabase.vercel.app/))

- [x] **Desktop (≥ lg):** top-right **⋯ menu** (hamburger icon) — opens
a dropdown that includes Theme. Not a separate theme button.
- [x] **Mobile (< lg):** top-right **hamburger** opens the sheet; close
(X) is the raw button we tagged. Theme inside the sheet uses
`ThemeToggle` / `DropdownMenuTrigger` from `ui` (already supposed to set
`tabIndex`).
- [x] **Code blocks** — copy / language controls
- [x] **Is this helpful?** — X / check are `Button` from `ui` (should
already Tab). After voting **while signed in**, the follow-up “What went
well?” / “How can we improve?” text button is the raw one we tagged.
- [x] **AI Tools → Copy as Markdown** (right rail on a guide) — this is
the only GuidesSidebar control this PR changed. “On this page” TOC items
are **links**, not covered by this lint.
- [x] **Reference docs** (e.g. JS client reference) — section headers
that expand/collapse in the left nav (`Collapsible.Trigger`)
- [x] **Troubleshooting index** — type in the search field, then Tab to
the **clear (X)** control

### Dashboard (`studio`)

No production UI changes in this PR (tests + lint config only). Quick
Safari smoke that prior tabindex work still holds:

- [x] Project sidebar — Tab through primary nav links
- [x] Settings → General — Tab through inputs / buttons
- [x] Storage → Files — Tab a bucket row / file actions
2026-07-23 05:21:15 +10:00

166 lines
4.6 KiB
TypeScript

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
import { describe, expect, it, vi } from 'vitest'
import { LocalDropdown } from './LocalDropdown'
const { mockRouter, mockSetTheme, mockSetLastRoute, mockToggleFeaturePreviewModal } = vi.hoisted(
() => ({
mockRouter: {
pathname: '/project/[ref]/editor',
asPath: '/project/default/editor',
},
mockSetTheme: vi.fn(),
mockSetLastRoute: vi.fn(),
mockToggleFeaturePreviewModal: vi.fn(),
})
)
vi.mock('next/router', () => ({
useRouter: () => mockRouter,
}))
vi.mock('next/link', () => ({
default: ({
href,
children,
onClick,
}: {
href: string
children: ReactNode
onClick?: MouseEventHandler<HTMLAnchorElement>
}) => (
<a href={href} onClick={onClick}>
{children}
</a>
),
}))
vi.mock('next-themes', () => ({
useTheme: () => ({
theme: 'dark',
setTheme: mockSetTheme,
}),
}))
vi.mock('@/state/app-state', () => ({
useAppStateSnapshot: () => ({
setLastRouteBeforeVisitingAccountPage: mockSetLastRoute,
}),
}))
vi.mock('@/components/ui/ProfileImage', () => ({
ProfileImage: () => <div>Avatar</div>,
}))
vi.mock('./App/FeaturePreview/FeaturePreviewContext', () => ({
useFeaturePreviewModal: () => ({
toggleFeaturePreviewModal: mockToggleFeaturePreviewModal,
}),
}))
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() }))
vi.mock('ui', async () => {
const React = await import('react')
return {
Button: ({
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
<button tabIndex={0} {...props}>
{children}
</button>
),
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
DropdownMenu: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({
children,
asChild,
onClick,
onSelect,
}: {
children: ReactNode
asChild?: boolean
onClick?: () => void
onSelect?: () => void
}) =>
asChild ? (
<div>{children}</div>
) : (
<button
tabIndex={0}
onClick={() => {
onClick?.()
onSelect?.()
}}
>
{children}
</button>
),
DropdownMenuLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSeparator: () => <hr />,
DropdownMenuRadioGroup: ({
children,
onValueChange,
}: {
children: ReactNode
onValueChange: (value: string) => void
}) => (
<div>
{React.Children.map(children, (child: ReactNode) =>
React.isValidElement<{ value: string; onClick?: () => void }>(child)
? React.cloneElement(child, {
onClick: () => onValueChange(child.props.value),
})
: (child as ReactElement)
)}
</div>
),
DropdownMenuRadioItem: ({
children,
onClick,
}: {
children: ReactNode
onClick?: () => void
}) => (
<button tabIndex={0} onClick={onClick}>
{children}
</button>
),
Tooltip: ({ children }: { children: ReactNode }) => <div>{children}</div>,
TooltipContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
singleThemes: [
{ value: 'dark', name: 'Dark' },
{ value: 'light', name: 'Light' },
],
}
})
describe('LocalDropdown', () => {
it('shows Preferences, removes Command menu, and keeps theme controls wired', async () => {
const user = userEvent.setup()
render(<LocalDropdown />)
expect(screen.getByText('Preferences')).toBeInTheDocument()
expect(screen.queryByText('Command menu')).not.toBeInTheDocument()
expect(screen.getByText('Theme')).toBeInTheDocument()
await user.click(screen.getByText('Preferences'))
expect(mockSetLastRoute).toHaveBeenCalledWith('/project/default/editor')
await user.click(screen.getByText('Feature previews'))
expect(mockToggleFeaturePreviewModal).toHaveBeenCalledWith(true)
await user.click(screen.getByText('Light'))
expect(mockSetTheme).toHaveBeenCalledWith('light')
})
})