Skip to content

ActionMenu component - #13

Merged
srcdev merged 7 commits into
mainfrom
feature/action-menu
May 20, 2026
Merged

ActionMenu component#13
srcdev merged 7 commits into
mainfrom
feature/action-menu

Conversation

@srcdev

@srcdev srcdev commented May 20, 2026

Copy link
Copy Markdown
Owner

ActionMenu component

Adds a new ActionMenu molecule comprising two components: ActionMenu (the trigger + popover container) and ActionMenuItemCore (the individual row items).

What

An ellipsis button (lucide:ellipsis) that opens an anchored popover menu positioned below and right-aligned with the trigger. Each menu item is passed via an indexed dynamic slot (item-{n}) containing a single ActionMenuItemCore, which renders as a <button>, <NuxtLink> (internal / paths), or <a> (external URLs) depending on whether href is supplied.

Why a new component rather than extending InputButtonCore

InputButtonCore is a form primitive — it carries form-specific props (isPending, isPill, readonly, type) and markup assumptions that don't belong on menu items. ActionMenuItemCore duplicates only the minimal link/button resolution logic (4 computed lines) and has a purpose-built API: label (required), href (optional), #icon slot, and a click emit.

Decisions

  • Dynamic slots over a general default slotitem-{n} slots enforce that only ActionMenuItemCore content enters the <ul role="menu">, keeping the ARIA menu/menuitem tree intact and preventing arbitrary HTML from breaking the accessible structure. Consistent with the AccordianCore pattern already in the codebase.
  • CSS anchor positioning + Popover API — no JavaScript click-outside handling needed. The menu opens below the trigger and right-aligns with it (right: anchor(right)), flipping above via position-try-fallbacks: flip-block when near the bottom of the viewport.
  • useId() for the anchor name — the anchor name is generated internally as a --dashed-ident (--action-menu-anchor-{id}) so consumers never need to pass an ID prop.
  • WAI-ARIA menu keyboard pattern — arrow keys navigate between items, Home/End jump to first/last, Tab closes the menu (letting focus move naturally to the next DOM element), Escape is handled natively by the Popover API which also restores focus to the trigger. No focus trap — focus-trap-vue cycles Tab within the element, which is correct for dialogs but wrong for menus.
  • closeMenu returns focus to the trigger — when an item is activated by click, focus is explicitly returned to the trigger. Escape-close handles this automatically via the Popover API's built-in focus restoration.

Files

File Notes
ActionMenu.vue Trigger button, popover, item-{n} slot loop, keyboard handler
ActionMenuItemCore.vue Polymorphic button/link row with icon slot and arrow
CONSUMER-STYLING.md Full public CSS token reference with global/page/instance override examples
.claude/skills/components/action-menu.md AI agent skill doc: props, slots, link vs button decision table, notes
ActionMenu.stories.ts 5 stories: Default (live itemCount control), Mixed items, No icons, In Context (data row), ActionMenuItemCore standalone
ActionMenu.spec.ts 36 tests: structure, ARIA, dynamic slots, anchor name, close behaviour, keyboard navigation, focus management
ActionMenuItemCore.spec.ts 25 tests: button/link/NuxtLink rendering, label, icon slot, arrow, click emit, styleClassPassthrough reactivity

CSS tokens

All --action-menu-* and --action-menu-item-* tokens are documented in CONSUMER-STYLING.md. Recommended approach is a single global file (assets/styles/setup/07.components/action-menu.css) on :root, consistent with DisplayDialog and TabNavigation.

Test notes

Keyboard focus tests use vi.spyOn(el, 'focus') and vi.spyOn(document, 'activeElement', 'get') rather than asserting document.activeElement directly — jsdom does not honour focus() on elements inside a hidden popover (display: none), so spying on the call is the reliable approach.


61 tests passing, 0 failing.

@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
srcdev-design-system Ready Ready Preview, Comment May 20, 2026 8:33pm
srcdev-design-system-storybook Ready Ready Preview, Comment May 20, 2026 8:33pm

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new ActionMenu molecule (trigger + anchored popover) and ActionMenuItemCore (menu row rendered as button/internal NuxtLink/external link), along with Storybook stories, consumer styling docs, and a comprehensive Vitest suite. The PR also updates Nuxt/tooling dependencies and config to support the new component stack (Popover API + CSS anchor positioning) in dev/test environments.

Changes:

  • Introduce ActionMenu.vue and ActionMenuItemCore.vue with ARIA menu semantics, Popover API integration, and keyboard navigation.
  • Add Storybook stories, consumer token documentation, and new unit tests + snapshots for both components.
  • Update Nuxt/tooling dependencies and configuration (including vite.optimizeDeps and Nuxt version bumps).

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package.json Bumps Nuxt/tooling dependencies required by the new components and updated dev environment.
nuxt.config.ts Updates optimizeDeps includes and Nuxt compatibility date for the runtime/dev setup.
modules/colour-scheme.ts Adjusts runtimeConfig access typing for the colour-scheme module.
app/components/05.forms/input-button/InputButtonCore.vue Switches NuxtLink resolution approach to align with new link/button polymorphism usage.
app/components/02.molecules/action-menu/ActionMenu.vue New popover-backed action menu container with dynamic item slots and keyboard handling.
app/components/02.molecules/action-menu/ActionMenuItemCore.vue New polymorphic menu item core (button/internal link/external link) with tokenized styling.
app/components/02.molecules/action-menu/CONSUMER-STYLING.md Documents the public CSS token API and recommended override patterns for consumers.
app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts Adds Storybook coverage for common usage variants and in-context examples.
app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts Adds ActionMenu unit tests covering structure, ARIA, focus behavior, and keyboard nav.
app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts Adds ActionMenuItemCore unit tests covering rendering modes, slots, and emits.
app/components/02.molecules/action-menu/tests/snapshots/ActionMenu.spec.ts.snap Adds snapshots for ActionMenu test suite.
app/components/02.molecules/action-menu/tests/snapshots/ActionMenuItemCore.spec.ts.snap Adds snapshots for ActionMenuItemCore test suite.
.claude/skills/index.md Registers the new ActionMenu skill doc in the skills index.
.claude/skills/components/action-menu.md Adds component skill documentation for AI assistance/reference.
.claude/settings.json Updates Claude permissions/config (currently includes non-portable absolute paths/commands).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/components/02.molecules/action-menu/ActionMenu.vue
Comment thread app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts
Comment thread .claude/settings.json Outdated
@srcdev
srcdev merged commit f96286e into main May 20, 2026
6 checks passed
@srcdev
srcdev deleted the feature/action-menu branch May 20, 2026 20:37
srcdev added a commit that referenced this pull request Aug 6, 2026
…rfaces

- --theme-text-inverted's light-mode branch matched --theme-surface-inverted's own
  light-mode branch, making InputButtonCore's .secondary text invisible in light mode.
  Switched to --colour-theme-0.
- InputButtonCore's base and .tertiary hover states paired --theme-surface-hover
  (dark in light mode) with --theme-text (also dark in light mode) instead of
  --theme-on-surface — same invisible-text bug. Documented as pitfall #13 in Claude.md.
- 00.element-defaults.css referenced --theme-button-surface, a token never declared
  anywhere in the codebase (typo since introduction); corrected to --theme-surface —
  autofill background/box-shadow were silently no-ops.
- Add theming-form-geometry-tokens.md documenting the non-colour form/button/input
  token inventory; expand theming-partial-override.md's slot table to 11 tokens.
- PricingCard: add optional ribbonText prop for a diagonal corner-ribbon marketing
  callout, with story and stories.ts docs/args update.
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