mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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'}
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
+4
-2
@@ -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>,
|
||||
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -55,6 +55,7 @@ const Index = () => {
|
||||
<p className="h1 tracking-[-1px]">{data.hero.title}</p>
|
||||
<CommandMenuTrigger>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className="
|
||||
mx-auto
|
||||
w-full
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user