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
This commit is contained in:
Danny White authored and GitHub committed 2026-07-23 05:21:15 +10:00
1 parent 08c4f64c42
commit 6f6badae51
90 files changed
+262 -100

No files matched your search

@@ -146,7 +146,7 @@ export default function Component() {
{['desktop', 'mobile'].map((key) => {
const chart = key as keyof typeof chartConfig
return (
<button
<button tabIndex={0}
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
@@ -7,6 +7,7 @@ export function ClickCounter() {
return (
<button
tabIndex={0}
onClick={() => setCount(count + 1)}
className="whitespace-nowrap rounded-lg bg-gray-700 px-3 py-1 text-sm font-medium tabular-nums text-gray-100 hover:bg-gray-500 hover:text-white"
>
@@ -60,6 +60,7 @@ const ColorPalette = () => {
const isCopied = copied === reference
return (
<button
tabIndex={0}
key={step * 100}
type="button"
onClick={() => handleCopy(reference)}
@@ -147,6 +147,7 @@ export default function Component() {
const chart = key as keyof typeof chartConfig
return (
<button
tabIndex={0}
key={chart}
data-active={activeChart === chart}
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
@@ -434,6 +434,7 @@ export default function FormPatternsPageLayout() {
<p className="text-xs text-foreground-lighter">
Drag and drop or{' '}
<button
tabIndex={0}
type="button"
onClick={() => fileUploadRef.current?.click()}
className="underline cursor-pointer hover:text-foreground-light"
@@ -316,6 +316,7 @@ export default function FormPatternsSidePanel() {
<FormControl className="col-span-6">
<div className="flex gap-4 items-center">
<button
tabIndex={0}
type="button"
onClick={() => uploadButtonRef.current?.click()}
className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-visible:outline-brand-600"
@@ -424,6 +425,7 @@ export default function FormPatternsSidePanel() {
<p className="text-xs text-foreground-lighter">
Drag and drop or{' '}
<button
tabIndex={0}
type="button"
onClick={() => fileUploadRef.current?.click()}
className="underline cursor-pointer hover:text-foreground-light"
@@ -139,6 +139,7 @@ export default function PageLayoutAuthEmails() {
{subPages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -212,6 +213,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
{AUTHENTICATION_TEMPLATES.map((template) => (
<CardContent key={template.title} className="p-0">
<button
tabIndex={0}
type="button"
className="flex w-full items-center justify-between px-6 py-4 text-left transition-colors hover:bg-surface-200"
>
@@ -248,7 +250,11 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
key={template.id}
className="flex h-full w-full items-center justify-between p-0 transition-colors hover:bg-surface-200"
>
<button type="button" className="flex flex-1 flex-col px-6 py-4 text-left">
<button
tabIndex={0}
type="button"
className="flex flex-1 flex-col px-6 py-4 text-left"
>
<h3 className="text-sm text-foreground">{template.title}</h3>
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
</button>
@@ -263,7 +269,12 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
</FormControl>
)}
/>
<button type="button" className="py-6 pr-6" aria-label="Edit template">
<button
tabIndex={0}
type="button"
className="py-6 pr-6"
aria-label="Edit template"
>
<ChevronRight size={16} className="text-foreground-muted" />
</button>
</div>
@@ -261,6 +261,7 @@ export default function PageLayoutEdgeFunction() {
{pages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -20,6 +20,7 @@ export default function PageNavDemo() {
{pages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
@@ -15,6 +15,7 @@ export default function SuccessCheckSelected() {
return (
<button
tabIndex={0}
key={option}
type="button"
aria-pressed={isSelected}
+4 -3
View File
@@ -1,7 +1,7 @@
import { XCircle } from 'lucide-react'
import { type FC, type PropsWithChildren, useState } from 'react'
import { CollapsibleContent, CollapsibleTrigger, Collapsible, cn } from 'ui'
import ApiSchema from '~/components/ApiSchema'
import { XCircle } from 'lucide-react'
import { useState, type FC, type PropsWithChildren } from 'react'
import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui'
interface IOptions {
name?: string
@@ -19,6 +19,7 @@ const ApiSchemaOptions: FC<PropsWithChildren<IOptions>> & OptionsSubComponents =
<Collapsible open={open} onOpenChange={setOpen} className="mt-0">
<CollapsibleTrigger asChild>
<button
tabIndex={0}
className={cn(
'px-5',
'border-t border-l border-r border-default',
@@ -1,14 +1,15 @@
'use client'
import { safeHistoryReplaceState } from '~/lib/historyUtils'
import { useEffect, useReducer, useRef } from 'react'
import { PhoneLoginsItems } from '../Navigation/NavigationMenu/NavigationMenu.constants'
import { IconPanel } from 'ui-patterns/IconPanel'
import { Dialog, DialogContent, DialogHeader, DialogSection, Heading } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
import { PhoneLoginsItems } from '../Navigation/NavigationMenu/NavigationMenu.constants'
import MessageBird from './MessageBirdConfig.mdx'
import TextLocal from './TextLocalConfig.mdx'
import Twilio from './TwilioConfig.mdx'
import Vonage from './VonageConfig.mdx'
import TextLocal from './TextLocalConfig.mdx'
import { safeHistoryReplaceState } from '~/lib/historyUtils'
const reducer = (_, action: (typeof PhoneLoginsItems)[number] | undefined) => {
const url = new URL(document.location.href)
@@ -43,6 +44,7 @@ const AuthSmsProviderConfig = () => {
<div className="grid grid-cols-6 gap-10 not-prose py-8">
{PhoneLoginsItems.map((provider) => (
<button
tabIndex={0}
key={provider.name}
className="col-span-6 xl:col-span-3"
onClick={() => setSelectedProvider(provider)}
@@ -89,6 +89,7 @@ export default function Extensions() {
</ul>
<p className="mt-2">
<button
tabIndex={0}
type="reset"
className="text-xs hover:underline"
onClick={() => setFilters([])}
@@ -28,6 +28,7 @@ const FeedbackButton = forwardRef<
return (
<button
tabIndex={0}
ref={ref}
className={cn(
'mt-0',
+1
View File
@@ -61,6 +61,7 @@ function AiTools({ className }: { className?: string }) {
</h3>
<div className="flex flex-col gap-2">
<button
tabIndex={0}
onClick={copyMarkdown}
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground text-left transition-colors"
>
@@ -142,6 +142,7 @@ const GlobalMobileMenu = ({ open, setOpen }: Props) => {
<div className="flex gap-4 items-center">
<ThemeToggle contentClassName="bg-surface-200" />
<button
tabIndex={0}
onClick={() => setOpen(false)}
type="button"
className="inline-flex items-center justify-center focus:ring-brand bg-surface-100 hover:bg-surface-200 focus:outline-hidden focus:ring-2 focus:ring-inset border border-default bg-surface-100/75 text-foreground-light rounded-sm min-w-[30px] w-[30px] h-[30px]"
@@ -57,6 +57,7 @@ const TopNavBar: FC = () => {
}
/>
<button
tabIndex={0}
title="Menu dropdown button"
className={cn(
buttonVariants({ variant: 'default' }),
@@ -67,6 +67,7 @@ const TopNavDropdown = () => {
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className="flex">
<button
tabIndex={0}
title="Menu dropdown button"
className={cn(
buttonVariants({ variant: 'default' }),
+1 -1
View File
@@ -1,6 +1,5 @@
import { XCircle } from 'lucide-react'
import { FC, PropsWithChildren, useState } from 'react'
import { Badge } from 'ui'
interface IOptions {
@@ -18,6 +17,7 @@ const Options: FC<PropsWithChildren<IOptions>> & OptionsSubComponents = (props)
return (
<div className="mt-0">
<button
tabIndex={0}
className={[
'px-5 py-1',
'border-t border-l border-r border-default',
@@ -125,7 +125,10 @@ function Resources({ schema }: { schema: any }) {
{schema[resource].block.attributes[attribute].type ?? (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-between gap-2">
<button
tabIndex={0}
className="flex items-center justify-between gap-2"
>
Nested type
<PlusCircle size={14} className="shrink-0" />
</button>
@@ -251,7 +254,10 @@ function DataSources({ schema }: { schema: any }) {
{schema[dataSource].block.attributes[attribute].type ?? (
<Popover>
<PopoverTrigger asChild>
<button className="flex items-center justify-between gap-2">
<button
tabIndex={0}
className="flex items-center justify-between gap-2"
>
Nested type
<PlusCircle size={14} />
</button>
@@ -2,7 +2,6 @@
import Link from 'next/link'
import { useState, type PropsWithChildren } from 'react'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
@@ -42,7 +41,10 @@ function MultipleSources({ children, sources }: PropsWithChildren<{ sources: (st
{children}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="block -mt-4 mb-4 ml-auto text-foreground-lighter text-sm focus-visible:outline-hidden focus-visible:underline">
<button
tabIndex={0}
className="block -mt-4 mb-4 ml-auto text-foreground-lighter text-sm focus-visible:outline-hidden focus-visible:underline"
>
View sources
</button>
</DropdownMenuTrigger>
@@ -331,6 +331,7 @@ function CompoundRefLink({
<Collapsible.Root open={open} onOpenChange={setOpen}>
<Collapsible.Trigger asChild disabled={isActive}>
<button
tabIndex={0}
className={cn(
'group',
'cursor-pointer',
@@ -260,6 +260,7 @@ function TroubleshootingFilterInternal({
/>
{searchState && (
<button
tabIndex={0}
className="absolute right-1 top-1/2 -translate-y-1/2 text-foreground-light border-alternative hover:border-stronger rounded-md p-1 transition-colors"
onClick={() => {
setSearchState('')
@@ -46,6 +46,7 @@ export function AnnotatedSpan({
<Tooltip open={open} onOpenChange={onOpenChange}>
<TooltipTrigger asChild onClick={handleClick}>
<button
tabIndex={0}
style={token.htmlStyle}
className={cn(
isTouchDevice &&
@@ -108,6 +109,7 @@ export function CodeCopyButton({ className, content }: { className?: string; con
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
onClick={handleCopy}
onBlur={resetStatus}
className={cn(
@@ -159,6 +161,7 @@ export function CodeBlockControls({ content }: { content: string }) {
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
onClick={toggleWrap}
className={cn('border rounded-md p-1', 'hover:bg-selection transition')}
aria-label={isWrapped ? 'Disable word wrap' : 'Enable word wrap'}
+2
View File
@@ -130,6 +130,7 @@ function ExpandableContent({ children }: { children: ReactNode }) {
)}
</div>
<button
tabIndex={0}
type="button"
onClick={() => setIsExpanded((expanded) => !expanded)}
className="mt-2 text-sm text-brand-link transition-colors hover:text-brand focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted"
@@ -184,6 +185,7 @@ function CopyButton({ label, value }: { label: string; value: string }) {
return (
<button
tabIndex={0}
type="button"
onClick={() => {
copyToClipboard(value, () => {
+1
View File
@@ -204,6 +204,7 @@ const MobileHeader = memo(function MobileHeader(props: MobileHeaderProps) {
)}
>
<button
tabIndex={0}
className={cn(
'h-8 w-8 flex group items-center justify-center mr-1',
mobileMenuOpen && 'mt-0.5'
-2
View File
@@ -20,7 +20,6 @@
"jsx-a11y/anchor-is-valid": 7,
"jsx-a11y/heading-has-content": 2,
"jsx-a11y/no-distracting-elements": 0,
"supabase/require-explicit-tabindex": 0,
"valtio/state-snapshot-rule": 1,
"react-hook-form/no-use-watch": 76
},
@@ -1060,7 +1059,6 @@
"components/layouts/Scaffold.tsx": 2
},
"jsx-a11y/no-distracting-elements": {},
"supabase/require-explicit-tabindex": {},
"valtio/state-snapshot-rule": {
"state/storage-explorer.tsx": 1
},
@@ -77,7 +77,7 @@ describe('IntegrationOverviewTab', () => {
customRender(
<IntegrationOverviewTab
hideRequiredExtensionsSection
actions={<button>Enable webhooks</button>}
actions={<button tabIndex={0}>Enable webhooks</button>}
/>
)
@@ -87,7 +87,9 @@ describe('IntegrationOverviewTab', () => {
})
it('disables actions when extensions are uninstalled and hideRequiredExtensionsSection is false', () => {
customRender(<IntegrationOverviewTab actions={<button>Enable integration</button>} />)
customRender(
<IntegrationOverviewTab actions={<button tabIndex={0}>Enable integration</button>} />
)
const actionsArea = screen.getByText('Enable integration').closest('[aria-disabled]')
expect(actionsArea).toHaveAttribute('aria-disabled', 'true')
@@ -70,7 +70,9 @@ vi.mock('ui', async () => {
children,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
<button {...props}>{children}</button>
<button tabIndex={0} {...props}>
{children}
</button>
),
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
DropdownMenu: ({ children }: { children: ReactNode }) => <div>{children}</div>,
@@ -92,6 +94,7 @@ vi.mock('ui', async () => {
<div>{children}</div>
) : (
<button
tabIndex={0}
onClick={() => {
onClick?.()
onSelect?.()
@@ -125,7 +128,11 @@ vi.mock('ui', async () => {
}: {
children: ReactNode
onClick?: () => void
}) => <button onClick={onClick}>{children}</button>,
}) => (
<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>,
@@ -27,7 +27,7 @@ vi.mock('@/components/ui/ButtonTooltip', () => ({
type: _type,
...props
}: any) => (
<button type="button" {...props}>
<button tabIndex={0} type="button" {...props}>
{icon}
{children}
</button>
@@ -27,7 +27,14 @@ vi.mock('ui', () => ({
children: ReactNode
asChild?: boolean
type?: string
}) => (asChild ? <>{children}</> : <button {...props}>{children}</button>),
}) =>
asChild ? (
<>{children}</>
) : (
<button tabIndex={0} {...props}>
{children}
</button>
),
Card: ({ children }: { children: ReactNode }) => <div>{children}</div>,
CardContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
@@ -29,7 +29,9 @@ const Page = ({ onClose }: { onClose: () => void }) => {
const [open, setOpen] = useState(false)
return (
<ProjectContextProvider projectRef="default">
<button onClick={() => setOpen(true)}>Open</button>
<button tabIndex={0} onClick={() => setOpen(true)}>
Open
</button>
<DeleteBucketModal
visible={open}
@@ -26,7 +26,9 @@ const Page = ({ onClose }: { onClose: () => void }) => {
const [open, setOpen] = useState(false)
return (
<ProjectContextProvider projectRef="default">
<button onClick={() => setOpen(true)}>Open</button>
<button tabIndex={0} onClick={() => setOpen(true)}>
Open
</button>
<EditBucketModal
visible={open}
@@ -29,7 +29,9 @@ const Page = ({ onClose }: { onClose: () => void }) => {
const [open, setOpen] = useState(false)
return (
<ProjectContextProvider projectRef="default">
<button onClick={() => setOpen(true)}>Open</button>
<button tabIndex={0} onClick={() => setOpen(true)}>
Open
</button>
<EmptyBucketModal
visible={open}
@@ -156,7 +156,11 @@ vi.mock('@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarni
ResourceExhaustionWarningBanner: () => null,
}))
vi.mock('@/components/ui/ButtonTooltip', () => ({
ButtonTooltip: ({ children, ...props }: any) => <button {...props}>{children}</button>,
ButtonTooltip: ({ children, ...props }: any) => (
<button tabIndex={0} {...props}>
{children}
</button>
),
}))
vi.mock('@/components/ui/PartnerIcon', () => ({
default: () => <div data-testid="partner-icon" />,
@@ -58,8 +58,12 @@ vi.mock('../EdgeFunctionBlock/EdgeFunctionBlock', () => ({
{showReplaceWarning && (
<div>
<p>An edge function with this name already exists.</p>
<button onClick={onCancelReplace}>Cancel</button>
<button onClick={onConfirmReplace}>Replace function</button>
<button tabIndex={0} onClick={onCancelReplace}>
Cancel
</button>
<button tabIndex={0} onClick={onConfirmReplace}>
Replace function
</button>
</div>
)}
</div>
@@ -73,7 +77,11 @@ vi.mock('./ConfirmFooter', () => ({
}: {
confirmLabel?: string
onConfirm?: () => void
}) => <button onClick={onConfirm}>{confirmLabel ?? 'Confirm'}</button>,
}) => (
<button tabIndex={0} onClick={onConfirm}>
{confirmLabel ?? 'Confirm'}
</button>
),
}))
describe('EdgeFunctionRenderer', () => {
+16 -14
View File
@@ -28,7 +28,7 @@ describe('Shortcut', () => {
it('renders the wrapped child', () => {
render(
<Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={() => {}}>
<button>Open</button>
<button tabIndex={0}>Open</button>
</Shortcut>
)
expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument()
@@ -37,7 +37,7 @@ describe('Shortcut', () => {
it('wraps the child in ShortcutTooltip', () => {
render(
<Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={() => {}}>
<button>Open</button>
<button tabIndex={0}>Open</button>
</Shortcut>
)
const tooltip = screen.getByTestId('shortcut-tooltip')
@@ -50,7 +50,7 @@ describe('Shortcut', () => {
const handler = vi.fn()
render(
<Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler}>
<button>Save</button>
<button tabIndex={0}>Save</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, undefined)
@@ -61,7 +61,7 @@ describe('Shortcut', () => {
const options = { enabled: true, registerInCommandMenu: true }
render(
<Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler} options={options}>
<button>Save</button>
<button tabIndex={0}>Save</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, options)
@@ -75,7 +75,7 @@ describe('Shortcut', () => {
onTrigger={handler}
options={{ enabled: false }}
>
<button>Save</button>
<button tabIndex={0}>Save</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, {
@@ -84,7 +84,7 @@ describe('Shortcut', () => {
rerender(
<Shortcut id={SHORTCUT_IDS.ACTION_BAR_SAVE} onTrigger={handler} options={{ enabled: true }}>
<button>Save</button>
<button tabIndex={0}>Save</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.ACTION_BAR_SAVE, handler, {
@@ -96,14 +96,14 @@ describe('Shortcut', () => {
const handler = vi.fn()
const { rerender } = render(
<Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={handler}>
<button>Go</button>
<button tabIndex={0}>Go</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenLastCalledWith(SHORTCUT_IDS.NAV_HOME, handler, undefined)
rerender(
<Shortcut id={SHORTCUT_IDS.NAV_TABLE_EDITOR} onTrigger={handler}>
<button>Go</button>
<button tabIndex={0}>Go</button>
</Shortcut>
)
expect(mockUseShortcut).toHaveBeenLastCalledWith(
@@ -118,7 +118,7 @@ describe('Shortcut', () => {
it('forwards shortcutId to ShortcutTooltip', () => {
render(
<Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}}>
<button>Home</button>
<button tabIndex={0}>Home</button>
</Shortcut>
)
expect(mockShortcutTooltip).toHaveBeenCalled()
@@ -135,7 +135,7 @@ describe('Shortcut', () => {
sideOffset={8}
delayDuration={100}
>
<button>Home</button>
<button tabIndex={0}>Home</button>
</Shortcut>
)
const props = mockShortcutTooltip.mock.calls.at(-1)![0]
@@ -148,7 +148,7 @@ describe('Shortcut', () => {
it('forwards label override to ShortcutTooltip', () => {
render(
<Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}} label="Go home">
<button>Home</button>
<button tabIndex={0}>Home</button>
</Shortcut>
)
expect(mockShortcutTooltip.mock.calls.at(-1)![0].label).toBe('Go home')
@@ -157,7 +157,7 @@ describe('Shortcut', () => {
it('omits undefined positioning props rather than fabricating them', () => {
render(
<Shortcut id={SHORTCUT_IDS.NAV_HOME} onTrigger={() => {}}>
<button>Home</button>
<button tabIndex={0}>Home</button>
</Shortcut>
)
const props = mockShortcutTooltip.mock.calls.at(-1)![0]
@@ -175,7 +175,9 @@ describe('Shortcut', () => {
const onTrigger = vi.fn()
render(
<Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={onTrigger}>
<button onClick={onClick}>Open</button>
<button tabIndex={0} onClick={onClick}>
Open
</button>
</Shortcut>
)
@@ -188,7 +190,7 @@ describe('Shortcut', () => {
const onTrigger = vi.fn()
render(
<Shortcut id={SHORTCUT_IDS.COMMAND_MENU_OPEN} onTrigger={onTrigger}>
<button>Open</button>
<button tabIndex={0}>Open</button>
</Shortcut>
)
expect(onTrigger).not.toHaveBeenCalled()
-1
View File
@@ -19,7 +19,6 @@
"jsx-a11y/anchor-is-valid",
"jsx-a11y/heading-has-content",
"jsx-a11y/no-distracting-elements",
"supabase/require-explicit-tabindex",
"valtio/state-snapshot-rule",
"react-hook-form/no-use-watch"
]
@@ -92,10 +92,11 @@ vi.mock('@/components/interfaces/SQLEditor/MonacoEditor', async () => {
return (
<div data-testid="monaco-editor">
<div data-testid="monaco-placeholder">{props.placeholder}</div>
<button data-testid="editor-run" onClick={() => props.executeQuery()}>
<button tabIndex={0} data-testid="editor-run" onClick={() => props.executeQuery()}>
run
</button>
<button
tabIndex={0}
data-testid="editor-prompt"
onClick={() =>
props.onPrompt?.({
@@ -131,13 +132,13 @@ vi.mock('@/components/ui/DiffEditor', async () => {
vi.mock('@/components/ui/AIEditor/ResizableAIWidget', () => ({
default: (props: any) => (
<div data-testid={props.id}>
<button data-testid={`${props.id}-accept`} onClick={props.onAccept}>
<button tabIndex={0} data-testid={`${props.id}-accept`} onClick={props.onAccept}>
accept
</button>
<button data-testid={`${props.id}-reject`} onClick={props.onReject}>
<button tabIndex={0} data-testid={`${props.id}-reject`} onClick={props.onReject}>
reject
</button>
<button data-testid={`${props.id}-cancel`} onClick={props.onCancel}>
<button tabIndex={0} data-testid={`${props.id}-cancel`} onClick={props.onCancel}>
cancel
</button>
</div>
@@ -150,13 +151,14 @@ vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/UtilityActions', () => (
UtilityActions: (props: any) => (
<div data-testid="utility-actions">
<button
tabIndex={0}
data-testid="run-button"
disabled={props.isDisabled}
onClick={() => props.executeQuery()}
>
Run
</button>
<button data-testid="prettify-button" onClick={() => props.prettifyQuery()}>
<button tabIndex={0} data-testid="prettify-button" onClick={() => props.prettifyQuery()}>
Prettify
</button>
<span data-testid="is-executing">{String(props.isExecuting)}</span>
@@ -168,7 +170,7 @@ vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel', () => ({
UtilityPanel: (props: any) => (
<div data-testid="utility-panel">
<span data-testid="active-tab">{props.activeTab}</span>
<button data-testid="debug-button" onClick={() => props.onDebug()}>
<button tabIndex={0} data-testid="debug-button" onClick={() => props.onDebug()}>
Debug
</button>
</div>
@@ -179,13 +181,13 @@ vi.mock('@/components/interfaces/SQLEditor/RunQueryWarningModal', () => ({
RunQueryWarningModal: (props: any) =>
props.visible ? (
<div data-testid="warning-modal">
<button data-testid="warn-confirm" onClick={props.onConfirm}>
<button tabIndex={0} data-testid="warn-confirm" onClick={props.onConfirm}>
Confirm
</button>
<button data-testid="warn-confirm-rls" onClick={props.onConfirmWithRLS}>
<button tabIndex={0} data-testid="warn-confirm-rls" onClick={props.onConfirmWithRLS}>
Confirm RLS
</button>
<button data-testid="warn-cancel" onClick={props.onCancel}>
<button tabIndex={0} data-testid="warn-cancel" onClick={props.onCancel}>
Cancel
</button>
</div>
@@ -35,6 +35,7 @@ export function DashboardFeaturesSection({
<div className="flex gap-2" role="tablist">
{tabs.map((tab, index) => (
<button
tabIndex={0}
key={index}
onClick={() => setActiveTabIdx(index)}
aria-selected={index === activeTabIdx}
@@ -58,6 +58,7 @@ export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[
>
{frameworks.map((framework, index) => (
<button
tabIndex={0}
key={framework.name}
role="tab"
aria-selected={index === activeIdx}
@@ -120,6 +120,7 @@ export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const Icon = ICONS[example.icon]
return (
<button
tabIndex={0}
key={example.title}
role="tab"
aria-selected={isActive}
@@ -111,6 +111,7 @@ export function RLSSectionClient({ examples }: { examples: RLSExample[] }) {
const isActive = index === activeIdx
return (
<button
tabIndex={0}
key={example.title}
role="tab"
aria-selected={isActive}
@@ -115,6 +115,7 @@ export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const Icon = ICONS[example.icon]
return (
<button
tabIndex={0}
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
@@ -124,6 +124,7 @@ export function SqlEditorSection() {
const Icon = ICONS[tab.icon]
return (
<button
tabIndex={0}
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
@@ -131,6 +131,7 @@ export function TableEditorSection() {
const Icon = ICONS[tab.icon]
return (
<button
tabIndex={0}
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
@@ -450,11 +450,17 @@ export function VisualDemo() {
>
{/* Toolbar */}
<div className="flex items-center gap-2 px-1.5 h-10 border-b border-default bg-surface-200 shrink-0">
<button className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors">
<button
tabIndex={0}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors"
>
<Filter size={14} strokeWidth={1.5} />
<span>Filter</span>
</button>
<button className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors">
<button
tabIndex={0}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-foreground-light hover:text-foreground transition-colors"
>
<List size={14} strokeWidth={1.5} />
<span>Sort</span>
</button>
@@ -118,6 +118,7 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
const Icon = ICONS[useCase.icon]
return (
<button
tabIndex={0}
key={useCase.label}
role="tab"
aria-selected={isActive}
@@ -114,6 +114,7 @@ export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const Icon = ICONS[example.icon]
return (
<button
tabIndex={0}
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
@@ -116,6 +116,7 @@ export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const Icon = ICONS[example.icon]
return (
<button
tabIndex={0}
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
@@ -107,6 +107,7 @@ export function PermissionsSectionClient({ examples }: { examples: PermissionExa
const isActive = index === activeIdx
return (
<button
tabIndex={0}
key={example.title}
onClick={() => handleTabClick(index)}
className={cn(
@@ -310,6 +310,7 @@ export default function IntegrationsContent({
</span>
<div className="flex items-center rounded-lg border border-muted">
<button
tabIndex={0}
title="Grid view"
onClick={() => setFilters({ view: 'grid' })}
className={cn(
@@ -322,6 +323,7 @@ export default function IntegrationsContent({
<LayoutGrid size={14} />
</button>
<button
tabIndex={0}
title="List view"
onClick={() => setFilters({ view: 'list' })}
className={cn(
@@ -162,6 +162,7 @@ export default function PartnerCatalogDetail({ partner, serializedListings }: Pr
<div className="flex">
{allListings.map((listing, i) => (
<button
tabIndex={0}
key={listing.slug}
type="button"
onClick={() => {
@@ -23,6 +23,7 @@ export function CohortToggle({ eyebrow, options, value, onValueChange }: CohortT
const isActive = option.label === value
return (
<button
tabIndex={0}
key={option.label}
type="button"
onClick={() => onValueChange(option.label)}
@@ -22,6 +22,7 @@ export function YearToggle({ className }: { className?: string }) {
const active = option === year
return (
<button
tabIndex={0}
key={option}
type="button"
role="radio"
@@ -16,6 +16,7 @@ export default function BlogViewToggle({
<div className="flex items-center border border-border rounded-md p-0.5 gap-0.5 bg-surface-100">
{(['list', 'grid'] as BlogView[]).map((v) => (
<button
tabIndex={0}
key={v}
onClick={() => {
setView(v)
@@ -25,6 +25,7 @@ function DraftModeBanner({ onDismiss }: DraftModeBannerProps) {
{onDismiss && (
<div className="order-2 shrink-0 sm:order-3 sm:ml-3">
<button
tabIndex={0}
type="button"
className="-mr-1 flex p-2 rounded-md sm:-mr-2"
onClick={onDismiss}
+20 -10
View File
@@ -25,15 +25,13 @@ const Button = (props: Props) => {
buttonStyles.textShadow = '0px 0px 6px rgba(13, 128, 86, 0.8)'
}
const renderButton = () => (
<button
type="button"
className={`
const sharedClassName = `
group inline-flex items-center rounded-md text-sm
leading-4 transition focus:outline-hidden ${colorClass} ${className}
`}
style={buttonStyles}
>
`
const content = (
<>
<span
className={`
relative transition-all ${url ? textClass : ''}
@@ -41,15 +39,27 @@ const Button = (props: Props) => {
>
{text}
</span>
{url && (
{url && !hideArrow && (
<span className={`ml-2 opacity-0 transition-all group-hover:opacity-100 ${arrowClass}`}>
→
</span>
)}
</button>
</>
)
return url ? <a href={url}>{renderButton()}</a> : renderButton()
if (url) {
return (
<a href={url} className={sharedClassName} style={buttonStyles}>
{content}
</a>
)
}
return (
<button tabIndex={0} type="button" className={sharedClassName} style={buttonStyles}>
{content}
</button>
)
}
export default Button
@@ -57,6 +57,7 @@ export function ChangelogDetailSidebar({ slug, url, labels, className }: Props)
View discussion on GitHub
</a>
<button
tabIndex={0}
type="button"
onClick={() => void copyMarkdown(mdPath)}
className="text-foreground-lighter hover:text-foreground flex items-center gap-1.5 text-left text-xs transition-colors"
@@ -165,6 +165,7 @@ export const SimilarSolvedThreads = ({ threads, parentThreadId }: SimilarSolvedT
<Card className={cn('relative')}>
<CardHeader className={cn('p-0', !isExpanded && 'border-b-0')}>
<button
tabIndex={0}
type="button"
onClick={() => setIsExpanded((prev) => !prev)}
className="flex w-full items-center gap-1.5 px-(--card-padding-x) py-4 text-left text-xs font-mono uppercase text-card-foreground"
@@ -200,6 +201,7 @@ export const SimilarSolvedThreads = ({ threads, parentThreadId }: SimilarSolvedT
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
type="button"
onClick={() => handleThumbClick('positive')}
disabled={isSubmitting}
@@ -214,6 +216,7 @@ export const SimilarSolvedThreads = ({ threads, parentThreadId }: SimilarSolvedT
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
type="button"
onClick={() => handleThumbClick('negative')}
disabled={isSubmitting}
@@ -434,6 +434,7 @@ function ThreadRow({
const isActive = productArea === area
return (
<button
tabIndex={0}
key={area}
onClick={(e) => {
e.preventDefault()
@@ -488,6 +489,7 @@ function ThreadRow({
const isActive = currentStack === tech
return (
<button
tabIndex={0}
key={tech}
onClick={(e) => {
e.preventDefault()
@@ -510,6 +512,7 @@ function ThreadRow({
<Popover>
<PopoverTrigger asChild>
<button
tabIndex={0}
type="button"
onClick={(e) => {
e.preventDefault()
@@ -541,6 +544,7 @@ function ThreadRow({
const isActive = currentStack === tech
return (
<button
tabIndex={0}
key={tech}
onClick={(e) => {
e.preventDefault()
@@ -10,6 +10,7 @@ interface TabProps {
const Tab = ({ isActive, label, onClick }: TabProps) => (
<button
tabIndex={0}
onClick={onClick}
className={`text-left text-lg flex flex-col group gap-1 transition-opacity flex-1 ${
isActive ? 'opacity-100' : 'opacity-50'
+2 -1
View File
@@ -60,7 +60,7 @@ const ImageFadeStack = ({
<div className="absolute bottom-4 right-4 flex gap-3">
{showNavigation &&
images.map((_, index) => (
<button className="text-white" onClick={() => handleNavClick(index)}>
<button tabIndex={0} className="text-white" onClick={() => handleNavClick(index)}>
<div
className={cn(
'w-2 h-2 rounded-full bg-foreground-muted hover:bg-surface-100 transition-colors',
@@ -70,6 +70,7 @@ const ImageFadeStack = ({
</button>
))}
<button
tabIndex={0}
onClick={() => setIsPlaying(!isPlaying)}
className="text-white bg-foreground-light hover:bg-overlay rounded-full p-0.5 transition-colors"
>
@@ -1,9 +1,10 @@
import { SupabaseClient } from '@supabase/supabase-js'
import { ArrowRight } from 'lucide-react'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { cn } from 'ui'
import useConfData from '../hooks/use-conf-data'
import { SupabaseClient } from '@supabase/supabase-js'
import { ArrowRight } from 'lucide-react'
export interface Meetup {
id?: any
@@ -104,6 +105,7 @@ const LW11Meetups = ({ meetups }: { meetups?: Meetup[] }) => {
return (
<>
<button
tabIndex={0}
key={meetup.id}
onClick={() => handleSelectMeetup(meetup)}
onMouseDown={() => handleSelectMeetup(meetup)}
@@ -38,12 +38,14 @@ const LW15MainStage = ({ className }: { className?: string }) => {
<h3 className="text-2xl lg:text-3xl">Main Stage</h3>
<div className="flex xl:hidden items-center gap-2 text-foreground-muted">
<button
tabIndex={0}
onClick={() => controlledSwiper?.slidePrev()}
className="p-2 rounded-full hover:text-foreground border hover:border-foreground transition-colors"
>
<ChevronLeft className="w-4 h-4 -translate-x-px text-current" />
</button>
<button
tabIndex={0}
onClick={() => controlledSwiper?.slideNext()}
className="p-2 rounded-full hover:text-foreground border hover:border-foreground transition-colors"
>
@@ -67,6 +67,7 @@ const LW15TicketPage = ({
<div className="flex gap-2 flex-wrap justify-end">
{TYPO_COLORS.map((color) => (
<button
tabIndex={0}
key={color}
className={cn(
'flex-1 max-w-5 aspect-square rounded-full border flex items-center justify-center transition-all',
@@ -87,6 +88,7 @@ const LW15TicketPage = ({
<div className="flex gap-2 flex-wrap justify-end">
{BG_COLORS.map((color) => (
<button
tabIndex={0}
key={color}
className={cn(
'flex-1 max-w-5 aspect-square rounded-full border flex items-center justify-center transition-all',
@@ -20,6 +20,7 @@ export default function TicketURLCopy({
return (
<button
tabIndex={0}
type="button"
name="Copy"
ref={buttonRef}
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'
import { IconDocumentation, IconMicSolid, IconProductHunt, IconYoutubeSolid, cn } from 'ui'
import { Music } from 'lucide-react'
import Link from 'next/link'
import { StepLink } from './data/lw15_data'
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { cn, IconDocumentation, IconMicSolid, IconProductHunt, IconYoutubeSolid } from 'ui'
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
import { StepLink } from './data/lw15_data'
export const LWSVG = (props: React.SVGProps<SVGSVGElement>) => {
return (
@@ -59,6 +59,7 @@ export const LW15ThemeSwitcher = ({
return (
<div className={cn('flex items-center justify-start gap-2', className)} {...props}>
<button
tabIndex={0}
onClick={() => setTheme('dark')}
className={cn(isDarkMode ? 'text-foreground' : 'text-foreground-lighter')}
>
@@ -66,6 +67,7 @@ export const LW15ThemeSwitcher = ({
</button>{' '}
/
<button
tabIndex={0}
onClick={() => setTheme('light')}
className={cn(!isDarkMode ? 'text-foreground' : 'text-foreground-lighter')}
>
@@ -22,6 +22,7 @@ export const Announcement = (props: AnnouncementType) => {
}
return (
<button
tabIndex={0}
className={[
'flex items-start gap-6 rounded-full border p-3 px-6 pr-8',
containerClasses,
@@ -1,10 +1,12 @@
import { NewspaperIcon } from '@heroicons/react/outline'
import { Article } from '../../types'
const ArticleButton = (props: Article) => {
return (
<div className="mr-2 mb-2 inline-block">
<button
tabIndex={0}
className="
bg-background text-foreground
hover:bg-background-overlay
@@ -1,10 +1,12 @@
import { SparklesIcon } from '@heroicons/react/outline'
import { Article } from '../../types'
const ProductButton = (props: Article) => {
return (
<div className="mr-2 mb-2 inline-block">
<button
tabIndex={0}
className="
text-brand-600
hover:bg-surface-100 flex items-start gap-3 rounded-md border bg-transparent p-3
@@ -1,27 +1,25 @@
import { SparklesIcon } from '@heroicons/react/outline'
import Link from 'next/link'
import { Article } from '../../types'
export const ProductButtonListItem = (props: Article) => {
return (
<Link href={props.url}>
<div className="inline-block">
<button
className="
text-brand-600
<Link
href={props.url}
className="
text-brand-600
group
flex items-start gap-3 bg-transparent
inline-flex items-start gap-3 bg-transparent
transition
drop-shadow-xs"
>
<div className="w-5">
<SparklesIcon strokeWidth={1} />
</div>
<div className="flex flex-col items-start gap-0">
<span className="group-hover:text-brand text-sm">{props.title}</span>
<p className="text-foreground-light text-left text-sm">{props.description}</p>
</div>
</button>
>
<div className="w-5">
<SparklesIcon strokeWidth={1} />
</div>
<div className="flex flex-col items-start gap-0">
<span className="group-hover:text-brand text-sm">{props.title}</span>
<p className="text-foreground-light text-left text-sm">{props.description}</p>
</div>
</Link>
)
@@ -34,6 +34,7 @@ const Player = () => {
</span>
</Link>
<button
tabIndex={0}
onClick={isPlaying ? controls.pause : controls.play}
className="relative outline-border-muted w-7 h-7 opacity-70 hover:opacity-100 rounded-full flex items-center justify-center text-foreground-muted hover:text-foreground-lighter transition-opacity"
style={{
@@ -9,6 +9,7 @@ type HamburgerButtonProps = {
const HamburgerButton = (props: HamburgerButtonProps) => (
<div className="inset-y-0 flex items-center lg:hidden" onClick={() => props.toggleFlyOut()}>
<button
tabIndex={0}
className={cn(
'text-foreground-lighter focus:ring-brand bg-transparent hover:text-foreground-light transition-colors hover:bg-overlay inline-flex items-center justify-center rounded-md p-2 focus:outline-hidden focus:ring-2 focus:ring-inset'
)}
+1
View File
@@ -229,6 +229,7 @@ export const MobileMenu = ({ open, setOpen, menu }: Props) => {
<SupabaseWordmark />
</Link>
<button
tabIndex={0}
onClick={() => setOpen(false)}
type="button"
className="inline-flex items-center justify-center p-2 rounded-md text-foreground-lighter focus:ring-brand hover:text-foreground-light transition-colors focus:outline-hidden focus:ring-2 focus:ring-inset"
@@ -1,8 +1,8 @@
'use client'
import { CheckIcon, ClipboardIcon } from '@heroicons/react/outline'
import Link from 'next/link'
import { useRouter } from 'next/compat/router'
import Link from 'next/link'
import { Fragment, MouseEvent, ReactNode, useEffect, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import {
@@ -126,6 +126,8 @@ const RightClickBrandLogo = () => {
</Link>
) : (
menuItem.type === 'clipboard' && (
// DropdownMenuItem asChild manages roving tabIndex; don't override it.
// eslint-disable-next-line supabase/require-explicit-tabindex -- in-menu item
<button
className="group/menu-item w-full text-left flex justify-between gap-2 items-center"
onClick={() => handleCopyToClipboard(menuItem)}
+1
View File
@@ -16,6 +16,7 @@ interface TabProps {
const Tab = ({ isActive, label, icon, onClick }: TabProps) => (
<button
tabIndex={0}
onClick={onClick}
className={`rounded-full px-4 md:px-3 py-2 md:py-1 nowrap flex group gap-1 transition-all ${
isActive ? 'bg-surface-300' : 'text-foreground-lighter bg-surface-200 hover:bg-overlay-hover'
@@ -218,6 +218,7 @@ const ComputePricingCalculator = ({
<div className="flex items-center gap-2">
{activeInstances.length > 1 && (
<button
tabIndex={0}
aria-label="Remove item"
title="Remove item"
className="p-1 text-lighter hover:text-foreground rounded-sm opacity-0 group-hover:opacity-100 transition-opacity"
@@ -187,6 +187,7 @@ const PricingComputeSection = () => {
</div>
</div>
<button
tabIndex={0}
onClick={() => setShowTable(!showTable)}
className="w-full p-2 border-t border-muted text-foreground focus-visible:outline-brand-600 focus-visible:rounded-b-xl text-sm bg-alternative flex items-center justify-center gap-2"
>
@@ -193,6 +193,7 @@ const FunctionsHero = () => {
>
<CopyToClipboard text="supabase functions new <function-name> && supabase functions deploy <function-name>">
<button
tabIndex={0}
onClick={handleCopy}
className="w-full px-3 py-2 group hover:border-strong flex gap-1 sm:gap-2 items-center bg-alternative rounded-xl border"
>
@@ -76,6 +76,7 @@ const LocalDXImage = ({ isHovered = false }: { isHovered?: boolean }) => {
/>
<CopyToClipboard text="supabase functions serve <function-name>">
<button
tabIndex={0}
onClick={handleCopy}
className="p-3 relative z-10 w-full group hover:border-strong flex gap-2 items-center bg-alternative-200 rounded-xl border overflow-hidden"
>
@@ -1,10 +1,11 @@
import { useState } from 'react'
import { ArrowUpRight } from 'lucide-react'
// Import Swiper React components
import Link from 'next/link'
import { useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Button } from 'ui'
import CodeBlock from '../CodeBlock/CodeBlock'
import { ArrowUpRight } from 'lucide-react'
interface Example {
lang: 'js' | 'py' | 'sql'
@@ -44,6 +45,7 @@ function FunctionsUsecases(props: Props) {
const active = i == apiSwiperActiveIndex
return (
<button
tabIndex={0}
key={`featureHighlighted-${i}`}
className={
'bg-background hover:bg-surface-100 hover:border group rounded-md border px-6 py-4 text-left transition-all' +
@@ -1,11 +1,12 @@
import 'swiper/css'
import React, { useState, useEffect, useRef } from 'react'
import { LazyMotion, domAnimation, m, useAnimation, useInView } from 'framer-motion'
import { cn } from 'ui'
import { Swiper, SwiperSlide } from 'swiper/react'
import Panel from '../Panel'
import { useBreakpoint } from 'common'
import { domAnimation, LazyMotion, m, useAnimation, useInView } from 'framer-motion'
import React, { useEffect, useRef, useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { cn } from 'ui'
import Panel from '../Panel'
interface PanelProps {
id: string
@@ -35,6 +36,7 @@ const TimedPanel = ({
return (
<button
tabIndex={0}
onMouseEnter={() => !isSm && onClick && onClick()}
onClick={() => isSm && onClick && onClick()}
className={cn(
@@ -25,6 +25,7 @@ const Tab = ({ isActive, label, paragraph, onClick, progress, intervalDuration }
return (
<button
tabIndex={0}
onClick={onClick}
className={cn(
'hover:text-foreground w-full text-left text-lg flex flex-col group transition-all',
@@ -21,6 +21,7 @@ interface TabProps {
const Tab = ({ isActive, label, paragraph, onClick, progress, intervalDuration }: TabProps) => (
<button
tabIndex={0}
onClick={onClick}
className={`text-left text-lg flex flex-col group gap-1 transition-all ${
isActive ? 'flex-2 text-foreground' : 'flex-1 text-foreground-light'
@@ -4,6 +4,7 @@ import { useTheme } from 'next-themes'
import Link from 'next/link'
import React, { useRef, useState } from 'react'
import { cn } from 'ui'
import BrowserFrame from './BrowserFrame'
export type Tab = {
@@ -97,6 +98,7 @@ interface TabProps {
const Tab = ({ label, isActive, onClick }: TabProps) => (
<button
tabIndex={0}
onClick={onClick}
aria-selected={isActive}
role="tab"
+6 -1
View File
@@ -291,7 +291,12 @@ function ChangelogIndex({ featured, restIndex, allIndex }: PageProps) {
{CHANGELOG_PRODUCT_TAGS.map(({ slug, label }) => {
const on = selectedTags.has(slug)
return (
<button key={slug} type="button" onClick={() => toggleProductTag(slug)}>
<button
tabIndex={0}
key={slug}
type="button"
onClick={() => toggleProductTag(slug)}
>
<Badge
variant={on ? 'success' : 'default'}
className={cn(!on && 'hover:text-foreground')}
+2
View File
@@ -276,6 +276,7 @@ function FeaturesPage() {
</span>
<div className="flex items-center rounded-lg border border-muted">
<button
tabIndex={0}
title="Grid view"
onClick={() => setViewMode('grid')}
className={cn(
@@ -288,6 +289,7 @@ function FeaturesPage() {
<LayoutGrid size={14} />
</button>
<button
tabIndex={0}
title="Matrix view"
onClick={() => setViewMode('matrix')}
className={cn(
+1
View File
@@ -55,6 +55,7 @@ const Index = () => {
<p className="h1 tracking-[-1px]">{data.hero.title}</p>
<CommandMenuTrigger>
<button
tabIndex={0}
className="
mx-auto
w-full
+1 -1
View File
@@ -43,7 +43,7 @@ const typescriptConfig = {
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'supabase/no-await-before-copy-to-clipboard': 'error',
'supabase/require-explicit-tabindex': 'warn',
'supabase/require-explicit-tabindex': 'error',
},
}