feat(design-system): hit area on table actions (#43680)

## What kind of change does this PR introduce?

Feature

## What is the current behavior?

- Platform webhooks have a ‘tap target on tap target’ pattern with
`DropdownMenu` being on top of a clickable `TableRow`
- Misclicks are common (at least for me)
- @jordienr recently added the `hit-area` Tailwind plugin that helps
with misclicks
	- https://github.com/supabase/supabase/pull/43636

## What is the new behavior?

- The aforementioned `DropdownMenu` in platform webhooks now uses
`hit-area` to avoid misclicks
- Design system documentation
- Extracted `hit-area` Tailwind plugin to shared packages so it can be
used in design-system as well as studio

## Preview


https://github.com/user-attachments/assets/89f9110a-6c99-4eed-a386-a6f646b1f4f6
This commit is contained in:
Danny White
2026-03-13 11:18:54 +11:00
committed by GitHub
parent b041ebfaa5
commit fd9d177e61
8 changed files with 127 additions and 38 deletions
@@ -177,6 +177,8 @@ When adding icon columns to your table, use [Accessibility](../accessibility) ma
Action should be placed in the last column of each Table Row to maintain a logical reading flow. Users scan table data from left to right, so positioning actions on the right ensures they encounter the primary information first before reaching interactive controls.
For compact controls in action cells, add the `hit-area-2` utility to increase the tap target by `8px` on each side without changing visual layout. When actions sit next to each other, keep at least `gap-x-2` spacing: with two adjacent `hit-area-2` buttons, hit areas meet at the midpoint but do not overlap.
#### Multiple actions
When multiple actions are available for a row, display one primary action as a button and place additional options in an overflow menu. This keeps the table clean and scannable while providing access to all necessary actions without overwhelming the user.
@@ -1,5 +1,18 @@
import { EllipsisVertical, User } from 'lucide-react'
import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { EllipsisVertical, Pencil, Trash2, User } from 'lucide-react'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
const users = [
{
@@ -41,16 +54,29 @@ export default function TableActions() {
<TableCell>{user.name}</TableCell>
<TableCell className="text-foreground-lighter">{user.email}</TableCell>
<TableCell className="flex items-center gap-x-2">
<Button type="default" size="tiny">
<Button type="default" size="tiny" className="hit-area-2">
Inspect
</Button>
<Button
icon={<EllipsisVertical />}
aria-label={`More actions`}
type="default"
size="tiny"
className="w-7"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem className="gap-x-2">
<Pencil size={14} />
<span>Edit user</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2">
<Trash2 size={14} />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
@@ -1,6 +1,19 @@
import { DatabaseZap, Edit2, EllipsisVertical } from 'lucide-react'
import { Copy, DatabaseZap, Edit2, EllipsisVertical, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
const triggers = [
{
@@ -71,16 +84,29 @@ export default function TableCrossLink() {
</Link>
</TableCell>
<TableCell className="flex items-center gap-x-2">
<Button type="default" size="tiny" icon={<Edit2 />}>
<Button type="default" size="tiny" icon={<Edit2 />} className="hit-area-2">
Edit
</Button>
<Button
icon={<EllipsisVertical />}
aria-label={`More actions`}
type="default"
size="tiny"
className="w-7"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem className="gap-x-2">
<Copy size={14} />
<span>Duplicate trigger</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2">
<Trash2 size={14} />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
@@ -1,5 +1,18 @@
import { ChevronRight, EllipsisVertical, Shield } from 'lucide-react'
import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { ChevronRight, EllipsisVertical, Pencil, Shield, Trash2 } from 'lucide-react'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
const policies = [
{
@@ -82,19 +95,36 @@ export default function TableRowLinkActions() {
</TableCell>
<TableCell className="text-foreground-lighter">{policy.appliedTo}</TableCell>
<TableCell className="flex justify-end items-center h-full gap-3">
<Button
icon={<EllipsisVertical />}
aria-label={`More actions`}
type="default"
size="tiny"
className="w-7"
/>
<div>
<div
className="flex justify-end items-center h-full gap-3"
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem className="gap-x-2">
<Pencil size={14} />
<span>Edit policy</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2">
<Trash2 size={14} />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ChevronRight aria-hidden={true} size={14} className="text-foreground-muted/60" />
<button tabIndex={-1} className="sr-only">
Go to policy
</button>
</div>
<button tabIndex={-1} className="sr-only">
Go to policy
</button>
</TableCell>
</TableRow>
))}
@@ -1,8 +1,7 @@
import { createNavigationHandler } from 'lib/navigation'
import { ChevronRight, Eye, MoreVertical, Plus, Search, Trash2 } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { createNavigationHandler } from 'lib/navigation'
import {
Badge,
Button,
@@ -16,12 +15,13 @@ import {
TableBody,
TableCell,
TableHead,
TableHeadSort,
TableHeader,
TableHeadSort,
TableRow,
} from 'ui'
import { EmptyStatePresentational, TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import type { WebhookEndpoint } from './PlatformWebhooks.types'
interface PlatformWebhooksEndpointListProps {
@@ -189,7 +189,11 @@ export const PlatformWebhooksEndpointList = ({
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" icon={<MoreVertical />} className="w-7" />
<Button
type="default"
icon={<MoreVertical />}
className="w-7 hit-area-2"
/>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = config({
'./../../packages/ui/src/**/*.{tsx,ts,js}',
'./../../packages/ui-patterns/src/**/*.{tsx,ts,js}',
],
plugins: [require('@tailwindcss/container-queries'), require('./tailwind-plugins/hit-area')],
plugins: [require('@tailwindcss/container-queries')],
theme: {
extend: {
fontSize: {
+1
View File
@@ -433,6 +433,7 @@ const uiConfig = ui({
plugins: [
require('@tailwindcss/typography'),
require('tailwindcss-animate'),
require('./tailwind-plugins/hit-area'),
plugin(motionSafeTransition),
function ({ addVariant }) {
addVariant('not-disabled', '&:not(:disabled)')