Skip to content

Commit d799b7a

Browse files
jacksonpiresclaude
andauthored
[Feature] Allow DropdownMenuItem to render as a div via as: (#427)
Add an `as:` option to `RubyUI::DropdownMenuItem` (default `:a`, option `:div`) so an item can host its own interactive element (e.g. a dialog or form trigger) without nesting a `<button>`/`<form>` inside the item's `<a>` (invalid HTML and a keyboard/accessibility issue). The `:div` variant keeps the menu-item styling, `role="menuitem"` and behavior, and drops `href`. `href` is tied to the rendered element (set unless `:div`), so an unexpected `as:` value still produces a valid `<a href>`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 857b515 commit d799b7a

4 files changed

Lines changed: 69 additions & 5 deletions

File tree

docs/app/views/docs/dropdown_menu.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ def view_template
2626
RUBY
2727
end
2828

29+
render Docs::VisualCodeExample.new(title: "Non-navigational item", description: "Use as: :div when the item hosts its own interactive element (e.g. a dialog or form trigger). This avoids nesting a <button>/<form> inside the item's <a> while keeping the menu-item styling, role and keyboard behavior.", context: self) do
30+
<<~RUBY
31+
DropdownMenu do
32+
DropdownMenuTrigger(class: 'w-full') do
33+
Button(variant: :outline) { "Open" }
34+
end
35+
DropdownMenuContent do
36+
DropdownMenuItem(href: '#') { "Edit" }
37+
DropdownMenuItem(as: :div) do
38+
Dialog do
39+
DialogTrigger(class: 'w-full text-left') { "Delete" }
40+
DialogContent do
41+
DialogHeader do
42+
DialogTitle { "Are you absolutely sure?" }
43+
DialogDescription { "This action cannot be undone." }
44+
end
45+
DialogFooter do
46+
DialogClose { Button(variant: :destructive) { "Delete" } }
47+
end
48+
end
49+
end
50+
end
51+
end
52+
end
53+
RUBY
54+
end
55+
2956
render Docs::VisualCodeExample.new(title: "Placement", description: "If the DropdownMenu conflicts with edge, it will auto-adjust it's placement", context: self) do
3057
<<~RUBY
3158
div(class: 'grid grid-cols-1 sm:grid-cols-3 gap-4') do

gem/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,33 @@
22

33
module RubyUI
44
class DropdownMenuItem < Base
5-
def initialize(href: "#", **attrs)
5+
def initialize(as: :a, href: "#", **attrs)
6+
@as = as
67
@href = href
78
super(**attrs)
89
end
910

1011
def view_template(&)
11-
a(**attrs, &)
12+
if @as == :div
13+
div(**attrs, &)
14+
else
15+
a(**attrs, &)
16+
end
1217
end
1318

1419
private
1520

1621
def default_attrs
17-
{
18-
href: @href,
22+
base = {
1923
role: "menuitem",
2024
class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
2125
data_action: "click->ruby-ui--dropdown-menu#close",
2226
data_ruby_ui__dropdown_menu_target: "menuItem",
2327
tabindex: "-1",
2428
data_orientation: "vertical"
2529
}
30+
base[:href] = @href unless @as == :div
31+
base
2632
end
2733
end
2834
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class RubyUI::DropdownMenuItemTest < ComponentTest
6+
def test_renders_as_anchor_by_default
7+
output = phlex { RubyUI.DropdownMenuItem(href: "/edit") { "Edit" } }
8+
9+
assert_match(/\A<a/, output)
10+
assert_match(/href="\/edit"/, output)
11+
assert_match(/role="menuitem"/, output)
12+
assert_match(/Edit/, output)
13+
end
14+
15+
def test_renders_as_div_when_as_div_keeping_role_and_dropping_href
16+
output = phlex { RubyUI.DropdownMenuItem(as: :div) { "Delete" } }
17+
18+
assert_match(/\A<div/, output)
19+
assert_match(/role="menuitem"/, output)
20+
assert_match(/Delete/, output)
21+
refute_match(/<a[\s>]/, output)
22+
refute_match(/href=/, output)
23+
end
24+
25+
def test_unexpected_as_value_still_renders_an_anchor_with_href
26+
output = phlex { RubyUI.DropdownMenuItem(as: :anchor, href: "/edit") { "Edit" } }
27+
28+
assert_match(/\A<a/, output)
29+
assert_match(/href="\/edit"/, output)
30+
end
31+
end

mcp/data/registry.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1322,7 +1322,7 @@
13221322
},
13231323
{
13241324
"path": "dropdown_menu_item.rb",
1325-
"content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DropdownMenuItem < Base\n def initialize(href: \"#\", **attrs)\n @href = href\n super(**attrs)\n end\n\n def view_template(&)\n a(**attrs, &)\n end\n\n private\n\n def default_attrs\n {\n href: @href,\n role: \"menuitem\",\n class: \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n data_action: \"click->ruby-ui--dropdown-menu#close\",\n data_ruby_ui__dropdown_menu_target: \"menuItem\",\n tabindex: \"-1\",\n data_orientation: \"vertical\"\n }\n end\n end\nend\n"
1325+
"content": "# frozen_string_literal: true\n\nmodule RubyUI\n class DropdownMenuItem < Base\n def initialize(as: :a, href: \"#\", **attrs)\n @as = as\n @href = href\n super(**attrs)\n end\n\n def view_template(&)\n if @as == :div\n div(**attrs, &)\n else\n a(**attrs, &)\n end\n end\n\n private\n\n def default_attrs\n base = {\n role: \"menuitem\",\n class: \"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n data_action: \"click->ruby-ui--dropdown-menu#close\",\n data_ruby_ui__dropdown_menu_target: \"menuItem\",\n tabindex: \"-1\",\n data_orientation: \"vertical\"\n }\n base[:href] = @href unless @as == :div\n base\n end\n end\nend\n"
13261326
},
13271327
{
13281328
"path": "dropdown_menu_label.rb",

0 commit comments

Comments
 (0)