Skip to content

Commit e3c1b28

Browse files
sampottsclaude
andauthored
feat(core): menu core layer and DOM keyboard navigation (#1503)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0cfd3bb commit e3c1b28

66 files changed

Lines changed: 6381 additions & 58 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/plans/menus.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Menus Implementation
2+
3+
Design doc: `internal/design/ui/menus.md`
4+
Branch: `feat/menu-ui-component` (PR #1078)
5+
6+
## Overview
7+
8+
Three PRs along the dependency chain:
9+
10+
```
11+
PR 1 (Core + DOM) → PR 2 (UI flat) → PR 3 (Submenus)
12+
```
13+
14+
---
15+
16+
## PR 1 — Core + DOM layer
17+
18+
**Status:** DONE — `feat/menu-core-dom` (PR #1503)
19+
20+
### Files
21+
22+
**New:**
23+
- `packages/core/src/core/ui/menu/menu-core.ts`
24+
- `packages/core/src/core/ui/menu/menu-data-attrs.ts`
25+
- `packages/core/src/core/ui/menu/menu-item-data-attrs.ts`
26+
- `packages/core/src/core/ui/menu/menu-css-vars.ts`
27+
- `packages/core/src/core/ui/menu/tests/menu-core.test.ts`
28+
- `packages/core/src/dom/ui/menu/create-menu.ts`
29+
- `packages/core/src/dom/ui/menu/tests/create-menu.test.ts`
30+
- `packages/core/src/dom/ui/menu/tests/create-menu-helpers.ts`
31+
32+
**Modified:**
33+
- `packages/core/src/core/index.ts` — add menu exports
34+
- `packages/core/src/dom/index.ts` — add menu exports
35+
- `packages/core/src/core/ui/transition.ts` — extract `TransitionDataAttrs`
36+
- `packages/core/src/core/ui/popover/popover-data-attrs.ts` — spread `TransitionDataAttrs`
37+
- `packages/core/src/core/ui/tooltip/tooltip-data-attrs.ts` — spread `TransitionDataAttrs`
38+
- `packages/core/src/core/ui/alert-dialog/alert-dialog-data-attrs.ts` — spread `TransitionDataAttrs`
39+
40+
### Key decisions
41+
- `MenuCore` follows `PopoverCore` pattern: `setProps` + `setInput(TransitionState)` + `getState()`
42+
- `isSubmenu` prop on `MenuCore` — suppresses `popover="manual"` in `getContentAttrs` and disables positioning props for nested menus
43+
- `MenuItemDataAttrs` is not constrained by `StateAttrMap` since items have their own state, not `MenuState`
44+
- `data-direction` belongs in DOM layer alongside `NavigationState`, not in core constants
45+
- `createMenu()` composes `createPopover()` internally; items stored as ordered array (registration order matches DOM order for standard React list rendering)
46+
- `destroy()` cancels the open RAF and typeahead timer before delegating to `popover.destroy()`
47+
- Open RAF guards against `status === 'ending'` to prevent highlight firing during a rapid open/close
48+
49+
---
50+
51+
## PR 2 — UI layer: flat menu (React + HTML)
52+
53+
**Status:** OPEN — `feat/menu-react-html` (PR #1504)
54+
55+
### React files (`packages/react/src/ui/menu/`)
56+
- `context.tsx`, `index.parts.ts`, `index.ts`
57+
- `menu-root.tsx`, `menu-trigger.tsx`, `menu-content.tsx`
58+
- `menu-item.tsx`, `menu-label.tsx`, `menu-separator.tsx`, `menu-group.tsx`
59+
- `menu-radio-group.tsx`, `menu-radio-item.tsx`, `menu-checkbox-item.tsx`, `menu-item-indicator.tsx`
60+
61+
### HTML files (`packages/html/src/ui/menu/`)
62+
- `menu-element.ts`, `menu-item-element.ts`, `menu-label-element.ts`, `menu-separator-element.ts`
63+
- `menu-group-element.ts`, `menu-radio-group-element.ts`, `menu-radio-item-element.ts`
64+
- `menu-checkbox-item-element.ts`, `menu-item-indicator-element.ts`
65+
66+
**Modified:**
67+
- `packages/react/src/ui/index.ts` — add Menu export
68+
- `packages/html/src/define/ui/menu.ts` — registration barrel
69+
- `packages/html/src/ui/index.ts` — add menu exports
70+
71+
### Scope
72+
- Fully functional flat single-level menu with items, radio groups, checkboxes, labels, separators
73+
- No nested Root / Back / submenu navigation — that comes in PR 4
74+
75+
---
76+
77+
## PR 3 — Submenu navigation
78+
79+
**Status:** OPEN — `feat/menu-sub` (PR pending)
80+
81+
### Files
82+
83+
**New DOM:**
84+
- `packages/core/src/dom/ui/menu/create-menu-view-transition.ts`
85+
- `packages/core/src/dom/ui/menu/menu-viewport-transition.ts`
86+
87+
**New React:**
88+
- `packages/react/src/ui/menu/menu-back.tsx`
89+
- `packages/react/src/ui/menu/menu-view.tsx`
90+
91+
**New HTML:**
92+
- `packages/html/src/ui/menu/menu-back-element.ts`
93+
- `packages/html/src/ui/menu/menu-view-element.ts`
94+
95+
**Modified:**
96+
- `packages/core/src/dom/ui/menu/create-menu.ts` — add `push`/`pop` to `MenuApi`, `NavigationState`, wire transition
97+
- `packages/react/src/ui/menu/menu-root.tsx` — nested Root detects parent context → submenu mode
98+
- `packages/react/src/ui/menu/menu-content.tsx``data-submenu`, `data-direction`, slide transition wiring
99+
- `packages/react/src/ui/menu/index.parts.ts` — export `Menu.Back` and `Menu.View`
100+
- `packages/html/src/ui/menu/menu-element.ts` — nested `<media-menu>` + `commandfor` support
101+
- `packages/html/src/ui/menu/menu-item-element.ts``commandfor` attribute handling
102+
- `packages/core/src/dom/index.ts` — add submenu and viewport transition exports
103+
- `packages/html/src/define/ui/menu.ts` — register `<media-menu-back>` and `<media-menu-view>`
104+
105+
**Status:** OPEN — branched off `feat/menu-react-html`
106+
107+
### Scope
108+
- `NavigationState`: stack of `{ menuId, triggerId }`, direction, exitingMenuId, transitioning
109+
- `createMenuViewTransition()`: generic menu view double-RAF lifecycle, data attribute hooks, `getAnimations()` settle
110+
- `menu-viewport-transition.ts`: shared root/child view measurement and `--media-menu-width/height` sizing
111+
- `Menu.View` / `<media-menu-view>`: optional root view wrapper for in-place view navigation; receives `data-menu-root-view` while root `Content` / `<media-menu>` acts as the shared viewport
112+
- Traditional flyout submenus are out of scope for this PR and should not require `Menu.View` / `<media-menu-view>` when added later
113+
- Nested `Menu.Root` detection via parent `MenuContext``isSubmenu: true` prop, Trigger registers as parent item
114+
- `Menu.Back` / `<media-menu-back>`: pops stack, focus restoration to trigger
115+
- Auto-back on `RadioItem` selection in submenu
116+
- RTL: direction-agnostic JS, CSS handles `translateX` flip via `[dir="rtl"]`
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Sandbox — HTML Menu</title>
7+
<link rel="preconnect" href="https://rsms.me/" />
8+
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
9+
10+
</head>
11+
<body class="bg-slate-50 text-slate-900 min-h-screen flex items-center justify-center p-8 font-[Inter,system-ui,sans-serif] antialiased">
12+
<div id="root"></div>
13+
<script type="module" src="./main.ts"></script>
14+
</body>
15+
</html>

0 commit comments

Comments
 (0)