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
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('ViewsMenu', () => {
document.body.appendChild(container)
const root = createRoot(container)
const onSetDefault = vi.fn()
const onDelete = vi.fn()

act(() => {
root.render(
Expand All @@ -79,15 +80,28 @@ describe('ViewsMenu', () => {
onSelect={vi.fn()}
onRename={vi.fn()}
onSetDefault={onSetDefault}
onDelete={vi.fn()}
onDelete={onDelete}
onNewView={vi.fn()}
canEdit
/>
)
})
act(() => container.querySelector<HTMLButtonElement>('button[aria-label="Views"]')?.click())

expect(document.body.querySelectorAll('button[aria-label="Delete"]')).toHaveLength(1)
// The default view's Delete stays hoverable (aria-disabled, no native
// title) so its tooltip can explain why it is inert.
const deleteButtons = [
...document.body.querySelectorAll<HTMLButtonElement>('button[aria-label="Delete"]'),
]
expect(deleteButtons).toHaveLength(2)
const defaultDelete = deleteButtons.find(
(button) => button.getAttribute('aria-disabled') === 'true'
)
expect(defaultDelete).not.toBeUndefined()
expect(defaultDelete?.title).toBe('')
act(() => defaultDelete?.click())
expect(onDelete).not.toHaveBeenCalled()

const defaultPin = document.body.querySelector<HTMLButtonElement>(
'button[aria-label="Current default view"]'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PopoverContent,
PopoverItem,
PopoverSection,
Tooltip,
} from '@sim/emcn'
import { Check, Pencil, Pin, Plus, Trash } from '@sim/emcn/icons'
import type { TableViewWire } from '@/lib/api/contracts/tables'
Expand Down Expand Up @@ -169,15 +170,14 @@ export const ViewsMenu = memo(function ViewsMenu({
label: 'Rename',
onClick: () => runAndClose(() => onRename(view.id)),
},
...(!view.isDefault
? [
{
icon: Trash,
label: 'Delete',
onClick: () => runAndClose(() => onDelete(view.id)),
},
]
: []),
{
icon: Trash,
label: 'Delete',
disabledReason: view.isDefault
? 'Default view cannot be deleted'
: undefined,
onClick: () => runAndClose(() => onDelete(view.id)),
},
]
: undefined
}
Expand Down Expand Up @@ -207,6 +207,8 @@ interface ViewRowAction {
icon: React.ElementType
label: string
onClick: () => void
/** Renders the action inert and dimmed, with this text in its hover tooltip. */
disabledReason?: string
}

interface ViewRowDefaultState {
Expand Down Expand Up @@ -252,24 +254,47 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
</PopoverItem>
{actionCount > 0 && (
<div className='pointer-events-none absolute right-1.5 flex items-center gap-0.5'>
{actions?.map((action) => (
<Button
key={action.label}
type='button'
variant='quiet'
size='icon'
aria-label={action.label}
title={action.label}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
action.onClick()
}}
className='pointer-events-none opacity-0 transition-[background-color,color,opacity] group-focus-within/view:pointer-events-auto group-focus-within/view:opacity-100 group-hover/view:pointer-events-auto group-hover/view:opacity-100'
>
<action.icon className='size-3' />
</Button>
))}
{actions?.map((action) => {
// Disabled via aria-disabled, not the `disabled` attribute: the button
// must keep receiving hover and focus events so the tooltip can explain
// why it is inert, and Button's disabled:opacity-70 would otherwise
// leak it through the hidden (opacity-0) resting state.
const button = (
<Button
key={action.label}
type='button'
variant='quiet'
size='icon'
aria-label={action.label}
title={action.disabledReason ? undefined : action.label}
aria-disabled={action.disabledReason ? true : undefined}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
if (action.disabledReason) return
action.onClick()
}}
className={cn(
'pointer-events-none opacity-0 transition-[background-color,color,opacity] group-focus-within/view:pointer-events-auto group-hover/view:pointer-events-auto',
action.disabledReason
? 'cursor-default hover-hover:bg-transparent group-focus-within/view:opacity-40 group-hover/view:opacity-40'
: 'group-focus-within/view:opacity-100 group-hover/view:opacity-100'
)}
>
<action.icon className='size-3' />
</Button>
)
return action.disabledReason ? (
<Tooltip.Root key={action.label}>
<Tooltip.Trigger asChild>{button}</Tooltip.Trigger>
<Tooltip.Content>
<p>{action.disabledReason}</p>
</Tooltip.Content>
</Tooltip.Root>
) : (
button
)
})}
{defaultState && (
<Button
type='button'
Expand Down
Loading