Skip to content

Commit 135c8d7

Browse files
authored
Update Dropdown API (#613)
I'm preparing `Dropdown` component to be used in dotcom and later promoting it to beta. With that in mind, ### Breaking changes - Renamed `Dropdown` and `Dropdown::Menu` removing the suffix. - Updated the `button` slot to be called `summary` and use `system_arguments` instead of relying in the `summary_classes` attribute. ### Dropdown::Menu Before this update, we always built the menu using: ```html <ul> <li class="dropdown-item">text</li> ... ``` but this isn't always how we use Dropdows, so I updated `Dropdown::Menu` to either render items using a `<ul><li><item_tag>` structure or just adding the tags inside `details-menu`
1 parent 7c3991a commit 135c8d7

28 files changed

+605
-291
lines changed

CHANGELOG.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22

33
## main
44

5+
### Updates
6+
7+
* Allow `Dropdown` menu items to be rendered outside a list.
8+
9+
*Manuel Puyol*
10+
511
### Breaking changes
612

713
* Require a label or `aria-label` to be provided for `AutoComplete` component.
814

915
*Kate Higa*
1016

1117
* Renames:
18+
* `DropdownComponent` to `Dropdown`.
19+
* `Dropdown::MenuComponent` to `Dropdown::Menu`.
1220
* `Primer::ButtonMarketingComponent` to `Primer::Alpha::ButtonMarketing`.
1321
* `Primer::TextComponent` to `Primer::Beta::Text`.
1422

1523
*Manuel Puyol*
1624

25+
* Removes `summary_classes` attribute in favor of the `summary` slot in `Dropdown`.
26+
27+
*Manuel Puyol*
28+
1729
### Misc
1830

1931
* Add linter suggestions for `Button` component.
@@ -32,11 +44,11 @@
3244

3345
*Manuel Puyol, Kate Higa*
3446

35-
## 0.0.43
47+
* Add preliminary criteria for new `alpha` components.
3648

37-
* Upgrade primer/css to 17.2.1
49+
*Joel Hawksley*
3850

39-
*Jon Rohan*
51+
## 0.0.43
4052

4153
### New
4254

@@ -78,9 +90,9 @@
7890

7991
*Manuel Puyol*
8092

81-
* Add preliminary criteria for new `alpha` components.
93+
* Upgrade primer/css to 17.2.1
8294

83-
*Joel Hawksley*
95+
*Jon Rohan*
8496

8597
## 0.0.42
8698

app/assets/javascripts/primer_view_components.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/primer_view_components.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/components/primer/dropdown_component.html.erb renamed to app/components/primer/dropdown.html.erb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<%= render(Primer::DetailsComponent.new(**@system_arguments)) do |c| %>
2-
<% c.summary(classes: @summary_classes) do %>
2+
<% c.summary(**@button_arguments) do %>
33
<%= button %>
4+
<% if @with_caret %><div class="dropdown-caret"></div><% end %>
45
<% end %>
56

67
<% c.body do %>

app/components/primer/dropdown.rb

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# frozen_string_literal: true
2+
3+
module Primer
4+
# `Dropdown` is a lightweight context menu for housing navigation and actions.
5+
# They're great for instances where you don't need the full power (and code) of the SelectMenu.
6+
class Dropdown < Primer::Component
7+
# Required trigger for the dropdown. Has the same arguments as <%= link_to_component(Primer::ButtonComponent) %>,
8+
# but it is locked as a `summary` tag.
9+
renders_one :button, lambda { |**system_arguments, &block|
10+
@button_arguments = system_arguments
11+
@button_arguments[:button] = true
12+
13+
view_context.capture { block&.call }
14+
}
15+
16+
# Required context menu for the dropdown.
17+
#
18+
# @param as [Symbol] When `as` is `:list`, wraps the menu in a `<ul>` with a `<li>` for each item.
19+
# @param direction [Symbol] <%= one_of(Primer::Dropdown::Menu::DIRECTION_OPTIONS) %>
20+
# @param scheme [Symbol] Pass `:dark` for dark mode theming
21+
# @param header [String] Optional string to display as the header
22+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
23+
renders_one :menu, "Primer::Dropdown::Menu"
24+
25+
# @example Default
26+
# <%= render(Primer::Dropdown.new) do |c| %>
27+
# <% c.button do %>
28+
# Dropdown
29+
# <% end %>
30+
#
31+
# <%= c.menu(header: "Options") do |menu|
32+
# menu.item { "Item 1" }
33+
# menu.item { "Item 2" }
34+
# menu.item { "Item 3" }
35+
# end %>
36+
# <% end %>
37+
#
38+
# @example With dividers
39+
#
40+
# @description
41+
# Dividers can be used to separate a group of items. They don't have any content.
42+
# @code
43+
# <%= render(Primer::Dropdown.new) do |c| %>
44+
# <% c.button do %>
45+
# Dropdown
46+
# <% end %>
47+
#
48+
# <%= c.menu(header: "Options") do |menu|
49+
# menu.item { "Item 1" }
50+
# menu.item { "Item 2" }
51+
# menu.item(divider: true)
52+
# menu.item { "Item 3" }
53+
# menu.item { "Item 4" }
54+
# menu.item(divider: true)
55+
# menu.item { "Item 5" }
56+
# menu.item { "Item 6" }
57+
# end %>
58+
# <% end %>
59+
#
60+
# @example With direction
61+
# <%= render(Primer::Dropdown.new(display: :inline_block)) do |c| %>
62+
# <% c.button do %>
63+
# Dropdown
64+
# <% end %>
65+
#
66+
# <%= c.menu(header: "Options", direction: :s) do |menu|
67+
# menu.item { "Item 1" }
68+
# menu.item { "Item 2" }
69+
# menu.item { "Item 3" }
70+
# menu.item { "Item 4" }
71+
# end %>
72+
# <% end %>
73+
#
74+
# @example With caret
75+
# <%= render(Primer::Dropdown.new(with_caret: true)) do |c| %>
76+
# <% c.button do %>
77+
# Dropdown
78+
# <% end %>
79+
#
80+
# <%= c.menu(header: "Options") do |menu|
81+
# menu.item { "Item 1" }
82+
# menu.item { "Item 2" }
83+
# menu.item { "Item 3" }
84+
# menu.item { "Item 4" }
85+
# end %>
86+
# <% end %>
87+
#
88+
# @example Customizing the button
89+
# <%= render(Primer::Dropdown.new) do |c| %>
90+
# <% c.button(scheme: :primary, variant: :small) do %>
91+
# Dropdown
92+
# <% end %>
93+
#
94+
# <%= c.menu(header: "Options") do |menu|
95+
# menu.item { "Item 1" }
96+
# menu.item { "Item 2" }
97+
# menu.item { "Item 3" }
98+
# menu.item { "Item 4" }
99+
# end %>
100+
# <% end %>
101+
#
102+
# @example Menu as list
103+
# <%= render(Primer::Dropdown.new) do |c| %>
104+
# <% c.button do %>
105+
# Dropdown
106+
# <% end %>
107+
#
108+
# <%= c.menu(as: :list, header: "Options") do |menu|
109+
# menu.item { "Item 1" }
110+
# menu.item { "Item 2" }
111+
# menu.item(divider: true)
112+
# menu.item { "Item 3" }
113+
# menu.item { "Item 4" }
114+
# end %>
115+
# <% end %>
116+
#
117+
# @example Customizing menu items
118+
# <%= render(Primer::Dropdown.new) do |c| %>
119+
# <% c.button do %>
120+
# Dropdown
121+
# <% end %>
122+
#
123+
# <%= c.menu(header: "Options") do |menu|
124+
# menu.item(tag: :button) { "Item 1" }
125+
# menu.item(classes: "custom-class") { "Item 2" }
126+
# menu.item { "Item 3" }
127+
# end %>
128+
# <% end %>
129+
#
130+
# @param overlay [Symbol] <%= one_of(Primer::DetailsComponent::OVERLAY_MAPPINGS.keys) %>
131+
# @param with_caret [Boolean] Whether or not a caret should be rendered in the button.
132+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
133+
def initialize(overlay: :default, with_caret: false, **system_arguments)
134+
@with_caret = with_caret
135+
136+
@system_arguments = system_arguments
137+
@system_arguments[:overlay] = overlay
138+
@system_arguments[:reset] = true
139+
@system_arguments[:classes] = class_names(
140+
@system_arguments[:classes],
141+
"dropdown"
142+
)
143+
end
144+
145+
def render?
146+
button.present? && menu.present?
147+
end
148+
end
149+
end

app/components/primer/dropdown.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './dropdown/menu'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
2+
<% if @header.present? %>
3+
<div class="dropdown-header">
4+
<%= @header %>
5+
</div>
6+
<% end %>
7+
8+
<% if list? %>
9+
<ul>
10+
<% items.each do |item| %>
11+
<% if item.divider? %>
12+
<%= item %>
13+
<% else %>
14+
<li>
15+
<%= item %>
16+
</li>
17+
<% end %>
18+
<% end %>
19+
</ul>
20+
<% else %>
21+
<% items.each do |item| %>
22+
<%= item %>
23+
<% end %>
24+
<% end %>
25+
<% end %>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
module Primer
4+
class Dropdown
5+
# This component is part of `Dropdown` and should not be
6+
# used as a standalone component.
7+
class Menu < Primer::Component
8+
AS_DEFAULT = :default
9+
AS_OPTIONS = [AS_DEFAULT, :list].freeze
10+
11+
SCHEME_DEFAULT = :default
12+
SCHEME_MAPPINGS = {
13+
SCHEME_DEFAULT => "",
14+
:dark => "dropdown-menu-dark"
15+
}.freeze
16+
17+
DIRECTION_DEFAULT = :se
18+
DIRECTION_OPTIONS = [DIRECTION_DEFAULT, :sw, :w, :e, :ne, :s].freeze
19+
20+
# @param tag [Symbol] <%= one_of(Primer::Dropdown::Menu::Item::TAG_OPTIONS) %>.
21+
# @param divider [Boolean] Whether the item is a divider without any function.
22+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
23+
renders_many :items, lambda { |divider: false, **system_arguments|
24+
Primer::Dropdown::Menu::Item.new(as: @as, divider: divider, **system_arguments)
25+
}
26+
27+
# @param as [Symbol] When `as` is `:list`, wraps the menu in a `<ul>` with a `<li>` for each item.
28+
# @param direction [Symbol] <%= one_of(Primer::Dropdown::Menu::DIRECTION_OPTIONS) %>.
29+
# @param header [String] Header to be displayed above the menu.
30+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
31+
def initialize(as: AS_DEFAULT, direction: DIRECTION_DEFAULT, scheme: SCHEME_DEFAULT, header: nil, **system_arguments)
32+
@header = header
33+
@direction = direction
34+
@as = fetch_or_fallback(AS_OPTIONS, as, AS_DEFAULT)
35+
36+
@system_arguments = system_arguments
37+
@system_arguments[:tag] = "details-menu"
38+
@system_arguments[:role] = "menu"
39+
40+
@system_arguments[:classes] = class_names(
41+
@system_arguments[:classes],
42+
"dropdown-menu",
43+
"dropdown-menu-#{fetch_or_fallback(DIRECTION_OPTIONS, direction, DIRECTION_DEFAULT)}",
44+
SCHEME_MAPPINGS[fetch_or_fallback(SCHEME_MAPPINGS.keys, scheme, SCHEME_DEFAULT)]
45+
)
46+
end
47+
48+
private
49+
50+
def list?
51+
@as == :list
52+
end
53+
54+
# Items to be rendered in the `Dropdown` menu.
55+
class Item < Primer::Component
56+
TAG_DEFAULT = :a
57+
BUTTON_TAGS = [:button, :summary].freeze
58+
TAG_OPTIONS = [TAG_DEFAULT, *BUTTON_TAGS].freeze
59+
60+
def initialize(as:, tag: TAG_DEFAULT, divider: false, **system_arguments)
61+
@divider = divider
62+
@as = as
63+
64+
@system_arguments = system_arguments
65+
@system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, TAG_DEFAULT)
66+
@system_arguments[:tag] = :li if list? && divider?
67+
@system_arguments[:role] ||= :menuitem
68+
@system_arguments[:role] = :separator if divider
69+
@system_arguments[:classes] = class_names(
70+
@system_arguments[:classes],
71+
"dropdown-item" => !divider,
72+
"dropdown-divider" => divider
73+
)
74+
end
75+
76+
def call
77+
component = if BUTTON_TAGS.include?(@system_arguments[:tag])
78+
Primer::ButtonComponent.new(scheme: :link, **@system_arguments)
79+
else
80+
Primer::BaseComponent.new(**@system_arguments)
81+
end
82+
83+
# divider has no content
84+
render(component) if divider?
85+
86+
render(component) { content }
87+
end
88+
89+
def divider?
90+
@divider
91+
end
92+
93+
def list?
94+
@as == :list
95+
end
96+
end
97+
end
98+
end
99+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '@github/details-menu-element'

app/components/primer/dropdown/menu_component.html.erb

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)