Skip to content

Commit 1d54b9d

Browse files
dnywhcharislam
andauthored
studio(chore): improve storage bucket presentation (supabase#40230)
* badge and admonition updates * whole file row tappable * file bucket improvements * match tables across all storage types * keyboard focussable rows * share function * other bucket types * fix: a11y of table rows * clean up focus state on rows * accessibility * address review comments * move accessibility --------- Co-authored-by: Charis Lam <[email protected]>
1 parent ff599ed commit 1d54b9d

File tree

21 files changed

+323
-306
lines changed

21 files changed

+323
-306
lines changed

.cursor/rules/studio-ui.mdc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ We use Tailwind for styling.
4242
- 'text-warning' for calling out information that needs action
4343
- 'text-destructive' for calling out when something went wrong
4444
- 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"
45+
- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles
4546

4647
## Page structure
4748

apps/design-system/config/docs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export const docsConfig: DocsConfig = {
3535
href: '/docs/icons',
3636
items: [],
3737
},
38+
{
39+
items: [],
40+
href: '/docs/ui-patterns/accessibility',
41+
title: 'Accessibility',
42+
},
3843
],
3944
},
4045
{
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
title: Accessibility
3+
description: Make Supabase work for everyone.
4+
---
5+
6+
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:
7+
8+
- Keyboard navigation
9+
- Legible and resizable elements
10+
- Large tap targets
11+
- Clear and simple language
12+
13+
## Checklist
14+
15+
About to push some code? At a minimum, check your work against this list:
16+
17+
- Are interactive page elements [keyboard-focusable](#focus-management)?
18+
- Are all elements announcable by a [screen reader](#screen-reader-support)?
19+
- Are textual elements legible and scalable?
20+
- Can I use this on a smaller and/or older device?
21+
22+
## Focus management
23+
24+
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.
25+
26+
```jsx
27+
<TableCell>
28+
<p>{name}</p>
29+
<button
30+
className={cn('absolute inset-0', 'inset-focus')}
31+
onClick={(event) => handleBucketNavigation(name, event)}
32+
>
33+
<span className="sr-only">Go to bucket details</span>
34+
</button>
35+
</TableCell>
36+
```
37+
38+
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.
39+
40+
## Screen reader support
41+
42+
Textual elements are supported out-of-the-box by screen readers. Imagery of course should be described by `alt` tags.
43+
44+
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:
45+
46+
```jsx
47+
<TableHead>
48+
<span className="sr-only">Actions</span>
49+
</TableHead>
50+
```

apps/studio/components/interfaces/Storage/AnalyticsBuckets/DeleteAnalyticsBucketModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const DeleteAnalyticsBucketModal = ({
6464
visible={visible}
6565
size="medium"
6666
variant="destructive"
67-
title={`Confirm deletion of ${bucketId}`}
67+
title={`Delete bucket “${bucketId}`}
6868
loading={isDeleting}
6969
confirmPlaceholder="Type bucket name"
7070
confirmString={bucketId ?? ''}
Lines changed: 46 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,23 @@
1-
import { ExternalLink, MoreVertical, Search, Trash2 } from 'lucide-react'
2-
import Link from 'next/link'
1+
import { ChevronRight, ExternalLink, Search } from 'lucide-react'
2+
import { useRouter } from 'next/navigation'
33
import { useState } from 'react'
44

55
import { useParams } from 'common'
66
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
77
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
8-
import { AnalyticsBucket, useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
9-
import {
10-
Button,
11-
Card,
12-
DropdownMenu,
13-
DropdownMenuContent,
14-
DropdownMenuItem,
15-
DropdownMenuTrigger,
16-
Table,
17-
TableBody,
18-
TableCell,
19-
TableHead,
20-
TableHeader,
21-
TableRow,
22-
} from 'ui'
8+
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
9+
import { Bucket as BucketIcon } from 'icons'
10+
import { Button, Card, cn, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
2311
import { Admonition, TimestampInfo } from 'ui-patterns'
2412
import { Input } from 'ui-patterns/DataInputs/Input'
2513
import { EmptyBucketState } from '../EmptyBucketState'
2614
import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal'
27-
import { DeleteAnalyticsBucketModal } from './DeleteAnalyticsBucketModal'
2815

2916
export const AnalyticsBuckets = () => {
3017
const { ref } = useParams()
18+
const router = useRouter()
3119

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

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

30+
const handleBucketNavigation = (
31+
bucketId: string,
32+
event: React.MouseEvent | React.KeyboardEvent
33+
) => {
34+
const url = `/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucketId)}`
35+
if (event.metaKey || event.ctrlKey) {
36+
window.open(url, '_blank')
37+
} else {
38+
router.push(url)
39+
}
40+
}
41+
4442
return (
4543
<ScaffoldSection isFullWidth>
4644
<Admonition
47-
type="warning"
45+
type="note"
4846
layout="horizontal"
49-
className="mb-12 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
50-
title="Analytics buckets are in alpha"
47+
className="-mt-4 mb-8 [&>div]:!translate-y-0 [&>svg]:!translate-y-1"
48+
title="Private alpha"
5149
actions={
5250
<Button asChild type="default" icon={<ExternalLink />}>
5351
<a
5452
target="_blank"
5553
rel="noopener noreferrer"
5654
href="https://github.com/orgs/supabase/discussions/40116"
5755
>
58-
Leave feedback
56+
Share feedback
5957
</a>
6058
</Button>
6159
}
6260
>
63-
<p className="!leading-normal !mb-0">
64-
Expect rapid changes, limited features, and possible breaking updates as we expand access.
61+
<p className="!leading-normal !mb-0 text-balance">
62+
Analytics buckets are now in private alpha. Expect rapid changes, limited features, and
63+
possible breaking updates. Please share feedback as we refine the experience and expand
64+
access.
6565
</p>
66-
<p className="!leading-normal !mb-0">Please share feedback as we refine the experience!</p>
6766
</Admonition>
6867

6968
{!isLoadingBuckets &&
@@ -94,9 +93,16 @@ export const AnalyticsBuckets = () => {
9493
<Table>
9594
<TableHeader>
9695
<TableRow>
96+
{analyticsBuckets.length > 0 && (
97+
<TableHead className="w-2 pr-1">
98+
<span className="sr-only">Icon</span>
99+
</TableHead>
100+
)}
97101
<TableHead>Name</TableHead>
98102
<TableHead>Created at</TableHead>
99-
<TableHead />
103+
<TableHead>
104+
<span className="sr-only">Actions</span>
105+
</TableHead>
100106
</TableRow>
101107
</TableHeader>
102108
<TableBody>
@@ -111,15 +117,18 @@ export const AnalyticsBuckets = () => {
111117
</TableRow>
112118
)}
113119
{analyticsBuckets.map((bucket) => (
114-
<TableRow key={bucket.id}>
120+
<TableRow key={bucket.id} className="relative cursor-pointer h-16">
121+
<TableCell className="w-2 pr-1">
122+
<BucketIcon size={16} className="text-foreground-muted" />
123+
</TableCell>
115124
<TableCell>
116-
<Link
117-
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
118-
title={bucket.id}
119-
className="text-link-table-cell"
125+
<p className="whitespace-nowrap max-w-[512px] truncate">{bucket.id}</p>
126+
<button
127+
className={cn('absolute inset-0', 'inset-focus')}
128+
onClick={(event) => handleBucketNavigation(bucket.id, event)}
120129
>
121-
{bucket.id}
122-
</Link>
130+
<span className="sr-only">Go to table details</span>
131+
</button>
123132
</TableCell>
124133

125134
<TableCell>
@@ -132,31 +141,8 @@ export const AnalyticsBuckets = () => {
132141
</TableCell>
133142

134143
<TableCell>
135-
<div className="flex justify-end gap-2">
136-
<Button asChild type="default">
137-
<Link
138-
href={`/project/${ref}/storage/analytics/buckets/${encodeURIComponent(bucket.id)}`}
139-
>
140-
View contents
141-
</Link>
142-
</Button>
143-
<DropdownMenu>
144-
<DropdownMenuTrigger asChild>
145-
<Button type="default" className="px-1" icon={<MoreVertical />} />
146-
</DropdownMenuTrigger>
147-
<DropdownMenuContent side="bottom" align="end" className="w-40">
148-
<DropdownMenuItem
149-
className="flex items-center space-x-2"
150-
onClick={(e) => {
151-
setModal('delete')
152-
setSelectedBucket(bucket)
153-
}}
154-
>
155-
<Trash2 size={12} />
156-
<p>Delete bucket</p>
157-
</DropdownMenuItem>
158-
</DropdownMenuContent>
159-
</DropdownMenu>
144+
<div className="flex justify-end items-center h-full">
145+
<ChevronRight size={14} className="text-foreground-muted/60" />
160146
</div>
161147
</TableCell>
162148
</TableRow>
@@ -167,14 +153,6 @@ export const AnalyticsBuckets = () => {
167153
)}
168154
</div>
169155
)}
170-
171-
{selectedBucket && (
172-
<DeleteAnalyticsBucketModal
173-
visible={modal === 'delete'}
174-
bucketId={selectedBucket.id}
175-
onClose={() => setModal(null)}
176-
/>
177-
)}
178156
</ScaffoldSection>
179157
)
180158
}

apps/studio/components/interfaces/Storage/CreateBucketModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export const CreateBucketModal = ({
229229

230230
<DialogContent aria-describedby={undefined}>
231231
<DialogHeader>
232-
<DialogTitle>Create a storage bucket</DialogTitle>
232+
<DialogTitle>Create file bucket</DialogTitle>
233233
</DialogHeader>
234234

235235
<DialogSectionSeparator />

apps/studio/components/interfaces/Storage/DeleteBucketModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const DeleteBucketModal = ({ visible, bucket, onClose }: DeleteBucketModa
8383
visible={visible}
8484
size="medium"
8585
variant="destructive"
86-
title={`Confirm deletion of ${bucket.id}`}
86+
title={`Delete bucket “${bucket.id}`}
8787
loading={isDeletingBucket || isDeletingPolicies}
8888
confirmPlaceholder="Type bucket name"
8989
confirmString={bucket.id}

apps/studio/components/interfaces/Storage/EditBucketModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro
197197
>
198198
<DialogContent>
199199
<DialogHeader>
200-
<DialogTitle>{`Edit bucket "${bucket?.name}"`}</DialogTitle>
200+
<DialogTitle>{`Edit bucket ${bucket?.name}`}</DialogTitle>
201201
</DialogHeader>
202202

203203
<DialogSectionSeparator />

apps/studio/components/interfaces/Storage/EmptyBucketModal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
5555
>
5656
<DialogContent>
5757
<DialogHeader>
58-
<DialogTitle>{`Confirm to delete all contents from ${bucket?.name}`}</DialogTitle>
58+
<DialogTitle>{`Empty bucket “${bucket?.name}`}</DialogTitle>
5959
</DialogHeader>
6060
<DialogSectionSeparator />
6161
<Admonition
@@ -65,7 +65,9 @@ export const EmptyBucketModal = ({ visible, bucket, onClose }: EmptyBucketModalP
6565
description="The contents of your bucket cannot be recovered once deleted."
6666
/>
6767
<DialogSection>
68-
<p className="text-sm">Are you sure you want to empty the bucket "{bucket?.name}"?</p>
68+
<p className="text-sm">
69+
Are you sure you want to remove all contents from the bucket “{bucket?.name}”?
70+
</p>
6971
</DialogSection>
7072
<DialogFooter>
7173
<Button type="default" disabled={isLoading} onClick={onClose}>

0 commit comments

Comments
 (0)