Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursor/rules/studio-ui.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ We use Tailwind for styling.
- 'text-warning' for calling out information that needs action
- 'text-destructive' for calling out when something went wrong
- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium"
- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles

## Page structure

Expand Down
5 changes: 5 additions & 0 deletions apps/design-system/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/icons',
items: [],
},
{
items: [],
href: '/docs/ui-patterns/accessibility',
title: 'Accessibility',
},
],
},
{
Expand Down
50 changes: 50 additions & 0 deletions apps/design-system/content/docs/ui-patterns/accessibility.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Accessibility
description: Make Supabase work for everyone.
---

Accessibility is about making an interface work for as many people as possible across as many circumstances as possible. All of us lean on affordances that accessible experiences provide:

- Keyboard navigation
- Legible and resizable elements
- Large tap targets
- Clear and simple language

## Checklist

About to push some code? At a minimum, check your work against this list:

- Are interactive page elements [keyboard-focusable](#focus-management)?
- Are all elements announcable by a [screen reader](#screen-reader-support)?
- Are textual elements legible and scalable?
- Can I use this on a smaller and/or older device?

## Focus management

All interactive page elements should be reachable by keyboard. They should also provide visual feedback upon selection via a `focus-visible` state. We use consistent focus styles such as `inset-focus` so users recognize this state instantly.

```jsx
<TableCell>
<p>{name}</p>
<button
className={cn('absolute inset-0', 'inset-focus')}
onClick={(event) => handleBucketNavigation(name, event)}
>
<span className="sr-only">Go to bucket details</span>
</button>
</TableCell>
```

Consider also affordances like `ctrl` and `meta` key support for opening in a new tab. Anything that you can do with a mouse input should be replicable by keyboard.

## Screen reader support

Textual elements are supported out-of-the-box by screen readers. Imagery of course should be described by `alt` tags.

Less obvious however are scaffolding elements that only makes sense visually, when paired with other content. For example: a table column for actions may not have a visual _Actions_ label because its purpose is obvious to a sighted person. For everyone else’s sake, this column should be titled with `sr-only` text:

```jsx
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
```
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const DeleteAnalyticsBucketModal = ({
visible={visible}
size="medium"
variant="destructive"
title={`Confirm deletion of ${bucketId}`}
title={`Delete bucket “${bucketId}`}
loading={isDeleting}
confirmPlaceholder="Type bucket name"
confirmString={bucketId ?? ''}
Expand Down
114 changes: 46 additions & 68 deletions apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
import { ExternalLink, MoreVertical, Search, Trash2 } from 'lucide-react'
import Link from 'next/link'
import { ChevronRight, ExternalLink, Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

import { useParams } from 'common'
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { AnalyticsBucket, useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
import { Bucket as BucketIcon } from 'icons'
import { Button, Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { Admonition, TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import { EmptyBucketState } from '../EmptyBucketState'
import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal'
import { DeleteAnalyticsBucketModal } from './DeleteAnalyticsBucketModal'

export const AnalyticsBuckets = () => {
const { ref } = useParams()
const router = useRouter()

const [filterString, setFilterString] = useState('')
const [selectedBucket, setSelectedBucket] = useState<AnalyticsBucket>()
const [modal, setModal] = useState<'edit' | 'empty' | 'delete' | null>(null)

const { data: buckets = [], isLoading: isLoadingBuckets } = useAnalyticsBucketsQuery({
projectRef: ref,
Expand All @@ -41,29 +27,42 @@ export const AnalyticsBuckets = () => {
filterString.length === 0 ? true : bucket.id.toLowerCase().includes(filterString.toLowerCase())
)

const handleBucketNavigation = (
bucketId: string,
event: React.MouseEvent | React.KeyboardEvent
) => {
const url = `/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucketId)}`
if (event.metaKey || event.ctrlKey) {
window.open(url, '_blank')
} else {
router.push(url)
}
}

return (
<ScaffoldSection isFullWidth>
<Admonition
type="warning"
type="note"
layout="horizontal"
className="mb-12 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
title="Analytics buckets are in alpha"
className="-mt-4 mb-8 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
title="Private alpha"
actions={
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/orgs/supabase/discussions/40116"
>
Leave feedback
Share feedback
</a>
</Button>
}
>
<p className="!leading-normal !mb-0">
Expect rapid changes, limited features, and possible breaking updates as we expand access.
<p className="!leading-normal !mb-0 text-balance">
Analytics buckets are now in private alpha. Expect rapid changes, limited features, and
possible breaking updates. Please share feedback as we refine the experience and expand
access.
</p>
<p className="!leading-normal !mb-0">Please share feedback as we refine the experience!</p>
</Admonition>

{!isLoadingBuckets &&
Expand Down Expand Up @@ -94,9 +93,16 @@ export const AnalyticsBuckets = () => {
<Table>
<TableHeader>
<TableRow>
{analyticsBuckets.length > 0 && (
<TableHead className="w-2 pr-1">
<span className="sr-only">Icon</span>
</TableHead>
)}
<TableHead>Name</TableHead>
<TableHead>Created at</TableHead>
<TableHead />
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -111,15 +117,18 @@ export const AnalyticsBuckets = () => {
</TableRow>
)}
{analyticsBuckets.map((bucket) => (
<TableRow key={bucket.id}>
<TableRow key={bucket.id} className="relative cursor-pointer h-16">
<TableCell className="w-2 pr-1">
<BucketIcon size={16} className="text-foreground-muted" />
</TableCell>
<TableCell>
<Link
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
title={bucket.id}
className="text-link-table-cell"
<p className="whitespace-nowrap max-w-[512px] truncate">{bucket.id}</p>
<button
className={cn('absolute inset-0', 'inset-focus')}
onClick={(event) => handleBucketNavigation(bucket.id, event)}
>
{bucket.id}
</Link>
<span className="sr-only">Go to table details</span>
</button>
</TableCell>

<TableCell>
Expand All @@ -132,31 +141,8 @@ export const AnalyticsBuckets = () => {
</TableCell>

<TableCell>
<div className="flex justify-end gap-2">
<Button asChild type="default">
<Link
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
>
View contents
</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
className="flex items-center space-x-2"
onClick={(e) => {
setModal('delete')
setSelectedBucket(bucket)
}}
>
<Trash2 size={12} />
<p>Delete bucket</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex justify-end items-center h-full">
<ChevronRight size={14} className="text-foreground-muted/60" />
</div>
</TableCell>
</TableRow>
Expand All @@ -167,14 +153,6 @@ export const AnalyticsBuckets = () => {
)}
</div>
)}

{selectedBucket && (
<DeleteAnalyticsBucketModal
visible={modal === 'delete'}
bucketId={selectedBucket.id}
onClose={() => setModal(null)}
/>
)}
</ScaffoldSection>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const CreateBucketModal = ({

<DialogContent aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>Create a storage bucket</DialogTitle>
<DialogTitle>Create file bucket</DialogTitle>
</DialogHeader>

<DialogSectionSeparator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa
visible={visible}
size="medium"
variant="destructive"
title={`Confirm deletion of ${bucket.id}`}
title={`Delete bucket “${bucket.id}`}
loading={isDeletingBucket || isDeletingPolicies}
confirmPlaceholder="Type bucket name"
confirmString={bucket.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro
>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Edit bucket "${bucket?.name}"`}</DialogTitle>
<DialogTitle>{`Edit bucket ${bucket?.name}`}</DialogTitle>
</DialogHeader>

<DialogSectionSeparator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
>
<DialogContent>
<DialogHeader>
<DialogTitle>{`Confirm to delete all contents from ${bucket?.name}`}</DialogTitle>
<DialogTitle>{`Empty bucket “${bucket?.name}`}</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<Admonition
Expand All @@ -65,7 +65,9 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
description="The contents of your bucket cannot be recovered once deleted."
/>
<DialogSection>
<p className="text-sm">Are you sure you want to empty the bucket "{bucket?.name}"?</p>
<p className="text-sm">
Are you sure you want to remove all contents from the bucket “{bucket?.name}”?
</p>
</DialogSection>
<DialogFooter>
<Button type="default" disabled={isLoading} onClick={onClose}>
Expand Down
Loading
Loading