Skip to content

fix(frontend): show submenu flyout and tooltip links when sidebar is collapsed (EVO-1048)#54

Open
marcelogorutuba wants to merge 3 commits into
developfrom
fix/EVO-1048
Open

fix(frontend): show submenu flyout and tooltip links when sidebar is collapsed (EVO-1048)#54
marcelogorutuba wants to merge 3 commits into
developfrom
fix/EVO-1048

Conversation

@marcelogorutuba
Copy link
Copy Markdown
Member

@marcelogorutuba marcelogorutuba commented May 11, 2026

Summary

  • Remove interactive links from collapsed Tooltip — shows item name only (hover informational, click opens flyout)
  • Fix backdrop to left-16 in MainLayout so icon sidebar stays directly clickable without 2-click toggle
  • Keep collapsed flyout container always mounted for CSS opacity + translate-x transitions to work correctly
  • Add aria-label to close button (lucide X icon is aria-hidden; screen readers had no accessible name)
  • Fix focus management: capture trigger ref only on first open, not on submenu switch — prevents focus returning to stale/unmounted element
  • Add keyboard trap: Tab cycles within flyout; Shift+Tab wraps backward; cannot escape to main content
  • Add Escape key dismissal (WAI-ARIA requirement)
  • Move backdrop from Sidebar into MainLayout to decouple overlay from sidebar component
  • Extract submenuHeader / submenuItems helpers — eliminates JSX duplication between collapsed and expanded branches
  • Replace local cn helpers with shared @/utils/cn
  • Add comment on relative container in MainLayout explaining flyout anchor role

Validation

  • pnpm exec tsc -b --noEmit ✅ (no errors in layout files)
  • pnpm exec eslint src/components/layout/MainLayout.tsx src/components/layout/components/MenuItem.tsx src/components/layout/components/Sidebar.tsx
  • pnpm test src/components/layout/components/Sidebar.spec.tsx → 11/11 ✅

Changed Files

  • src/components/layout/MainLayout.tsx
  • src/components/layout/components/Sidebar.tsx
  • src/components/layout/components/MenuItem.tsx
  • src/components/layout/components/Sidebar.spec.tsx

Linked Issue

  • EVO-1048

…collapsed (EVO-1048)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 11, 2026

Reviewer's Guide

This PR updates the sidebar layout so that submenu panels and tooltip content work correctly when the sidebar is collapsed, including a flyout-style secondary panel with backdrop and richer, clickable submenu tooltips.

Sequence diagram for collapsed sidebar submenu flyout behavior

sequenceDiagram
  actor User
  participant Sidebar
  participant MainLayout

  User->>Sidebar: clickMenuItem
  Sidebar->>Sidebar: setActiveSubmenu
  Sidebar->>MainLayout: render with isCollapsed true
  MainLayout->>Sidebar: render submenu panel
  Sidebar->>Sidebar: [activeSubmenu && isCollapsed]
  Sidebar->>Sidebar: renderBackdrop
  Sidebar->>Sidebar: renderSubmenuPanel absolute left_16 top_0

  User->>Sidebar: clickBackdrop
  Sidebar->>Sidebar: setActiveSubmenu null
  Sidebar->>MainLayout: rerender without submenu panel
Loading

Sequence diagram for tooltip submenu links in MenuItem

sequenceDiagram
  actor User
  participant MenuItem
  participant TooltipContent
  participant LinkComponent

  User->>MenuItem: hover
  MenuItem->>TooltipContent: render side right
  MenuItem->>TooltipContent: [hasSubItems]
  TooltipContent->>TooltipContent: renderSubItemHeader item.name
  TooltipContent->>TooltipContent: map item.subItems
  TooltipContent->>LinkComponent: render to subItem.href

  User->>LinkComponent: click
  LinkComponent-->>User: navigate to subItem.href
Loading

File-Level Changes

Change Details Files
Make submenu panel render as a flyout when the sidebar is collapsed, including click-outside dismissal and layout bounding.
  • Remove the isCollapsed guard so the secondary submenu always renders when active
  • Wrap the submenu in conditional styles that switch to an absolutely positioned flyout when the sidebar is collapsed
  • Add an absolute, full-viewport backdrop div in collapsed mode that clears the active submenu on click
  • Ensure the main layout container is relatively positioned so the flyout is positioned under the header and within the layout
src/components/layout/components/Sidebar.tsx
src/components/layout/MainLayout.tsx
Enhance collapsed sidebar tooltip content to show submenu items as clickable, styled links with icons and improved contrast.
  • Adjust TooltipContent padding/overflow based on whether the item has sub-items
  • Render a styled header for menu items with sub-items in the tooltip, including stronger typography and primary-foreground colors
  • Render submenu entries as Link components with icons, spacing, hover background, and primary-foreground text colors
  • Fallback to the original simple text tooltip when there are no sub-items
src/components/layout/components/MenuItem.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The submenu tooltip now assumes every subItem has an icon (<subItem.icon />); if any menu configs omit an icon this will throw at runtime, so consider rendering the icon conditionally or providing a fallback.
  • The collapsed-mode backdrop is absolute inset-0 z-40, so it will cover the entire positioned ancestor; double-check that the ancestor is intentionally relative and, if not, consider constraining the backdrop to just the sidebar/flyout area to avoid intercepting unintended clicks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The submenu tooltip now assumes every subItem has an icon (`<subItem.icon />`); if any menu configs omit an icon this will throw at runtime, so consider rendering the icon conditionally or providing a fallback.
- The collapsed-mode backdrop is `absolute inset-0 z-40`, so it will cover the entire positioned ancestor; double-check that the ancestor is intentionally `relative` and, if not, consider constraining the backdrop to just the sidebar/flyout area to avoid intercepting unintended clicks.

## Individual Comments

### Comment 1
<location path="src/components/layout/components/MenuItem.tsx" line_range="83" />
<code_context>
+                    to={subItem.href}
+                    className="flex items-center gap-2.5 px-3 py-2 text-sm text-primary-foreground hover:bg-primary-foreground/15 transition-colors"
+                  >
+                    <subItem.icon className="h-4 w-4 flex-shrink-0 text-primary-foreground/70" />
+                    <span>{subItem.name}</span>
+                  </Link>
</code_context>
<issue_to_address>
**suggestion:** Consider explicitly marking the icon as decorative for accessibility if it doesn't convey unique information.

If the icon is decorative and the text already conveys the meaning, ensure the icon component either sets `aria-hidden="true"` or supports a prop to do so to prevent redundant screen reader announcements.

```suggestion
                    <subItem.icon
                      className="h-4 w-4 flex-shrink-0 text-primary-foreground/70"
                      aria-hidden="true"
                    />
```
</issue_to_address>

### Comment 2
<location path="src/components/layout/components/Sidebar.tsx" line_range="123-129" />
<code_context>
       {/* Second Sidebar for Submenus */}
-      {activeSubmenu && !isCollapsed && (
-        <div className="hidden md:flex w-64 bg-sidebar text-sidebar-foreground flex-col border-r border-sidebar-border">
+      {activeSubmenu && isCollapsed && (
+        <div
+          className="hidden md:block absolute inset-0 z-40"
+          onClick={() => setActiveSubmenu(null)}
+        />
+      )}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider keyboard accessibility for the overlay used to dismiss the submenu.

Currently the overlay is a non-interactive `div` with only an `onClick` handler, so keyboard users can’t dismiss the submenu. Either make it an interactive element (e.g. add `role="button"` and a key handler for Enter/Escape) or hook into an existing focus/blur mechanism to close the submenu without relying solely on a clickable div.

```suggestion
      {/* Second Sidebar for Submenus */}
      {activeSubmenu && isCollapsed && (
        <div
          role="button"
          tabIndex={0}
          aria-label="Close submenu"
          className="hidden md:block absolute inset-0 z-40"
          onClick={() => setActiveSubmenu(null)}
          onKeyDown={(event) => {
            if (event.key === 'Enter' || event.key === ' ' || event.key === 'Escape') {
              event.preventDefault();
              setActiveSubmenu(null);
            }
          }}
        />
      )}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

to={subItem.href}
className="flex items-center gap-2.5 px-3 py-2 text-sm text-primary-foreground hover:bg-primary-foreground/15 transition-colors"
>
<subItem.icon className="h-4 w-4 flex-shrink-0 text-primary-foreground/70" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider explicitly marking the icon as decorative for accessibility if it doesn't convey unique information.

If the icon is decorative and the text already conveys the meaning, ensure the icon component either sets aria-hidden="true" or supports a prop to do so to prevent redundant screen reader announcements.

Suggested change
<subItem.icon className="h-4 w-4 flex-shrink-0 text-primary-foreground/70" />
<subItem.icon
className="h-4 w-4 flex-shrink-0 text-primary-foreground/70"
aria-hidden="true"
/>

Comment thread src/components/layout/components/Sidebar.tsx Outdated
@dpaes
Copy link
Copy Markdown

dpaes commented May 11, 2026

🔥 Code Review — Follow-ups (AI Review)

Findings from adversarial review against EVO-1048 acceptance criteria. AC coverage: PARTIAL (hover UI diverges from click UI), no regressions in expanded mode, all submenu entries (Contacts, Agents, Settings) covered by the same code path.

🔴 High

  • [AI-Review][High] Tooltip used for interactive clickable content — anti-pattern; closes on pointerdown, role="tooltip" is wrong semantics for a menu. Replace with Popover / HoverCard. [MenuItem.tsx:72–94]
  • [AI-Review][High] Backdrop (absolute inset-0 z-40) covers the icon-only sidebar; clicking sibling submenu icons only dismisses the current flyout (requires 2 clicks to switch). Add relative z-50 to the collapsed sidebar OR restrict backdrop to left-16 right-0. [Sidebar.tsx:124–128]
  • [AI-Review][High] Hover UI ≠ click UI for the same intent — hover shows a tooltip-mini-list, click opens the full flyout. Pick one: open flyout on hover too (with delay), or make tooltip purely informational. [MenuItem.tsx + Sidebar.tsx]

🟡 Medium

  • [AI-Review][Medium] No Escape key dismissal on the collapsed flyout — WAI-ARIA requires it for popovers/dialogs. [Sidebar.tsx:131–188]
  • [AI-Review][Medium] No focus management — opening the flyout via click doesn't move focus into it; closing doesn't restore focus to the trigger. [Sidebar.tsx:131–188]
  • [AI-Review][Medium] Flyout opens/closes with no transition; rest of layout uses transition-colors duration-150. Add fade/slide. [Sidebar.tsx:131–188]
  • [AI-Review][Medium] No tests covering isCollapsed + activeSubmenu (open via click, dismiss via backdrop, switch between submenus).
  • [AI-Review][Medium] Backdrop coupling — overlay logic lives inside Sidebar but visually covers <main>. Consider moving to MainLayout or extracting <SubmenuFlyout>. [Sidebar.tsx:124–128]

🟢 Low

  • [AI-Review][Low] cn helper duplicated in Sidebar.tsx:15–18 and MenuItem.tsx:11–14. Use the project's shared utility.
  • [AI-Review][Low] relative added to the layout container in MainLayout.tsx:129 — add a short comment noting it's the anchor for the collapsed-mode flyout.

🤖 Generated by /evo-code-review

marcelogorutuba and others added 2 commits May 12, 2026 15:55
…debar flyout

- Simplify collapsed Tooltip to show item name only (remove interactive links anti-pattern)
- Fix backdrop to start at left-16 so icon sidebar remains directly clickable (no 2-click toggle)
- Move backdrop from Sidebar into MainLayout to decouple overlay from sidebar component
- Add Escape key dismissal for collapsed flyout (WAI-ARIA)
- Add focus management: move focus into flyout on open, restore to trigger on close
- Add transition animation (duration-150) to flyout open/close
- Add Sidebar.spec.tsx covering collapsed+activeSubmenu scenarios (7 tests)
- Replace local cn helpers with shared import from @/utils/cn
- Add comment on relative container in MainLayout explaining flyout anchor role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…psed flyout

- Fix animation: keep flyout container always mounted when collapsed so
  CSS opacity+translate transitions actually run on show/hide
- Fix accessible name: add aria-label to close button
- Fix focus management: only capture previousFocusRef on first open,
  not when switching between submenus
- Add keyboard trap: Tab cycles within flyout and cannot escape
- Extract submenuHeader/submenuItems helpers to remove JSX duplication
- Update Sidebar.spec.tsx: fresh vi.fn() per test via makeProps factory,
  add tests for animation classes, aria-label, and Tab trap (11 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dpaes
Copy link
Copy Markdown

dpaes commented May 12, 2026

🔥 Code Review — Follow-ups (AI Review, iter 2)

Adversarial review against EVO-1048 acceptance criteria. AC coverage: ✅ AC #1 (click opens flyout when collapsed), ✅ AC #2 (every entry with submenu covered by the same code path), ✅ AC #3 (no regression in expanded mode — inline flyout preserved via class conditional).

Status of prior iteration: 9/9 prior findings addressed — simplified tooltip, backdrop fixed with left-16, Escape, focus mgmt, transition classes, tests, backdrop moved to MainLayout, shared cn util, relative comment. Good progress. 👏

Now, new findings on the latest commit:

🔴 High

  • [AI-Review][High] Transition animation is a placebotransition-all duration-150 ease-in-out on the flyout does not animate open/close because the <div> is conditionally mounted/unmounted ({activeSubmenu && (...)}). CSS transitions don't fire on mount/unmount — only on property changes of an element already in the DOM. The PR claim ("transition animation to flyout open/close") doesn't hold visually. Concrete fix: render the flyout whenever isCollapsed, and toggle opacity-0 -translate-x-2 pointer-events-noneopacity-100 translate-x-0 based on activeSubmenu. Alternative: migrate to Radix Popover (which handles entry/exit). [Sidebar.tsx:148–156]

🟡 Medium

  • [AI-Review][Medium] WAI-ARIA semantics on flyout missing — keyboard behavior (Escape + focus mgmt) is implemented, but the flyout <div> has no role="dialog" (or role="menu"), aria-modal="true", or aria-labelledby pointing to the <h3> with the submenu name. Screen readers don't announce that a popover opened, nor its title. [Sidebar.tsx:149–158]

  • [AI-Review][Medium] Backdrop in MainLayout lacks keyboard semantics and accessible name (Sourcery also flagged) — <div onClick={...} /> with no role="button", tabIndex={0}, aria-label="Close submenu", or key handler. Escape works via Sidebar, but jsx-a11y/click-events-have-key-events should warn and the backdrop isn't discoverable to screen readers. [MainLayout.tsx:142–147]

  • [AI-Review][Medium] Test coverage gaps:

🟢 Low

  • [AI-Review][Low] Focus lands on Close (X) button, not on first menu itemflyoutRef.current?.querySelector('button, [href], …') returns X first (DOM order in the header) before the sub-item <Link> elements. For a nav-menu pattern, the expected behavior is to start on the first navigable item. Use a more specific selector, e.g.: flyoutRef.current?.querySelector('nav a, nav button'). [Sidebar.tsx:66–71]

  • [AI-Review][Low] Focus bug in edge case: collapse toggle while flyout open — if the user expands the sidebar with flyout open and then collapses back, the focus effect re-enters if (activeSubmenu && isCollapsed) and overwrites previousFocusRef.current with the current document.activeElement (the toggle button or <body>), losing the original trigger reference. Fix: only update previousFocusRef.current when it's null (null→open transition), not on every re-entry of the branch. [Sidebar.tsx:62–73]

  • [AI-Review][Low] Focus restoration doesn't verify element still exists in DOM — if the original trigger was unmounted between open/close (permission change, hot reload, dashboard apps reloading the menu), .focus() silently fails and focus goes to <body>. Check document.contains(previousFocusRef.current) first; if missing, fall back to the first sidebar item. [Sidebar.tsx:75–78]

  • [AI-Review][Low] Fragile test selectorscreen.getAllByRole('button').find(btn => btn.querySelector('svg')) breaks silently the moment another svg-containing button shows up in the tested DOM. Use getByRole('button', { name: /close|fechar/i }) (backed by aria-label on the button) or a data-testid="submenu-close". [Sidebar.spec.tsx:111–114]


🤖 Generated by /evo-code-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants