mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
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:
@@ -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>
|
||||
))}
|
||||
|
||||
+8
-4
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)')
|
||||
|
||||
Reference in New Issue
Block a user