diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
index f83504cecae..2915b67c981 100644
--- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
+++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.tsx
@@ -8,6 +8,7 @@ import {
ChatNavigationLink,
CollapsedChatFlyoutItem,
CollapsedSidebarMenu,
+ SidebarRowActions,
SidebarSection,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row'
@@ -56,30 +57,27 @@ function ChatRow({
href={chat.href}
chatId={chat.id}
isCurrentRoute={isCurrentRoute}
- className={chipVariants({ active: isCurrentRoute || isMenuOpen, fullWidth: true })}
+ className={cn(
+ chipVariants({ active: isCurrentRoute || isMenuOpen, fullWidth: true }),
+ 'group/sidebar-row'
+ )}
onContextMenu={(e) => onContextMenu(e, chat.id)}
>
-
- {showStatusDot && (
-
- )}
- {!showStatusDot && chat.isPinned && (
-
- )}
+
+ ) : chat.isPinned ? (
+
+ ) : undefined
+ }
+ >
-
+
)
}
diff --git a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx
index 8d1bfa0b942..faae4ebd437 100644
--- a/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx
+++ b/apps/sim/app/o/[organizationId]/components/organization-sidebar/components/workspaces-section/workspace-list.tsx
@@ -18,6 +18,7 @@ import { WorkspaceContextMenu } from '@/components/workspaces/workspace-context-
import { getWorkspaceInitial } from '@/lib/workspaces/initials'
import { useOrganizationWorkspaces } from '@/app/o/[organizationId]/components/organization-sidebar/hooks/use-organization-workspaces'
import { SidebarRenameRow } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-rename-row'
+import { SidebarRowActions } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions'
import { useFlyoutInlineRename } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-flyout-inline-rename'
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu'
import { useToggleWorkspacePin, useUpdateWorkspace } from '@/hooks/queries/workspace'
@@ -139,6 +140,7 @@ export function WorkspaceList({ organizationId, pathname, flyout }: WorkspaceLis
key={workspace.id}
asChild
active={isActive || isMenuOpen}
+ actionOpen={isMenuOpen}
onPointerMove={(event) => {
if (menu.isOpen || rename.editingId) event.preventDefault()
}}
@@ -176,34 +178,36 @@ export function WorkspaceList({ organizationId, pathname, flyout }: WorkspaceLis
openMenu(event, workspace.id)}
>
{label}
-
- {isPinned && (
-
- )}
+
+ ) : undefined
+ }
+ >
-
+
)
})}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
index f1790e469f5..726bfbb02a9 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
@@ -10,17 +10,13 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuItemAction,
+ DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Duplicate,
Layout,
MoreHorizontal,
- Popover,
- PopoverContent,
- PopoverItem,
- PopoverScrollArea,
- PopoverSection,
- PopoverTrigger,
Trash,
toast,
} from '@sim/emcn'
@@ -913,63 +909,55 @@ export const Panel = memo(function Panel() {
- {
setIsCopilotHistoryOpen(open)
if (open) loadCopilotChats()
}}
>
-
-
-
+
+
{copilotChatList.length === 0 ? (
-
- No chats yet
-
+ No chats yet
) : (
-
- Recent
-
- {copilotChatList.map((chat) => (
-
-
handleCopilotSelectChat(chat)}
+ <>
+ Recent
+ {copilotChatList.map((chat) => (
+ handleCopilotSelectChat(chat)}
+ action={
+ handleCopilotDeleteChat(chat.id)}
>
-
- {
- e.stopPropagation()
- handleCopilotDeleteChat(chat.id)
- }}
- aria-label='Delete chat'
- >
-
-
-
- }
- />
-
-
- ))}
-
-
+
+
+ }
+ >
+
+
+ ))}
+ >
)}
-
-
+
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
index 04c633b19fe..d3cbc7e1eb7 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
@@ -257,6 +257,7 @@ export function CollapsedSidebarMenu({
onMoreClick?.(e, chat.id)}
- className={cn(isMenuOpen && 'opacity-100')}
>
@@ -409,6 +410,7 @@ export function CollapsedWorkflowFlyoutItem({
setActionsOpen((prev) => !prev)}
- className={cn(actionsOpen && 'opacity-100')}
>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
index e735e674e89..583f7b5aeca 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts
@@ -1,3 +1,4 @@
+export { SidebarRowActions } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions'
export { ChatNavigationLink } from './chat-navigation-link/chat-navigation-link'
export {
CollapsedChatFlyoutItem,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions.tsx
new file mode 100644
index 00000000000..1d966c4102a
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions.tsx
@@ -0,0 +1,51 @@
+import type { ReactNode } from 'react'
+import { cn } from '@sim/emcn'
+
+interface SidebarRowActionsProps {
+ children: ReactNode
+ indicator?: ReactNode
+ open?: boolean
+ revealOnHover?: boolean
+}
+
+/** Reclaims idle action space while retaining indicators and touch/keyboard access. */
+export function SidebarRowActions({
+ children,
+ indicator,
+ open = false,
+ revealOnHover = true,
+}: SidebarRowActionsProps) {
+ return (
+
+ {indicator && (
+
+ {indicator}
+
+ )}
+
+ {children}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
index 914643aeba4..67a8bba50fe 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
@@ -17,6 +17,7 @@ import { useRouter } from 'next/navigation'
import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { generateSubfolderName } from '@/lib/workspaces/naming'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
+import { SidebarRowActions } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
import {
@@ -385,16 +386,13 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold
[handleToggleExpanded, shouldPreventClickRef, isEditing, onFolderClick, folder.id]
)
- const handleKeyDown = useCallback(
- (e: React.KeyboardEvent) => {
- if (isEditing) {
- handleRenameKeyDown(e)
- } else {
- handleExpandKeyDown(e)
- }
- },
- [isEditing, handleRenameKeyDown, handleExpandKeyDown]
- )
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (isEditing) {
+ handleRenameKeyDown(e)
+ } else if (e.target === e.currentTarget) {
+ handleExpandKeyDown(e)
+ }
+ }
const handleMorePointerDown = useCallback(() => {
if (isContextMenuOpen) {
@@ -503,6 +501,7 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold
aria-label={`${folder.name} folder, ${isExpanded ? 'expanded' : 'collapsed'}`}
className={cn(
chipVariants({ active: isSelected || isContextMenuOpen, fullWidth: true }),
+ 'group/sidebar-row',
(isDragging || (isAnyDragActive && isSelected)) && 'opacity-50'
)}
onClick={handleFolderSelect}
@@ -548,34 +547,30 @@ export const FolderItem = memo(function FolderItem({ workspaceId, folder }: Fold
>
-
- {folder.locked && (
-
-
-
- )}
+
+ ) : undefined
+ }
+ >
-
+
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
index 734aefef876..08c60fd1a81 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
@@ -6,6 +6,7 @@ import { Lock, MoreHorizontal } from '@sim/emcn/icons'
import Link from 'next/link'
import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
+import { SidebarRowActions } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
import { Avatars } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars'
@@ -410,6 +411,7 @@ export const WorkflowItem = memo(function WorkflowItem({
active: active || isContextMenuOpen || (isSelected && selectedWorkflows.size > 1),
fullWidth: true,
}),
+ 'group/sidebar-row',
(isDragging || (isAnyDragActive && isSelected)) && 'opacity-50'
)}
draggable={!isEditing && !dragDisabled && !effectiveLocked}
@@ -448,43 +450,30 @@ export const WorkflowItem = memo(function WorkflowItem({
{!isEditing && (
-
- {workflow.locked && (
-
-
-
- )}
+
+ ) : undefined
+ }
+ >
-
+
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
index 6d083c20c63..5053a95637e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx
@@ -694,6 +694,7 @@ function WorkspaceHeaderImpl({
) : (
event.preventDefault()}
onPointerMove={preserveSearchFocus}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index 44f0619a5c6..98b1c4a670c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -77,6 +77,7 @@ import {
SidebarFooter,
SidebarNavChip,
type SidebarNavItemData,
+ SidebarRowActions,
SidebarSection,
SidebarTooltip,
StatusNotice,
@@ -249,10 +250,13 @@ const SidebarChatItem = memo(function SidebarChatItem({
chatId={chat.id}
href={chat.href}
isCurrentRoute={isCurrentRoute}
- className={chipVariants({
- active: isCurrentRoute || isSelected || isMenuOpen,
- fullWidth: true,
- })}
+ className={cn(
+ chipVariants({
+ active: isCurrentRoute || isSelected || isMenuOpen,
+ fullWidth: true,
+ }),
+ 'group/sidebar-row'
+ )}
onClick={(e) => {
if (e.metaKey || e.ctrlKey) return
if (e.shiftKey) {
@@ -269,28 +273,20 @@ const SidebarChatItem = memo(function SidebarChatItem({
>
{chat.id !== 'new' && (
-
- {showStatusDot && (
-
- )}
- {!showStatusDot && isPinned && (
-
- )}
+
+ ) : isPinned ? (
+
+ ) : undefined
+ }
+ >
-
+
)}
diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
index 94145633802..d810dfb4472 100644
--- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
+++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.test.tsx
@@ -10,12 +10,13 @@
*/
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
-import { afterEach, describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuItemAction,
DropdownMenuItemLabel,
DropdownMenuTrigger,
} from './dropdown-menu'
@@ -129,3 +130,73 @@ describe('menu row labels', () => {
expect(label?.className).not.toContain('truncate')
})
})
+
+describe('menu row actions', () => {
+ it('moves focus to an action and back without selecting the row', () => {
+ const onSelect = vi.fn()
+ openMenu(
+ More}
+ >
+ A workflow
+
+ )
+ const item = row()
+ const action = row('button[aria-label="Options"]')
+ act(() => {
+ item.focus()
+ item.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
+ })
+ expect(document.activeElement).toBe(action)
+ act(() => {
+ action.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
+ })
+ expect(document.activeElement).toBe(item)
+ expect(onSelect).not.toHaveBeenCalled()
+ })
+
+ it('invokes an action without navigating its asChild row or closing the menu', () => {
+ const onSelect = vi.fn()
+ const onAction = vi.fn()
+ openMenu(
+
+ More
+
+ }
+ >
+ A workflow
+
+ )
+ act(() => row('button[aria-label="Options"]').click())
+ expect(onAction).toHaveBeenCalledOnce()
+ expect(onSelect).not.toHaveBeenCalled()
+ expect(row('a').getAttribute('href')).toBe('#workflow')
+ expect(document.querySelector('[role="menu"]')).not.toBeNull()
+ })
+
+ it('keeps focus on the row when its action is disabled', () => {
+ openMenu(
+
+ More
+
+ }
+ >
+ A workflow
+
+ )
+ const item = row()
+ act(() => {
+ item.focus()
+ item.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
+ })
+ expect(document.activeElement).toBe(item)
+ })
+})
diff --git a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
index 8417f9e6208..6fea1973ce2 100644
--- a/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
+++ b/packages/emcn/src/components/dropdown-menu/dropdown-menu.tsx
@@ -327,28 +327,75 @@ const DropdownMenuItem = React.forwardRef<
/**
* Optional inline action rendered on the right edge of the item — e.g. a
* "more" icon button. Reveals on hover/focus of the row, and the row stays
- * highlighted while the cursor is over the action.
+ * highlighted while the cursor is over the action. ArrowRight moves from
+ * the row to its action; ArrowLeft returns to the row.
*/
action?: React.ReactNode
- /** Idle indicator sharing the action slot so the label never shifts. */
+ /** Keeps the action visible while its portaled menu is open. */
+ actionOpen?: boolean
+ /** Idle indicator sharing the action slot on hover-capable devices. */
actionIndicator?: React.ReactNode
}
>(
(
- { className, size, inset, active, action, actionIndicator, asChild, children, ...props },
+ {
+ className,
+ size,
+ inset,
+ active,
+ action,
+ actionOpen,
+ actionIndicator,
+ asChild,
+ children,
+ ...props
+ },
ref
) => {
+ const actionRef = React.useRef(null)
const content = asChild ? children : withOverflowLabel(children)
const stateClasses = active ? MENU_ROW_SELECTED_CLASS : MENU_ROW_HIGHLIGHT_CLASS
if (action) {
return (
-
+
{
+ if (
+ event.defaultPrevented ||
+ event.altKey ||
+ event.ctrlKey ||
+ event.metaKey ||
+ event.shiftKey
+ )
+ return
+ const row = event.currentTarget.firstElementChild
+ const actionButton =
+ actionRef.current?.querySelector
('button:not(:disabled)')
+ if (event.key === 'ArrowRight' && event.target === row && actionButton) {
+ event.preventDefault()
+ event.stopPropagation()
+ actionButton.focus()
+ } else if (
+ event.key === 'ArrowLeft' &&
+ row instanceof HTMLElement &&
+ event.target instanceof Node &&
+ actionRef.current?.contains(event.target)
+ ) {
+ event.preventDefault()
+ event.stopPropagation()
+ row.focus()
+ }
+ }}
+ >
{content}
-