Skip to content

feat(components): add menu_button component with keyboard navigation#7170

Open
elisa-a-v wants to merge 12 commits intomainfrom
menu-button-component
Open

feat(components): add menu_button component with keyboard navigation#7170
elisa-a-v wants to merge 12 commits intomainfrom
menu-button-component

Conversation

@elisa-a-v
Copy link
Copy Markdown
Contributor

@elisa-a-v elisa-a-v commented Mar 31, 2026

Summary

Closes #7064

Adds a c-menu-button Cotton component implementing the ARIA menu-button pattern using the Alpine UI x-menu plugin. Includes keyboard navigation support with visual active-item feedback via $menuItem.isActive.

What's included

  • c-menu-button component with sub-components: c-menu-button.item, c-menu-button.divider
  • menuButton Alpine component with itemClass getter for keyboard-active item styling
  • Header refactored to use c-menu-button for both the "Support FLP" dropdown and the profile menu
  • Banner refactored to use c-menu-button for dropdown buttons and c-button for plain links (drops expansion_panel.js + collapse plugin dependency)
  • Menu button trigger uses c-button internally, with button_variant prop for variant control
  • Chevron caret rotation restored via group-aria-[expanded=true]:rotate-180 (CSS-only, replaces the old JS-based rotation from header.js)
  • Missing @alpinejs/anchor plugin added (was omitted from Upgrade Alpine.js 3.14.8 → 3.15.9, drop version from filenames #7167)
  • Component library entry on /components/
  • Header tests updated to match new markup

Dependencies

  • Blocked by Depends on #7167 (Alpine upgrade 3.14.8 → 3.15.9, now merged) — the UI plugin's static string bindings (role, tabindex, x-ref) require the CSP-compliant expression parser introduced in Alpine v3.15.0.

Keyboard behavior (ARIA menu-button pattern)

  • Arrow keys open menu and navigate between items
  • Enter/Space activates the highlighted item
  • Escape closes menu and returns focus to trigger
  • Tab moves focus out of the menu (expected — the menu is a single focus stop)
  • Focus stays on the menu container; active item indicated via aria-activedescendant

Test plan

  • /components/ page: menu button demo opens, navigates with keyboard, closes with Escape
  • Header (homepage): "Support FLP" dropdown works, chevron rotates on open/close
  • Header (signed in): profile dropdown works, items highlight on keyboard nav, hover state correct
  • Banner (homepage): "Install now" dropdown works with keyboard, "Learn more" links work
  • Signed-out header: Login/Sign-up buttons render correctly

🤖 Generated with Claude Code

elisa-a-v and others added 5 commits March 16, 2026 20:37
…ugin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add menu_button section to the components page with demo, slots,
props, and sub-component documentation. Remove unused disabled
attr from item.html c-vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nuItem

Add menuButton Alpine component with itemClass getter that uses
$menuItem.isActive for keyboard navigation visual feedback. Move
hover/active styling from <li> to the <a>/<button> menu item elements
where the Alpine UI plugin scope is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

This comment has been minimized.

{{ slot }}
</button>
{% else %}
<a x-menu:item href="{{ href }}" x-bind:class="itemClass" data-focused-class="{{ active_class }}" data-item-class="{{ item_class }}" {{ attrs }}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
Detected a template variable used in an anchor tag with the 'href' attribute. This allows a malicious actor to input the 'javascript:' URI and is subject to cross- site scripting (XSS) attacks. If using Flask, use 'url_for()' to safely generate a URL. If using Django, use the 'url' filter to safely generate a URL. If using Mustache, use a URL encoding library, or prepend a slash '/' to the variable for relative links (href="/{{link}}"). You may also consider setting the Content Security Policy (CSP) header.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by var-in-href.

You can view more details about this finding in the Semgrep AppSec Platform.

elisa-a-v and others added 6 commits April 8, 2026 02:26
The anchor plugin was omitted from the Alpine 3.15.9 upgrade (#7167).
It's required by the menu_button component for dropdown positioning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace plain <button> trigger with <c-button>, add button_variant
  prop (default: "primary") so callers can set the variant
- Fix caret rotation regression: old header.js rotated chevrons via
  JS, now handled with group-aria-[expanded=true]:rotate-180 in CSS
- Update header profile menu to use button_variant="outline"
- Update v2_components docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l dependency

Replace hand-rolled dropdown with c-menu-button component and plain
links with c-button. Banner no longer needs expansion_panel.js or the
collapse plugin. Button data uses variant names instead of CSS classes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests were checking for old header markup (hardcoded IDs, x-data="header")
that no longer exists after the menu_button refactor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: To Do

Development

Successfully merging this pull request may close these issues.

Component: Dropdown button

2 participants