@@ -13,6 +13,7 @@ import {
1313 PopoverContent ,
1414 PopoverItem ,
1515 PopoverSection ,
16+ Tooltip ,
1617} from '@sim/emcn'
1718import { Check , Pencil , Pin , Plus , Trash } from '@sim/emcn/icons'
1819import type { TableViewWire } from '@/lib/api/contracts/tables'
@@ -169,15 +170,14 @@ export const ViewsMenu = memo(function ViewsMenu({
169170 label : 'Rename' ,
170171 onClick : ( ) => runAndClose ( ( ) => onRename ( view . id ) ) ,
171172 } ,
172- ...( ! view . isDefault
173- ? [
174- {
175- icon : Trash ,
176- label : 'Delete' ,
177- onClick : ( ) => runAndClose ( ( ) => onDelete ( view . id ) ) ,
178- } ,
179- ]
180- : [ ] ) ,
173+ {
174+ icon : Trash ,
175+ label : 'Delete' ,
176+ disabledReason : view . isDefault
177+ ? 'Default view cannot be deleted'
178+ : undefined ,
179+ onClick : ( ) => runAndClose ( ( ) => onDelete ( view . id ) ) ,
180+ } ,
181181 ]
182182 : undefined
183183 }
@@ -207,6 +207,8 @@ interface ViewRowAction {
207207 icon : React . ElementType
208208 label : string
209209 onClick : ( ) => void
210+ /** Renders the action inert and dimmed, with this text in its hover tooltip. */
211+ disabledReason ?: string
210212}
211213
212214interface ViewRowDefaultState {
@@ -252,24 +254,47 @@ function ViewRow({ label, isActive, onSelect, defaultState, actions }: ViewRowPr
252254 </ PopoverItem >
253255 { actionCount > 0 && (
254256 < div className = 'pointer-events-none absolute right-1.5 flex items-center gap-0.5' >
255- { actions ?. map ( ( action ) => (
256- < Button
257- key = { action . label }
258- type = 'button'
259- variant = 'quiet'
260- size = 'icon'
261- aria-label = { action . label }
262- title = { action . label }
263- onClick = { ( event ) => {
264- event . preventDefault ( )
265- event . stopPropagation ( )
266- action . onClick ( )
267- } }
268- 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'
269- >
270- < action . icon className = 'size-3' />
271- </ Button >
272- ) ) }
257+ { actions ?. map ( ( action ) => {
258+ // Disabled via aria-disabled, not the `disabled` attribute: the button
259+ // must keep receiving hover and focus events so the tooltip can explain
260+ // why it is inert, and Button's disabled:opacity-70 would otherwise
261+ // leak it through the hidden (opacity-0) resting state.
262+ const button = (
263+ < Button
264+ key = { action . label }
265+ type = 'button'
266+ variant = 'quiet'
267+ size = 'icon'
268+ aria-label = { action . label }
269+ title = { action . disabledReason ? undefined : action . label }
270+ aria-disabled = { action . disabledReason ? true : undefined }
271+ onClick = { ( event ) => {
272+ event . preventDefault ( )
273+ event . stopPropagation ( )
274+ if ( action . disabledReason ) return
275+ action . onClick ( )
276+ } }
277+ className = { cn (
278+ 'pointer-events-none opacity-0 transition-[background-color,color,opacity] group-focus-within/view:pointer-events-auto group-hover/view:pointer-events-auto' ,
279+ action . disabledReason
280+ ? 'cursor-default hover-hover:bg-transparent group-focus-within/view:opacity-40 group-hover/view:opacity-40'
281+ : 'group-focus-within/view:opacity-100 group-hover/view:opacity-100'
282+ ) }
283+ >
284+ < action . icon className = 'size-3' />
285+ </ Button >
286+ )
287+ return action . disabledReason ? (
288+ < Tooltip . Root key = { action . label } >
289+ < Tooltip . Trigger asChild > { button } </ Tooltip . Trigger >
290+ < Tooltip . Content >
291+ < p > { action . disabledReason } </ p >
292+ </ Tooltip . Content >
293+ </ Tooltip . Root >
294+ ) : (
295+ button
296+ )
297+ } ) }
273298 { defaultState && (
274299 < Button
275300 type = 'button'
0 commit comments