Skip to content

feat(core): menu core layer and DOM keyboard navigation#1503

Open
sampotts wants to merge 7 commits intomainfrom
feat/menu-core-dom
Open

feat(core): menu core layer and DOM keyboard navigation#1503
sampotts wants to merge 7 commits intomainfrom
feat/menu-core-dom

Conversation

@sampotts
Copy link
Copy Markdown
Collaborator

@sampotts sampotts commented Apr 30, 2026

Summary

First in a stack of PRs implementing the menu component (see #1078 for the design doc).

  • Adds MenuCore — ARIA attrs, state computation, defaultProps — following the same pattern as PopoverCore
  • Adds MenuDataAttrs, MenuItemDataAttrs, and MenuCSSVars constants
  • Extracts TransitionDataAttrs into transition.ts and spreads it into Popover, Tooltip, and AlertDialog — removes duplicated data-starting-style/data-ending-style entries across four files
  • Adds createMenu() DOM factory: composes createPopover for open/close/escape/outside-click, adds roving tabindex item registry, ArrowUp/Down/Home/End/Enter/Space keyboard navigation, 500ms type-ahead, and focus management (first item on open; trigger on close)

Stack

PR Branch Description
👉 this feat/menu-core-dom Core + DOM layers
next feat/menu-react-html React components + HTML custom elements (flat menu)
next feat/menu-sub Submenu navigation stack

Test plan

  • pnpm -F @videojs/core test src/core/ui/menu — 17 tests (MenuCore)
  • pnpm -F @videojs/core test src/dom/ui/menu — 36 tests (createMenu)
  • pnpm typecheck — clean
  • pnpm lint — clean

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new menu primitive with focus/keyboard behavior and composes it with the Popover/DismissLayer lifecycle; regressions could impact accessibility and interaction flows. Also refactors shared transition data-attrs used by popover/tooltip/alert-dialog, which could affect styling/hooks that depend on these attributes.

Overview
Adds the first cut of a Menu primitive in @videojs/core: a new MenuCore (state + ARIA attrs), MenuDataAttrs/MenuItemDataAttrs, and MenuCSSVars, and exports these from packages/core/src/core/index.ts.

Introduces createMenu() in the DOM layer (exported from packages/core/src/dom/index.ts) that composes createPopover() for open/close behavior and implements roving-tabindex item registration, Arrow/Home/End navigation, Enter/Space activation, typeahead search, and focus restoration/cleanup.

Deduplicates transition-related data attributes by extracting TransitionDataAttrs into transition.ts and spreading it into PopoverDataAttrs, TooltipDataAttrs, and AlertDialogDataAttrs.

Reviewed by Cursor Bugbot for commit cd116ca. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

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

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment May 1, 2026 3:35am

Request Review

Comment thread packages/core/src/dom/ui/menu/create-menu.ts Outdated
Comment thread packages/core/src/dom/ui/menu/create-menu.ts Outdated
Adds the core and DOM layers for the menu component (PRs 1+2):

Core layer:
- `MenuCore` class with ARIA attr helpers, state computation, and defaultProps
- `MenuDataAttrs`, `MenuItemDataAttrs`, `MenuCSSVars` constants
- Extracts `TransitionDataAttrs` into `transition.ts` and spreads it into
  Popover, Tooltip, and AlertDialog data-attrs to remove duplication

DOM layer:
- `createMenu()` factory composing `createPopover` for open/close/escape/outside-click
- Roving tabindex with item registration (`registerItem(element) → cleanup`)
- Keyboard navigation: ArrowUp/Down (wrap), Home, End, Enter/Space
- Type-ahead search with 500ms debounce buffer
- Focus management: first item on open, trigger on close animation complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs from PR review:

1. destroy() delegated directly to popover.destroy(), leaving a pending
   requestAnimationFrame (scheduled on open) and typeaheadTimer uncancelled.
   If destroy() was called shortly after open(), the RAF would fire and call
   highlight() / element.focus() / onHighlightChange on a destroyed menu.

2. The RAF guard only checked `!active`, but active stays true during the
   closing animation (status: 'ending'). Calling open(); close() before the
   RAF fired caused the RAF to still execute and highlight the first item,
   leaving a stale highlightedItem that prevented focus on the next open.

Fixes: add openRafId tracking + cancel on destroy; extend RAF guard to also
check status !== 'ending'; wrap destroy() to clear timer and RAF first.

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

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 67decd7. Configure here.

Comment thread packages/core/src/dom/ui/menu/create-menu.ts
The popover positioning layer already computes and sets availableWidth
alongside availableHeight for every popover — omitting it from MenuCSSVars
was an inconsistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The registerItem cleanup was nulling highlightedItem and firing
onHighlightChange but leaving data-highlighted and tabIndex=0 on the
element. Use clearHighlight() so DOM cleanup, state reset, and callback
all happen consistently in one place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Base automatically changed from feat/menu-ui-component to main May 1, 2026 03:34
@netlify
Copy link
Copy Markdown

netlify Bot commented May 1, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit cd116ca
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69f41f84b8e1b40008eeb2bc
😎 Deploy Preview https://deploy-preview-1503--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 29.27 kB
/video (default + hls) 162.85 kB
/video (minimal) 26.43 kB
/video (minimal + hls) 160.12 kB
/audio (default) 27.14 kB
/audio (minimal) 24.39 kB
/background 4.15 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.72 kB
/media/dash-video 236.58 kB
/media/hls-video 134.87 kB
/media/mux-audio 160.89 kB
/media/mux-video 161.01 kB
/media/native-hls-video 4.62 kB
/media/simple-hls-video 16.01 kB
Players (5)
Entry Size
/video/player 7.00 kB
/audio/player 5.12 kB
/background/player 3.86 kB
/live-video/player 7.01 kB
/live-audio/player 5.13 kB
Skins (29)
Entry Type Size
/video/minimal-skin.css css 3.66 kB
/video/skin.css css 3.69 kB
/video/minimal-skin js 26.39 kB
/video/minimal-skin.tailwind js 26.62 kB
/video/skin js 29.28 kB
/video/skin.tailwind js 29.41 kB
/audio/minimal-skin.css css 2.67 kB
/audio/skin.css css 2.64 kB
/audio/minimal-skin js 24.36 kB
/audio/minimal-skin.tailwind js 24.56 kB
/audio/skin js 27.15 kB
/audio/skin.tailwind js 27.28 kB
/background/skin.css css 115 B
/background/skin js 1.15 kB
/live-video/minimal-skin.css css 3.66 kB
/live-video/skin.css css 3.69 kB
/live-video/minimal-skin js 26.26 kB
/live-video/minimal-skin.tailwind js 26.35 kB
/live-video/skin js 28.72 kB
/live-video/skin.tailwind js 28.81 kB
/live-audio/minimal-skin.css css 2.67 kB
/live-audio/skin.css css 2.64 kB
/live-audio/minimal-skin js 24.23 kB
/live-audio/minimal-skin.tailwind js 24.17 kB
/live-audio/skin js 26.64 kB
/live-audio/skin.tailwind js 26.65 kB
/base.css css 176 B
/shared.css css 88 B
/skin-element js 1.37 kB
UI Components (25)
Entry Size
/ui/alert-dialog 1009 B
/ui/alert-dialog-close 479 B
/ui/alert-dialog-description 351 B
/ui/alert-dialog-title 367 B
/ui/buffering-indicator 2.51 kB
/ui/captions-button 2.59 kB
/ui/cast-button 2.57 kB
/ui/compounds 4.21 kB
/ui/controls 2.50 kB
/ui/error-dialog 3.07 kB
/ui/fullscreen-button 2.61 kB
/ui/hotkey 1.98 kB
/ui/mute-button 2.58 kB
/ui/pip-button 2.59 kB
/ui/play-button 2.59 kB
/ui/playback-rate-button 2.70 kB
/ui/popover 1.85 kB
/ui/poster 2.32 kB
/ui/seek-button 2.60 kB
/ui/slider 1.50 kB
/ui/thumbnail 2.98 kB
/ui/time 2.51 kB
/ui/time-slider 3.92 kB
/ui/tooltip 2.01 kB
/ui/volume-slider 2.69 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 23.54 kB
/video (default + hls) 155.84 kB
/video (minimal) 21.16 kB
/video (minimal + hls) 153.43 kB
/audio (default) 19.11 kB
/audio (minimal) 17.65 kB
/background 756 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.21 kB
/media/hls-video 133.39 kB
/media/mux-audio 159.70 kB
/media/mux-video 159.68 kB
/media/native-hls-video 3.13 kB
/media/simple-hls-video 14.55 kB
Skins (26)
Entry Type Size
/video/minimal-skin.css css 3.58 kB
/video/skin.css css 3.60 kB
/video/minimal-skin js 21.07 kB
/video/minimal-skin.tailwind js 24.71 kB
/video/skin js 23.43 kB
/video/skin.tailwind js 24.80 kB
/audio/minimal-skin.css css 2.54 kB
/audio/skin.css css 2.50 kB
/audio/minimal-skin js 17.57 kB
/audio/minimal-skin.tailwind js 20.21 kB
/audio/skin js 19.04 kB
/audio/skin.tailwind js 20.20 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 3.58 kB
/live-video/skin.css css 3.60 kB
/live-video/minimal-skin js 18.27 kB
/live-video/minimal-skin.tailwind js 21.76 kB
/live-video/skin js 20.64 kB
/live-video/skin.tailwind js 21.91 kB
/live-audio/minimal-skin.css css 2.54 kB
/live-audio/skin.css css 2.50 kB
/live-audio/minimal-skin js 16.22 kB
/live-audio/minimal-skin.tailwind js 18.65 kB
/live-audio/skin js 17.75 kB
/live-audio/skin.tailwind js 18.75 kB
UI Components (21)
Entry Size
/ui/alert-dialog 1.05 kB
/ui/buffering-indicator 1.82 kB
/ui/captions-button 2.02 kB
/ui/cast-button 2.04 kB
/ui/controls 1.78 kB
/ui/error-dialog 2.30 kB
/ui/fullscreen-button 2.08 kB
/ui/live-button 2.06 kB
/ui/mute-button 2.02 kB
/ui/pip-button 1.98 kB
/ui/play-button 2.05 kB
/ui/playback-rate-button 2.05 kB
/ui/popover 1.84 kB
/ui/poster 1.75 kB
/ui/seek-button 2.11 kB
/ui/slider 3.31 kB
/ui/thumbnail 2.06 kB
/ui/time 2.51 kB
/ui/time-slider 2.95 kB
/ui/tooltip 2.26 kB
/ui/volume-slider 2.41 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

Path Base PR Diff %
/dom 11.93 kB 12.46 kB +542 B +4.4% 🔺
Entries (9)
Entry Size
. 5.39 kB
/dom 12.46 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.36 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 132.98 kB
/dom/media/mux 159.10 kB
/dom/media/native-hls 2.52 kB
/dom/media/simple-hls 13.89 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 4.29 kB
/dom 13.40 kB
/playback-engine 13.26 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant