mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
fix(studio): chip away explicit-tabindex ratchet debt (#48040)
## What kind of change does this PR introduce? A11y cleanup follow-up to #47984 / [DEPR-626](https://linear.app/supabase/issue/DEPR-626). ## What is the current behavior? Studio had 82 ratcheted `supabase/require-explicit-tabindex` violations (raw `<button>` / `role="button"` without explicit `tabIndex`). ## What is the new behavior? - Explicit `tabIndex={0}` (or disabled → `-1`) on those Studio call sites across nav, `components/ui`, Database, Storage, and the remainder - Ratchet baseline cleared (**82 → 0**) and the rule **removed from the Studio ratchet** (debt is gone; ratchet is temporary) - Rule remains a shared **`warn`** for now — promoting to `error` (and sweeping www/docs/design-system) is a follow-up - Also fixed the learn/ui-library call sites that surfaced while experimenting with error promotion - Small follow-ups where making controls focusable exposed gaps: accessible names, disabled/focus consistency, focus-ring polish on To-test surfaces, home section `KeyboardSensor`, and an E2E locator tightened after `aria-label="Remove column"` Prefer migrating to `Button` from `ui` in future touch-ups; this PR takes the minimal path so Studio debt can stay at zero. ## Additional context Batches landed together so baseline conflicts stayed simple while chipping away: - Hotspots / nav (FirstLevelNav, Marketplace, AttachmentUpload, Column, Tabs, …) - `components/ui` shared - Database + Storage - Remainder **Out of scope / intentional deferrals** - Promoting `supabase/require-explicit-tabindex` to a lint **error** (follow-up after www/docs/design-system sweeps) - Tabs/Radio roving, tooltips, context menus, in-menu items - Full keyboard-accessible tab-close UX (close stays hover + `tabIndex={-1}`; context menu still closes tabs) - Data API docs links (`/project/<ref>/api` redirect) **Reviewer notes** - Rule only flags raw `<button>` / `role="button"` without a `tabIndex` prop. `Button` from `ui` already bakes this in - `tabIndex={-1}` is intentional for disabled controls, in-menu / roving-focus children, and hover-only tab close - For dnd-kit grips, put `tabIndex` **after** `{...attributes}` so it isn’t overwritten (TS2783) ### To test Use **Safari** with macOS Keyboard navigation **off** (System Settings → Keyboard). Chrome once for a sanity pass. For each surface below: Tab until the control is focused, then activate with Enter/Space where relevant. 1. **API Docs side panel** (Table Editor → open a table → **API docs**) - Floating API Docs panel — **not** `/project/<ref>/api` (that redirects to Data API docs; language ToggleGroup uses arrow keys; links are out of scope) - Left nav buttons — Tab through several and activate one; active highlight / navigation still works 2. **Integrations → Marketplace** - Enable **Integrations layout** feature preview first (avatar menu → Feature previews) - `/org/<slug>/integrations` or project integrations marketplace - “Clear all”, grid/list toggles — Tab + activate 3. **Table Editor → create a table → Columns** - Drag handles only appear while **creating** (not when editing an existing table) - Tab to grip / remove (X) / sensitive-data eye if shown 4. **Project Home** — section drag handles - Tab to a grip (visible focus ring) - Optional: Space to pick up, arrows to move, Space/Esc to drop (KeyboardSensor added) - Mouse dnd still works 5. **Storage → Policies** — expand/collapse bucket list chevron (design-system focus ring, no stuck grey open bg) 6. **Support form** (Help → Support) — attachment remove (×) and add-attachment control when visible Disabled controls should be **skipped** by Tab. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Accessibility Improvements** * Improved keyboard navigation throughout Studio by explicitly managing focus (`tabIndex`) across many interactive controls (menus, tabs, tables, charts, dialogs, navigation, and form actions). * Disabled or non-interactive controls are now removed from the tab order (or made unfocusable), while available actions remain reachable. * Ensured `type="button"` on relevant controls to prevent unintended submissions, and refined keyboard focus behavior for various toggles and copy/remove actions. * **Chores** * Updated the ESLint rule baseline configuration to match the new focus behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
37296128e0
commit
e3d7267845
68 files changed
+227
-159
No files matched your search
@@ -93,6 +93,8 @@ const NavigationItem: React.FC<NavigationItemProps> = ({
|
||||
{hasChildren ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={handleButtonClick}
|
||||
className={cn('w-full flex items-center justify-between gap-2 zans', itemClasses)}
|
||||
>
|
||||
|
||||
+2
-69
@@ -19,8 +19,7 @@
|
||||
"jsx-a11y/tabindex-no-positive": 0,
|
||||
"jsx-a11y/anchor-is-valid": 7,
|
||||
"jsx-a11y/heading-has-content": 2,
|
||||
"jsx-a11y/no-distracting-elements": 0,
|
||||
"supabase/require-explicit-tabindex": 82
|
||||
"jsx-a11y/no-distracting-elements": 0
|
||||
},
|
||||
"ruleFiles": {
|
||||
"react-hooks/exhaustive-deps": {
|
||||
@@ -1060,72 +1059,6 @@
|
||||
"jsx-a11y/heading-has-content": {
|
||||
"components/layouts/Scaffold.tsx": 2
|
||||
},
|
||||
"jsx-a11y/no-distracting-elements": {},
|
||||
"supabase/require-explicit-tabindex": {
|
||||
"components/grid/components/grid/ColumnHeader.tsx": 1,
|
||||
"components/grid/components/header/sort/SortRow.tsx": 1,
|
||||
"components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx": 1,
|
||||
"components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx": 1,
|
||||
"components/interfaces/BranchManagement/WorkflowLogs.tsx": 1,
|
||||
"components/interfaces/Connect/OrganizationSelector.tsx": 1,
|
||||
"components/interfaces/ConnectSheet/ConnectConfigSection.tsx": 1,
|
||||
"components/interfaces/ConnectSheet/ConnectionParameters.tsx": 1,
|
||||
"components/interfaces/Database/EnumeratedTypes/EnumeratedTypeValueRow.tsx": 1,
|
||||
"components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx": 1,
|
||||
"components/interfaces/Database/Privileges/PrivilegesTable.tsx": 1,
|
||||
"components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DuckLake/Fields.tsx": 1,
|
||||
"components/interfaces/Database/Replication/DestinationPanel/ReadReplicaForm/ReadReplicaPricingDialog.tsx": 1,
|
||||
"components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx": 1,
|
||||
"components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotStatus.tsx": 1,
|
||||
"components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx": 1,
|
||||
"components/interfaces/Database/Roles/RoleRow.tsx": 1,
|
||||
"components/interfaces/Database/Roles/RolesList.tsx": 2,
|
||||
"components/interfaces/Database/Triggers/TriggerSheet.tsx": 1,
|
||||
"components/interfaces/ExplainVisualizer/ExplainVisualizer.NodeRow.tsx": 1,
|
||||
"components/interfaces/Functions/CommandRender.tsx": 1,
|
||||
"components/interfaces/Functions/EdgeFunctionsListItem.tsx": 1,
|
||||
"components/interfaces/Integrations/Integration/MarkdownContent.tsx": 1,
|
||||
"components/interfaces/Integrations/Marketplace/MarketplaceFilterBar.tsx": 3,
|
||||
"components/interfaces/LogDrains/LogDrainsCard.tsx": 1,
|
||||
"components/interfaces/Observability/ServiceHealthTable.tsx": 1,
|
||||
"components/interfaces/Organization/OrganizationCard.tsx": 1,
|
||||
"components/interfaces/Organization/PrivateApps/Apps/ViewAppSheet/ViewAppSheetInfo.tsx": 1,
|
||||
"components/interfaces/ProjectAPIDocs/FirstLevelNav.tsx": 6,
|
||||
"components/interfaces/ProjectAPIDocs/SecondLevelNav.Layout.tsx": 1,
|
||||
"components/interfaces/ProjectHome/ConnectSection.tsx": 1,
|
||||
"components/interfaces/ProjectHome/SortableSection.tsx": 1,
|
||||
"components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.tsx": 2,
|
||||
"components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx": 2,
|
||||
"components/interfaces/Settings/Integrations/VercelIntegration/VercelIntegrationConnectionForm.tsx": 3,
|
||||
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx": 1,
|
||||
"components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 1,
|
||||
"components/interfaces/Storage/StorageExplorer/MoveItemsModal.tsx": 1,
|
||||
"components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx": 1,
|
||||
"components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx": 1,
|
||||
"components/interfaces/Storage/StorageSettings/StorageSettings.ValidateSizeLimit.tsx": 1,
|
||||
"components/interfaces/Support/AttachmentUpload.tsx": 3,
|
||||
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx": 1,
|
||||
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 3,
|
||||
"components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx": 1,
|
||||
"components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx": 1,
|
||||
"components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/OrgMenuItem.tsx": 1,
|
||||
"components/layouts/ProjectLayout/LayoutHeader/MobileMenuContent/TopLevelRouteItem.tsx": 1,
|
||||
"components/layouts/TableEditorLayout/EntityListItem.tsx": 1,
|
||||
"components/layouts/Tabs/CollapseButton.tsx": 1,
|
||||
"components/layouts/Tabs/SortableTab.tsx": 1,
|
||||
"components/layouts/Tabs/Tabs.tsx": 1,
|
||||
"components/ui/AIAssistantPanel/ModelSelector.tsx": 1,
|
||||
"components/ui/Charts/ChartHighlightActions.tsx": 1,
|
||||
"components/ui/Charts/ComposedChart.utils.tsx": 1,
|
||||
"components/ui/ComputeBadgeWrapper.tsx": 1,
|
||||
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx": 2,
|
||||
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckboxAsync.tsx": 1,
|
||||
"components/ui/DevToolbar/ProjectStatusTab.tsx": 1,
|
||||
"components/ui/DevToolbar/ResourceWarningsTab.tsx": 2,
|
||||
"components/ui/FeaturePreviewBadge.tsx": 1,
|
||||
"components/ui/FilterPopover.tsx": 1,
|
||||
"components/ui/PasswordStrengthBar.tsx": 1,
|
||||
"components/ui/SingleStat.tsx": 1
|
||||
}
|
||||
"jsx-a11y/no-distracting-elements": {}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ export function ColumnHeader<R>({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className="flex items-center"
|
||||
tabIndex={0}
|
||||
onClick={() => openSheet(column.name as string)}
|
||||
>
|
||||
<span className="sr-only">View {column.name} index suggestion</span>
|
||||
|
||||
@@ -40,6 +40,7 @@ const SortRow = ({ index, columnName, sort, onDelete, onToggle }: SortRowProps)
|
||||
{...listeners}
|
||||
className="opacity-50 hover:opacity-100 transition cursor-grab text-foreground"
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<GripVertical size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
@@ -251,6 +251,7 @@ const FeaturePreviewItem = ({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
key={feature.key}
|
||||
onClick={() => selectFeaturePreview(feature.key)}
|
||||
className={cn(
|
||||
|
||||
@@ -128,6 +128,7 @@ const FunctionDiff = ({
|
||||
<li key={fileInfo.key} className="flex">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setActiveFileKey(fileInfo.key)}
|
||||
className={cn(
|
||||
'flex-1 text-left text-xs px-4 py-2 flex items-center gap-2',
|
||||
|
||||
@@ -115,6 +115,7 @@ export const WorkflowLogs = ({ branch }: WorkflowLogsProps) => {
|
||||
<li key={workflowRun.id} className="flex justify-between px-4 py-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={workflowRun.id === projectRef ? -1 : 0}
|
||||
disabled={workflowRun.id === projectRef}
|
||||
onClick={() => setSelectedWorkflowRun(workflowRun)}
|
||||
className="flex items-center gap-2 w-full justify-between"
|
||||
|
||||
@@ -193,6 +193,7 @@ const ConnectOrganizationButton = ({
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
aria-pressed={selected}
|
||||
|
||||
@@ -253,6 +253,7 @@ export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) {
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onChange(mode.id)}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 p-4 transition-colors border-r last:border-r-0',
|
||||
|
||||
@@ -23,6 +23,9 @@ export const ConnectionParameters = ({ parameters, onCopy }: ConnectionParameter
|
||||
<span className="text-foreground-lighter">{param.key}:</span>
|
||||
<span className="ml-1 text-foreground">{param.value}</span>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Copy ${param.key}`}
|
||||
onClick={() => {
|
||||
copyToClipboard(param.value, () => {
|
||||
setCopiedMap((prev) => ({ ...prev, [param.key]: true }))
|
||||
@@ -34,7 +37,7 @@ export const ConnectionParameters = ({ parameters, onCopy }: ConnectionParameter
|
||||
}}
|
||||
className={cn(
|
||||
'text-foreground-lighter',
|
||||
'ml-2 opacity-0 group-hover/param:opacity-100',
|
||||
'ml-2 opacity-0 group-hover/param:opacity-100 focus-visible:opacity-100',
|
||||
'hover:text-foreground rounded-xs p-1',
|
||||
copiedMap[param.key] && 'opacity-100',
|
||||
'transition-all'
|
||||
|
||||
@@ -52,6 +52,7 @@ const EnumeratedTypeValueRow = <TFieldValues extends FieldValues>({
|
||||
isDisabled ? 'text-foreground-lighter cursor-default!' : 'text-foreground'
|
||||
}`}
|
||||
type="button"
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<GripVertical size={16} strokeWidth={1.5} />
|
||||
|
||||
+1
@@ -105,6 +105,7 @@ export const PolicyEditorPanelHeader = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
tabIndex={0}
|
||||
aria-expanded={showTools}
|
||||
aria-controls="ai-chat-assistant"
|
||||
className={cn(
|
||||
|
||||
@@ -73,6 +73,7 @@ const PrivilegesTable = ({
|
||||
<Table.tr key={column.column_id}>
|
||||
<Table.td>
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={() => handleClickColumnName(column.column_id)}
|
||||
className="block w-full truncate text-left text-foreground"
|
||||
>
|
||||
|
||||
+1
@@ -76,6 +76,7 @@ const DuckLakeModeSelector = ({
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => onChange(option.value)}
|
||||
|
||||
+1
@@ -37,6 +37,7 @@ export const ReadReplicaPricingDialog = () => {
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={cn(InlineLinkClassName, 'cursor-pointer text-foreground-light')}
|
||||
>
|
||||
Learn more
|
||||
|
||||
+1
@@ -133,6 +133,7 @@ export const SlotLagMetricsList = ({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label={`What is ${field.label}`}
|
||||
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
|
||||
>
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ export const SlotStatusLegend = () => {
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="What do the slot statuses mean?"
|
||||
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
|
||||
>
|
||||
|
||||
+5
-1
@@ -81,7 +81,11 @@ export const TableReplicationRow = ({
|
||||
<div className="text-sm text-foreground">
|
||||
{statusConfig.description}{' '}
|
||||
{isErrorState && 'reason' in table.state && (
|
||||
<button className={InlineLinkClassName} onClick={() => onSelectShowError()}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className={InlineLinkClassName}
|
||||
onClick={() => onSelectShowError()}
|
||||
>
|
||||
View error.
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -117,6 +117,7 @@ export const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps
|
||||
<button
|
||||
id={`collapsible-trigger-${role.id}`}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="group flex w-full items-center justify-between rounded-sm py-3 px-card text-foreground"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -149,6 +149,7 @@ export const RolesList = () => {
|
||||
/>
|
||||
<div className="flex items-center border border-strong rounded-full w-min h-[26px]">
|
||||
<button
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'text-xs w-[80px] h-full text-center rounded-l-full flex items-center justify-center transition',
|
||||
filterType === 'all'
|
||||
@@ -161,6 +162,7 @@ export const RolesList = () => {
|
||||
</button>
|
||||
<div className="h-full w-px border-r border-strong"></div>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'text-xs w-[80px] h-full text-center rounded-r-full flex items-center justify-center transition',
|
||||
filterType === 'active'
|
||||
|
||||
@@ -420,6 +420,7 @@ export const TriggerSheet = ({
|
||||
{function_name.length === 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'relative w-full rounded-sm border border-default',
|
||||
'bg-surface-200 px-5 py-1 shadow-xs transition-all',
|
||||
|
||||
@@ -49,6 +49,7 @@ export function ExplainNodeRow({ node, depth, maxDuration }: ExplainNodeRowProps
|
||||
{/* Expand/collapse button */}
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={canExpand ? 0 : -1}
|
||||
onClick={() => canExpand && setIsExpanded(!isExpanded)}
|
||||
disabled={!canExpand}
|
||||
className={cn(
|
||||
|
||||
@@ -31,6 +31,7 @@ const Command = ({ item }: any) => {
|
||||
<span>{item.jsx ? item.jsx() : null} </span>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="text-foreground-lighter hover:text-foreground"
|
||||
onClick={() => {
|
||||
function onCopy(value: any) {
|
||||
|
||||
@@ -74,6 +74,7 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="text-foreground-lighter hover:text-foreground transition"
|
||||
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
||||
function onCopy(value: string) {
|
||||
|
||||
@@ -58,6 +58,8 @@ export const MarkdownContent = ({
|
||||
{supportExpanding && (
|
||||
<div className={cn('bottom-0', !isExpanded ? 'absolute' : 'relative mt-3')}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="text-foreground-light hover:text-foreground underline text-sm"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
|
||||
@@ -201,18 +201,21 @@ export const MarketplaceFilterBar = ({
|
||||
</DropdownMenu>
|
||||
|
||||
{showClear && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
size="tiny"
|
||||
onClick={onClearFilters}
|
||||
className="px-1 text-xs text-foreground-light underline-offset-2 hover:underline"
|
||||
className="text-foreground-light"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex rounded-md border">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Grid view"
|
||||
onClick={() => onViewModeChange('grid')}
|
||||
className={cn(
|
||||
@@ -227,6 +230,7 @@ export const MarketplaceFilterBar = ({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="List view"
|
||||
onClick={() => onViewModeChange('list')}
|
||||
className={cn(
|
||||
|
||||
@@ -16,7 +16,7 @@ export const LogDrainsCard = ({
|
||||
onClick,
|
||||
}: LogDrainsCardProps) => {
|
||||
return (
|
||||
<button className="w-full h-full text-left" onClick={onClick}>
|
||||
<button type="button" tabIndex={0} className="w-full h-full text-left" onClick={onClick}>
|
||||
<Card className="p-6 cursor-pointer hover:bg-surface-200 hover:border-strong transition-colors h-48">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-start justify-between w-full">
|
||||
|
||||
@@ -127,6 +127,7 @@ const ServiceCell = ({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="relative z-10 text-foreground-lighter hover:text-foreground-light transition-colors shrink-0"
|
||||
aria-label={`About ${service.name}`}
|
||||
>
|
||||
|
||||
@@ -118,6 +118,7 @@ export const CreateOrganizationCard = ({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
|
||||
+2
@@ -37,6 +37,8 @@ function CopyableId({ id, label }: { id: string; label: string }) {
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-light px-1 -ml-1"
|
||||
>
|
||||
|
||||
@@ -86,6 +86,7 @@ export const FirstLevelNav = (): ReactNode => {
|
||||
return (
|
||||
<Fragment key={item.key}>
|
||||
<button
|
||||
tabIndex={0}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md',
|
||||
@@ -168,6 +169,7 @@ const Subsections = ({ category }: SubsectionsProps): ReactNode => {
|
||||
{snippets.map((snippet) => (
|
||||
<button
|
||||
key={snippet.key}
|
||||
tabIndex={0}
|
||||
className={MENU_BUTTON_CLASSES}
|
||||
onClick={() => {
|
||||
navigateToSection(snippet.key)
|
||||
@@ -197,6 +199,7 @@ const TablesSubsections = (): ReactNode => {
|
||||
{tables.map((table) => (
|
||||
<button
|
||||
key={table.name}
|
||||
tabIndex={0}
|
||||
className={MENU_BUTTON_CLASSES}
|
||||
onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.ENTITIES, table.name])}
|
||||
>
|
||||
@@ -226,6 +229,7 @@ const DbFunctionsSubsections = (): ReactNode => {
|
||||
{functions.map((fn) => (
|
||||
<button
|
||||
key={fn.name}
|
||||
tabIndex={0}
|
||||
className={MENU_BUTTON_CLASSES}
|
||||
onClick={() =>
|
||||
snap.setActiveDocsSection([API_DOCS_CATEGORIES.STORED_PROCEDURES, fn.name])
|
||||
@@ -250,6 +254,7 @@ const BucketButton = ({ item: bucket, style }: RowComponentBaseProps<Bucket>) =>
|
||||
return (
|
||||
<button
|
||||
key={bucket.name}
|
||||
tabIndex={0}
|
||||
className={cn(MENU_BUTTON_CLASSES, 'py-1')}
|
||||
style={style}
|
||||
onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.STORAGE, bucket.name])}
|
||||
@@ -301,6 +306,7 @@ const EdgeFunctionsSubsections = (): ReactNode => {
|
||||
{(edgeFunctions ?? []).map((fn) => (
|
||||
<button
|
||||
key={fn.name}
|
||||
tabIndex={0}
|
||||
className={MENU_BUTTON_CLASSES}
|
||||
onClick={() => snap.setActiveDocsSection([API_DOCS_CATEGORIES.EDGE_FUNCTIONS, fn.name])}
|
||||
>
|
||||
|
||||
@@ -153,6 +153,7 @@ const MenuItems = ({ category, menuItemFilter }: MenuItemsProps) => {
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
tabIndex={0}
|
||||
className="w-full text-left text-sm text-foreground-light px-4 hover:text-foreground"
|
||||
onClick={() => navigateToSection(item.key)}
|
||||
>
|
||||
|
||||
@@ -77,6 +77,15 @@ export const ConnectSection = () => {
|
||||
<button
|
||||
key={action.id}
|
||||
type="button"
|
||||
tabIndex={
|
||||
(action.requiresActiveProject ?? true)
|
||||
? !isActiveHealthy
|
||||
? -1
|
||||
: 0
|
||||
: !selectedProject?.ref
|
||||
? -1
|
||||
: 0
|
||||
}
|
||||
disabled={
|
||||
(action.requiresActiveProject ?? true) ? !isActiveHealthy : !selectedProject?.ref
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { useFlag, useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useRef } from 'react'
|
||||
@@ -42,7 +54,10 @@ export const ProjectHome = () => {
|
||||
|
||||
const UsageSection = showHomepageUsageDeltas ? ProjectUsageSectionDeltas : ProjectUsageSection
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
)
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
@@ -23,10 +23,16 @@ export const SortableSection = ({ id, children }: SortableSectionProps) => {
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="relative will-change-transform">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Drag to reorder section"
|
||||
className="absolute -left-6 top-2 text-foreground-muted hover:text-foreground focus:outline-hidden cursor-grab active:cursor-grabbing"
|
||||
className={cn(
|
||||
'absolute -left-6 top-2 text-foreground-muted hover:text-foreground cursor-grab active:cursor-grabbing',
|
||||
'rounded-sm outline-hidden',
|
||||
'focus-visible:outline-solid focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-border-strong'
|
||||
)}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
tabIndex={0}
|
||||
>
|
||||
<GripVertical size={14} />
|
||||
</button>
|
||||
|
||||
+2
@@ -103,6 +103,7 @@ export const QueryInsightsChart = ({
|
||||
<button
|
||||
key={item.dataKey}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSeries(item.dataKey)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-[11px] transition-colors cursor-pointer',
|
||||
@@ -124,6 +125,7 @@ export const QueryInsightsChart = ({
|
||||
{hasSelection && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSeries(selDataKey)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-[11px] transition-colors cursor-pointer',
|
||||
|
||||
@@ -69,6 +69,7 @@ export const QueryPerformanceMetrics = () => {
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="How are slow queries calculated?"
|
||||
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
|
||||
onClick={(e) => {
|
||||
@@ -89,6 +90,7 @@ export const QueryPerformanceMetrics = () => {
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label={`What is ${card.title}?`}
|
||||
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
|
||||
onClick={(e) => {
|
||||
|
||||
+20
-29
@@ -295,35 +295,26 @@ const VercelIntegrationConnectionForm = ({
|
||||
</FormControl>
|
||||
<FormDescription className="col-start-5 col-span-8 text-xs">
|
||||
e.g.{' '}
|
||||
<code
|
||||
className="cursor-pointer"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
field.onChange('NEXT_PUBLIC_')
|
||||
}}
|
||||
>
|
||||
NEXT_PUBLIC_
|
||||
</code>
|
||||
,{' '}
|
||||
<code
|
||||
className="cursor-pointer"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
field.onChange('VITE_PUBLIC_')
|
||||
}}
|
||||
>
|
||||
VITE_PUBLIC_
|
||||
</code>
|
||||
,{' '}
|
||||
<code
|
||||
className="cursor-pointer"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
field.onChange('PUBLIC_')
|
||||
}}
|
||||
>
|
||||
PUBLIC_
|
||||
</code>
|
||||
{(
|
||||
[
|
||||
['NEXT_PUBLIC_', 'NEXT_PUBLIC_'],
|
||||
['VITE_PUBLIC_', 'VITE_PUBLIC_'],
|
||||
['PUBLIC_', 'PUBLIC_'],
|
||||
] as const
|
||||
).map(([label, value], index) => (
|
||||
<span key={value}>
|
||||
{index > 0 && ', '}
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
disabled={disabled}
|
||||
className="font-mono text-xs rounded-sm bg-surface-200 px-1 py-0.5 hover:bg-surface-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => field.onChange(value)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
, etc.
|
||||
</FormDescription>
|
||||
|
||||
|
||||
+1
@@ -97,6 +97,7 @@ const HeaderBreadcrumbs = ({
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="max-w-24 truncate border-0 bg-transparent p-0 text-left text-sm text-foreground-lighter transition-colors hover:text-foreground focus-visible:text-foreground"
|
||||
onClick={() => selectBreadcrumb(crumb.index)}
|
||||
>
|
||||
|
||||
@@ -115,6 +115,7 @@ export const FileExplorerRowEditing = ({
|
||||
onBlur={(event) => onSaveItemName(itemName.trim() || item.name, event)}
|
||||
/>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
className="hidden"
|
||||
type="submit"
|
||||
onClick={(event) => onSaveItemName(itemName.trim() || item.name, event)}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const MoveItemsModal = ({
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
<button className="hidden" type="submit" onClick={onConfirmMove} />
|
||||
<button tabIndex={-1} className="hidden" type="submit" onClick={onConfirmMove} />
|
||||
</form>
|
||||
</DialogSection>
|
||||
<DialogFooter>
|
||||
|
||||
+8
-12
@@ -68,18 +68,14 @@ export const BucketsPolicies = ({
|
||||
Write policies for each bucket to control access to the bucket and its contents
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
<CollapsibleTrigger asChild>
|
||||
<button>
|
||||
<span className="sr-only">Toggle bucket list</span>
|
||||
<ChevronUp
|
||||
size={14}
|
||||
className={cn(
|
||||
!expanded && 'rotate-180',
|
||||
'transition',
|
||||
'text-foreground-light hover:text-foreground'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<CollapsibleTrigger
|
||||
aria-label="Toggle bucket list"
|
||||
className={cn(
|
||||
'rounded-md p-1 text-foreground-light hover:text-foreground',
|
||||
'outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background'
|
||||
)}
|
||||
>
|
||||
<ChevronUp size={14} className={cn(!expanded && 'rotate-180', 'transition')} />
|
||||
</CollapsibleTrigger>
|
||||
</PageSectionMeta>
|
||||
<CollapsibleContent>
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export const StoragePolicyEditorModalTitle = ({
|
||||
<div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={onSelectBackFromTemplates}
|
||||
className={cn(
|
||||
'cursor-pointer rounded-xs opacity-20 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-foreground-muted',
|
||||
|
||||
+1
@@ -123,6 +123,7 @@ export const ValidateSizeLimit = ({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={isValidating ? -1 : 0}
|
||||
className={cn(
|
||||
'mt-2 ml-auto text-foreground-lighter',
|
||||
'w-fit! text-xs',
|
||||
|
||||
@@ -22,6 +22,19 @@ import { useProfile } from '@/lib/profile'
|
||||
|
||||
const MAX_ATTACHMENTS = 5
|
||||
|
||||
const removeAttachmentButtonClassName = cn(
|
||||
'absolute -top-1 -right-1 size-4 shrink-0 rounded-full bg-red-900 p-0 cursor-pointer',
|
||||
'outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background'
|
||||
)
|
||||
|
||||
const RemoveAttachmentIcon = () => (
|
||||
<X
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 text-contrast"
|
||||
/>
|
||||
)
|
||||
|
||||
const uploadAttachments = async ({ userId, files }: { userId: string; files: File[] }) => {
|
||||
const supportSupabaseClient = createSupportStorageClient()
|
||||
|
||||
@@ -209,14 +222,12 @@ export function AttachmentUploadDisplay({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Remove attachment"
|
||||
className={cn(
|
||||
'flex h-4 w-4 items-center justify-center rounded-full bg-red-900',
|
||||
'absolute -top-1 -right-1 cursor-pointer'
|
||||
)}
|
||||
className={removeAttachmentButtonClassName}
|
||||
onClick={() => removeFileUpload(idx)}
|
||||
>
|
||||
<X aria-hidden="true" size={10} strokeWidth={3} className="text-contrast" />
|
||||
<RemoveAttachmentIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -229,14 +240,12 @@ export function AttachmentUploadDisplay({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Remove attachment"
|
||||
className={cn(
|
||||
'flex h-4 w-4 items-center justify-center rounded-full bg-red-900',
|
||||
'absolute -top-1 -right-1 cursor-pointer'
|
||||
)}
|
||||
className={removeAttachmentButtonClassName}
|
||||
onClick={() => removeFileUpload(idx)}
|
||||
>
|
||||
<X aria-hidden="true" size={10} strokeWidth={3} className="text-contrast" />
|
||||
<RemoveAttachmentIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -245,9 +254,12 @@ export function AttachmentUploadDisplay({
|
||||
{!isFull && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Add attachment"
|
||||
className={cn(
|
||||
'border border-stronger opacity-50 transition hover:opacity-100',
|
||||
'group flex h-14 w-14 cursor-pointer items-center justify-center rounded-sm'
|
||||
'group flex h-14 w-14 cursor-pointer items-center justify-center rounded-sm',
|
||||
'outline-hidden focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background'
|
||||
)}
|
||||
onClick={addFile}
|
||||
>
|
||||
|
||||
+1
@@ -178,6 +178,7 @@ export const SpreadsheetImportPreview = ({
|
||||
{errorData !== undefined ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="flex items-center space-x-2 cursor-pointer focus:outline-hidden focus-visible:ring-2 focus-visible:ring-primary"
|
||||
onClick={() => onSelectExpandError(key)}
|
||||
aria-expanded={isExpanded}
|
||||
|
||||
+9
-1
@@ -126,6 +126,7 @@ export const Column = ({
|
||||
ref={setActivatorNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
tabIndex={0}
|
||||
className="opacity-50 hover:opacity-100 disabled:hover:opacity-50 transition cursor-grab text-foreground"
|
||||
type="button"
|
||||
>
|
||||
@@ -163,6 +164,7 @@ export const Column = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
const SENSITIVE_DATA_MARKER = '[SENSITIVE]'
|
||||
|
||||
@@ -448,7 +450,13 @@ export const Column = ({
|
||||
</div>
|
||||
{!hasImportContent && (
|
||||
<div className="flex w-[5%] justify-end">
|
||||
<button className="cursor-pointer" onClick={() => onRemoveColumn()}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Remove column"
|
||||
className="cursor-pointer"
|
||||
onClick={() => onRemoveColumn()}
|
||||
>
|
||||
<X size={16} strokeWidth={1} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +313,7 @@ const EdgeFunctionDetailsLayout = ({
|
||||
onOpenChange={setIsTimestampHoverCardOpen}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<button type="button" className="flex items-center gap-2 group">
|
||||
<button type="button" tabIndex={0} className="flex items-center gap-2 group">
|
||||
<Clock size={16} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4">
|
||||
{updatedRelative ?? 'Deploy status unavailable'}
|
||||
|
||||
+3
@@ -252,6 +252,9 @@ export const FeedbackWidget = ({ onClose, onSwitchToIssueOptions }: FeedbackWidg
|
||||
className="cursor-pointer rounded-sm h-[26px] w-[26px] border border-control relative bg-cover bg-center bg-no-repeat"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label="Remove screenshot"
|
||||
className={[
|
||||
'cursor-pointer rounded-full bg-red-900 h-3 w-3',
|
||||
'flex items-center justify-center absolute -top-1 -right-1',
|
||||
|
||||
+5
@@ -39,6 +39,7 @@ export function OrgMenuItem({
|
||||
{onSubmenuClick ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
data-active={isActive}
|
||||
onClick={() => onSubmenuClick?.(item)}
|
||||
disabled={disabled}
|
||||
@@ -46,6 +47,10 @@ export function OrgMenuItem({
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : disabled ? (
|
||||
<span aria-disabled="true" data-active={isActive} className={menuButtonClass}>
|
||||
{content}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href}
|
||||
|
||||
+15
-8
@@ -40,6 +40,7 @@ export function TopLevelRouteItem({
|
||||
{hasItems ? (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={route.disabled ? -1 : 0}
|
||||
data-active={isActive}
|
||||
onClick={() => onTopLevelClick(route)}
|
||||
disabled={route.disabled}
|
||||
@@ -48,14 +49,20 @@ export function TopLevelRouteItem({
|
||||
{content}
|
||||
</button>
|
||||
) : route.link ? (
|
||||
<Link
|
||||
href={route.link}
|
||||
onClick={onCloseSheet}
|
||||
data-active={isActive}
|
||||
className={menuButtonClass}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
route.disabled ? (
|
||||
<span aria-disabled="true" data-active={isActive} className={menuButtonClass}>
|
||||
{content}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={route.link}
|
||||
onClick={onCloseSheet}
|
||||
data-active={isActive}
|
||||
className={menuButtonClass}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<span data-active={false} className={cn(menuButtonClass, 'cursor-default')}>
|
||||
{content}
|
||||
|
||||
@@ -184,6 +184,7 @@ export const EntityListItem = ({
|
||||
id={String(entity.id)}
|
||||
href={buildTableEditorUrl({ projectRef, tableId: entity.id, schema: entity.schema })}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`View ${entity.name}`}
|
||||
className={cn(
|
||||
TreeViewItemVariant({
|
||||
|
||||
@@ -20,6 +20,7 @@ export function CollapseButton({ hideTabs }: { hideTabs: boolean }) {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'hidden md:flex items-center justify-center w-10 h-(--header-height) hover:bg-surface-100 shrink-0',
|
||||
!hideTabs && 'border-b border-b-default'
|
||||
|
||||
@@ -82,9 +82,7 @@ export const ModelSelector = ({ selectedModel, onSelectModel }: ModelSelectorPro
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Badge role="button" variant="warning">
|
||||
Upgrade
|
||||
</Badge>
|
||||
<Badge variant="warning">Upgrade</Badge>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
|
||||
@@ -134,6 +134,7 @@ export const ChartHighlightActions = ({
|
||||
<DropdownMenuItem asChild key={action.id} disabled={disabled} className={cn('group')}>
|
||||
<button
|
||||
disabled={disabled}
|
||||
tabIndex={-1}
|
||||
onClick={() => ctx && action.onSelect({ ...ctx })}
|
||||
className="w-full flex items-center gap-1.5"
|
||||
>
|
||||
|
||||
@@ -368,6 +368,7 @@ export const CustomLabel = ({
|
||||
return (
|
||||
<button
|
||||
key={entry.name}
|
||||
tabIndex={0}
|
||||
className="flex md:flex-col gap-1 md:gap-0 w-fit text-foreground rounded-lg hover:bg-background-overlay-hover"
|
||||
onMouseOver={() => handleMouseEnter(entry.name)}
|
||||
onMouseOutCapture={handleMouseLeave}
|
||||
|
||||
@@ -189,8 +189,6 @@ export const ComputeBadgeWrapper = ({
|
||||
<Button
|
||||
asChild
|
||||
variant="default"
|
||||
type="button"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
track('compute_badge_upgrade_clicked', {
|
||||
computeSize: computeSize ?? 'unknown',
|
||||
|
||||
@@ -123,6 +123,7 @@ export function DataTableFilterCheckbox<TData>({
|
||||
{hasNested && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => toggleExpanded(optionKey)}
|
||||
@@ -147,10 +148,11 @@ export function DataTableFilterCheckbox<TData>({
|
||||
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => column?.setFilterValue([option.value])}
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground hover:text-foreground',
|
||||
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex items-center cursor-pointer',
|
||||
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex group-focus-within:flex items-center cursor-pointer',
|
||||
'ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
||||
)}
|
||||
>
|
||||
|
||||
+2
-1
@@ -118,10 +118,11 @@ export function DataTableFilterCheckboxAsync<TData>({
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => column?.setFilterValue([option.value])}
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground hover:text-foreground',
|
||||
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex items-center cursor-pointer',
|
||||
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex group-focus-within:flex items-center cursor-pointer',
|
||||
'ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -124,6 +124,7 @@ export const ProjectStatusTab = () => {
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={isDisabled || !hasOverride}
|
||||
tabIndex={isDisabled || !hasOverride ? -1 : 0}
|
||||
className="text-xs text-foreground-lighter hover:text-foreground transition underline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Reset to real data
|
||||
|
||||
@@ -138,6 +138,7 @@ export const ResourceWarningsTab = () => {
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={isDisabled}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
className="text-xs text-foreground-lighter hover:text-foreground transition underline disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Reset to real data
|
||||
@@ -157,6 +158,7 @@ export const ResourceWarningsTab = () => {
|
||||
<SeverityButton
|
||||
active={!isReadOnly}
|
||||
variant="off"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleReadOnlyChange(false)}
|
||||
>
|
||||
Off
|
||||
@@ -164,6 +166,7 @@ export const ResourceWarningsTab = () => {
|
||||
<SeverityButton
|
||||
active={isReadOnly}
|
||||
variant="critical"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleReadOnlyChange(true)}
|
||||
>
|
||||
On
|
||||
@@ -178,6 +181,7 @@ export const ResourceWarningsTab = () => {
|
||||
<SeverityButton
|
||||
active={severities[key] === null}
|
||||
variant="off"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleSeverityChange(key, null)}
|
||||
>
|
||||
Off
|
||||
@@ -185,6 +189,7 @@ export const ResourceWarningsTab = () => {
|
||||
<SeverityButton
|
||||
active={severities[key] === 'warning'}
|
||||
variant="warning"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleSeverityChange(key, 'warning')}
|
||||
>
|
||||
Warn
|
||||
@@ -193,6 +198,7 @@ export const ResourceWarningsTab = () => {
|
||||
<SeverityButton
|
||||
active={severities[key] === 'critical'}
|
||||
variant="critical"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleSeverityChange(key, 'critical')}
|
||||
>
|
||||
Crit
|
||||
@@ -209,13 +215,17 @@ export const ResourceWarningsTab = () => {
|
||||
interface SeverityButtonProps {
|
||||
active: boolean
|
||||
variant: 'off' | 'warning' | 'critical'
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SeverityButton = ({ active, variant, onClick, children }: SeverityButtonProps) => (
|
||||
const SeverityButton = ({ active, variant, disabled, onClick, children }: SeverityButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 rounded-sm text-xs font-mono transition border',
|
||||
active
|
||||
@@ -224,7 +234,8 @@ const SeverityButton = ({ active, variant, onClick, children }: SeverityButtonPr
|
||||
: variant === 'warning'
|
||||
? 'bg-warning/20 text-warning border-warning'
|
||||
: 'bg-destructive/20 text-destructive border-destructive'
|
||||
: 'bg-transparent text-foreground-muted border-transparent hover:border-border'
|
||||
: 'bg-transparent text-foreground-muted border-transparent hover:border-border',
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const FeaturePreviewBadge = ({ featureKey, className }: FeaturePreviewBad
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => selectFeaturePreview(featureKey)}
|
||||
className="group flex items-center"
|
||||
title="Feature preview — click to manage"
|
||||
|
||||
@@ -121,7 +121,8 @@ export const FilterPopover = <T extends Record<string, any>>({
|
||||
<div className="flex-1">{label}</div>
|
||||
{showOnlyButton && (
|
||||
<button
|
||||
className="text-xs text-foreground-lighter hover:text-foreground-muted opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
tabIndex={0}
|
||||
className="text-xs text-foreground-lighter hover:text-foreground-muted opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setSelectedOptions([value])
|
||||
|
||||
@@ -41,7 +41,12 @@ export const PasswordStrengthBar = ({
|
||||
? passwordStrengthMessage
|
||||
: 'This is the password to your Postgres database, so it must be strong and hard to guess.') +
|
||||
' '}
|
||||
<button type="button" className={InlineLinkClassName} onClick={generateStrongPassword}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={InlineLinkClassName}
|
||||
onClick={generateStrongPassword}
|
||||
>
|
||||
Generate a password
|
||||
</button>
|
||||
.
|
||||
|
||||
@@ -59,7 +59,7 @@ export const SingleStat = ({
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button className="group" onClick={onClick}>
|
||||
<button type="button" className="group" tabIndex={0} onClick={onClick}>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -18,6 +18,5 @@
|
||||
"jsx-a11y/tabindex-no-positive",
|
||||
"jsx-a11y/anchor-is-valid",
|
||||
"jsx-a11y/heading-has-content",
|
||||
"jsx-a11y/no-distracting-elements",
|
||||
"supabase/require-explicit-tabindex"
|
||||
"jsx-a11y/no-distracting-elements"
|
||||
]
|
||||
+6
-1
@@ -224,7 +224,12 @@ function DialogView({ projectRef, isMobile }: { projectRef: string; isMobile?: b
|
||||
{index === stack.length - 1 ? (
|
||||
<span className="font-semibold text-foreground">{item.title}</span>
|
||||
) : (
|
||||
<button onClick={() => popTo(index)} className="hover:underline">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
onClick={() => popTo(index)}
|
||||
className="hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { expect, Page } from '@playwright/test'
|
||||
|
||||
import { env } from '../env.config.js'
|
||||
import { expectClipboardValue } from '../utils/clipboard.js'
|
||||
import { dropTable, query } from '../utils/db/index.js'
|
||||
@@ -14,7 +16,6 @@ import {
|
||||
waitForGridDataToLoad,
|
||||
waitForTableToLoad,
|
||||
} from '../utils/wait-for-response.js'
|
||||
import { expect, Page } from '@playwright/test'
|
||||
|
||||
const deleteTable = async (page: Page, ref: string, tableName: string) => {
|
||||
const viewLocator = page.getByLabel(`View ${tableName}`)
|
||||
@@ -1254,7 +1255,7 @@ testRunner('table editor', () => {
|
||||
await expect(page.getByRole('link', { name: 'public.pw_table_fk_target' })).toBeVisible()
|
||||
|
||||
// Remove the foreign key relation
|
||||
await page.getByRole('button', { name: 'Remove' }).click()
|
||||
await page.getByRole('button', { name: 'Remove', exact: true }).click()
|
||||
|
||||
// Save the table changes after removing foreign key
|
||||
const removeFkPromise = waitForApiResponseWithTimeout(
|
||||
|
||||
Reference in new issue
Block a user