Skip to content

Commit 195a277

Browse files
authored
Merge pull request #89 from opf/feature/mobile-behavior-for-pageHeader
[52582] Mobile behavior for page header
2 parents b41b67f + 72b2811 commit 195a277

17 files changed

Lines changed: 336 additions & 176 deletions

.changeset/proud-pears-smash.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@openproject/primer-view-components": minor
3+
---
4+
5+
Changes `Primer::OpenProject::PageHeader`:
6+
7+
* Remove the `context_bar_actions` slot
8+
* Make `breadcrumbs` required
9+
* Remove the `parent_link` slot (will be derived automatically from the breadcrumb)
10+
* Change the slot definition for `actions` to be type specific (allowed types are: icon_button, button, link, text and menu)
11+
* On mobile, the actions collapse into a single action menu which is placed in the context_bar

app/components/primer/open_project/page_header.html.erb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
2-
<% if parent_link || breadcrumbs || context_bar_actions %>
2+
<% if @parent_link || breadcrumbs || actions.any? %>
33
<div class="PageHeader-contextBar">
4-
<%= parent_link %>
4+
<%= @parent_link %>
55
<%= breadcrumbs %>
6-
<% if context_bar_actions.any? %>
7-
<%= render Primer::BaseComponent.new(tag: :div, classes: 'PageHeader-contextBarActions', display: DEFAULT_CONTEXT_BAR_ACTIONS_DISPLAY, align_items: :center) do %>
8-
<% context_bar_actions.each do |action| %>
9-
<%= action %>
10-
<% end %>
6+
<% if render_mobile_menu? %>
7+
<%= render(@mobile_action_menu) do |menu| %>
8+
<% menu.with_show_button(icon: :"kebab-horizontal", "aria-label": @mobile_menu_label) %>
9+
<% @desktop_menu_block.call(menu) unless @desktop_menu_block.nil? %>
1110
<% end %>
1211
<% end %>
1312
</div>

app/components/primer/open_project/page_header.pcss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@
4646
align-items: center;
4747
}
4848

49+
.PageHeader--singleAction .PageHeader-action {
50+
@media (max-width: 543.98px) {
51+
position: absolute;
52+
top: 10px;
53+
54+
/* Normally, the actions are hidden on mobile, except for this special case of a single action */
55+
display: flex !important;
56+
}
57+
}
58+
4959
.PageHeader-breadcrumbs {
5060
display: block;
5161
width: 100%;

app/components/primer/open_project/page_header.rb

Lines changed: 150 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ class PageHeader < Primer::Component
2020
"triangle-left"
2121
].freeze
2222

23+
DEFAULT_ACTION_SCHEME = :default
24+
MORE_MENU_DISPLAY = [:flex, :none].freeze
25+
2326
DEFAULT_LEADING_ACTION_DISPLAY = [:none, :flex].freeze
2427
DEFAULT_BREADCRUMBS_DISPLAY = [:none, :flex].freeze
2528
DEFAULT_PARENT_LINK_DISPLAY = [:block, :none].freeze
26-
DEFAULT_CONTEXT_BAR_ACTIONS_DISPLAY = [:flex, :none].freeze
2729

2830
status :open_project
2931

@@ -54,24 +56,50 @@ class PageHeader < Primer::Component
5456
# Actions
5557
#
5658
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
57-
renders_many :actions, lambda { |**system_arguments|
58-
deny_tag_argument(**system_arguments)
59-
system_arguments[:tag] = :div
60-
system_arguments[:ml] ||= 2
59+
renders_many :actions, types: {
60+
icon_button: lambda { |icon:, mobile_icon:, label:, scheme: DEFAULT_ACTION_SCHEME, **system_arguments|
61+
deny_tag_argument(**system_arguments)
62+
system_arguments = set_action_arguments(system_arguments, scheme: scheme)
63+
add_option_to_mobile_menu(system_arguments, mobile_icon, label, scheme)
6164

62-
Primer::BaseComponent.new(**system_arguments)
63-
}
65+
Primer::Beta::IconButton.new(icon: icon, "aria-label": label, **system_arguments)
66+
},
67+
button: lambda { |mobile_icon:, mobile_label:, scheme: DEFAULT_ACTION_SCHEME, **system_arguments|
68+
deny_tag_argument(**system_arguments)
69+
system_arguments = set_action_arguments(system_arguments, scheme: scheme)
70+
add_option_to_mobile_menu(system_arguments, mobile_icon, mobile_label, scheme)
6471

65-
# Context Bar Actions
66-
# By default shown on narrow screens. Can be overridden with system_argument: display
67-
#
68-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
69-
renders_many :context_bar_actions, lambda { |**system_arguments|
70-
deny_tag_argument(**system_arguments)
71-
system_arguments[:tag] = :div
72-
system_arguments[:ml] ||= 2
72+
Primer::Beta::Button.new(**system_arguments)
73+
},
74+
link: lambda { |mobile_icon:, mobile_label:, scheme: DEFAULT_ACTION_SCHEME, **system_arguments|
75+
deny_tag_argument(**system_arguments)
76+
system_arguments = set_action_arguments(system_arguments, scheme: scheme)
77+
add_option_to_mobile_menu(system_arguments, mobile_icon, mobile_label, scheme)
7378

74-
Primer::BaseComponent.new(**system_arguments)
79+
Primer::Beta::Link.new(**system_arguments)
80+
},
81+
# Should only be used rarely on a per-need basis
82+
text: lambda { |**system_arguments|
83+
system_arguments = set_action_arguments(system_arguments)
84+
85+
system_arguments[:color] ||= :muted
86+
87+
# Enforce that texts are hidden on mobile
88+
system_arguments[:display] = [:none, :flex]
89+
90+
Primer::Beta::Text.new(**system_arguments)
91+
},
92+
menu: {
93+
renders: lambda { |**system_arguments, &block|
94+
deny_tag_argument(**system_arguments)
95+
system_arguments[:menu_arguments] = set_action_arguments(system_arguments[:menu_arguments])
96+
97+
# Add the options individually to the mobile menu in the template
98+
@desktop_menu_block = block
99+
100+
PageHeaderActionMenu.new(**system_arguments)
101+
},
102+
},
75103
}
76104

77105
# Optional leading action prepend the title
@@ -93,21 +121,6 @@ class PageHeader < Primer::Component
93121
Primer::Beta::IconButton.new(icon: icon, **system_arguments)
94122
}
95123

96-
# Optional parent link in the context area
97-
# By default shown on narrow screens. Can be overridden with system_argument: display
98-
#
99-
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
100-
renders_one :parent_link, lambda { |icon: DEFAULT_BACK_BUTTON_ICON, **system_arguments, &block|
101-
deny_tag_argument(**system_arguments)
102-
system_arguments[:icon] = fetch_or_fallback(BACK_BUTTON_ICON_OPTIONS, icon, DEFAULT_BACK_BUTTON_ICON)
103-
system_arguments[:classes] = class_names(system_arguments[:classes], "PageHeader-parentLink")
104-
system_arguments[:display] ||= DEFAULT_PARENT_LINK_DISPLAY
105-
106-
render(Primer::Beta::Link.new(scheme: :primary, muted: true, **system_arguments)) do
107-
render(Primer::Beta::Octicon.new(icon: "arrow-left", "aria-label": "aria_label", mr: 2)) + content_tag(:span, &block)
108-
end
109-
}
110-
111124
# Optional breadcrumbs above the title row
112125
# By default shown on wider screens. Can be overridden with system_argument: display
113126
#
@@ -117,6 +130,25 @@ class PageHeader < Primer::Component
117130
system_arguments[:classes] = class_names(system_arguments[:classes], "PageHeader-breadcrumbs")
118131
system_arguments[:display] ||= DEFAULT_BREADCRUMBS_DISPLAY
119132

133+
# show parent link if there is a parent for current page
134+
if items.length > 1
135+
link_arguments = {}
136+
parent_item = items[items.length - 2]
137+
parsed_parent_item = anchor_tag_string?(parent_item) ? anchor_string_to_object(parent_item) : parent_item
138+
139+
link_arguments[:icon] = fetch_or_fallback(BACK_BUTTON_ICON_OPTIONS, DEFAULT_BACK_BUTTON_ICON)
140+
link_arguments[:href] = parsed_parent_item[:href]
141+
link_arguments[:classes] = class_names(link_arguments[:classes], "PageHeader-parentLink")
142+
link_arguments[:display] ||= DEFAULT_PARENT_LINK_DISPLAY
143+
144+
@parent_link = render(Primer::Beta::Link.new(scheme: :primary, muted: true, **link_arguments)) do
145+
render(Primer::Beta::Octicon.new(icon: "arrow-left",
146+
"aria-label": I18n.t("button_back"),
147+
mr: 2)
148+
) + content_tag(:span, parsed_parent_item[:text])
149+
end
150+
end
151+
120152
render(Primer::Beta::Breadcrumbs.new(**system_arguments)) do |breadcrumbs|
121153
items.each do |item|
122154
item = anchor_string_to_object(item) if anchor_tag_string?(item)
@@ -130,23 +162,83 @@ class PageHeader < Primer::Component
130162
end
131163
}
132164

133-
def initialize(**system_arguments)
165+
# @param mobile_menu_label [String] The tooltip label of the mobile menu
166+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
167+
def initialize(mobile_menu_label: I18n.t("label_more"), **system_arguments)
134168
@system_arguments = deny_tag_argument(**system_arguments)
169+
@mobile_menu_label = mobile_menu_label
135170

136-
@system_arguments[:tag] = :header
171+
@system_arguments[:tag] = :"page-header"
137172
@system_arguments[:classes] =
138173
class_names(
139174
@system_arguments[:classes],
140175
"PageHeader"
141176
)
177+
178+
@mobile_action_menu = Primer::Alpha::ActionMenu.new(
179+
display: MORE_MENU_DISPLAY,
180+
anchor_align: :end
181+
)
142182
end
143183

144184
def render?
145-
title?
185+
raise ArgumentError, "PageHeader needs a title and a breadcrumb. Please use the `with_title` and `with_breadcrumbs` slot" unless breadcrumbs? || Rails.env.production?
186+
title? && breadcrumbs?
187+
end
188+
189+
def before_render
190+
@system_arguments[:classes] = class_names(
191+
@system_arguments[:classes],
192+
"PageHeader--singleAction": !render_mobile_menu?
193+
)
194+
195+
content
196+
end
197+
198+
def render_mobile_menu?
199+
actions.count > 1
146200
end
147201

148202
private
149203

204+
def set_action_arguments(system_arguments, scheme: nil)
205+
system_arguments[:ml] ||= 2
206+
system_arguments[:display] = [:none, :flex]
207+
system_arguments[:scheme] = scheme unless scheme.nil?
208+
system_arguments[:classes] = class_names(
209+
system_arguments[:classes],
210+
"PageHeader-action",
211+
)
212+
213+
system_arguments[:id] ||= self.class.generate_id
214+
system_arguments
215+
end
216+
217+
def add_option_to_mobile_menu(system_arguments, mobile_icon, mobile_label, scheme)
218+
unless mobile_icon.nil? || mobile_label.nil?
219+
# In action menus, only :default and :danger are allowed
220+
scheme = DEFAULT_ACTION_SCHEME unless scheme == :danger
221+
222+
with_menu_item(id: system_arguments[:id], label: mobile_label, scheme: scheme) do |c|
223+
c.with_leading_visual_icon(icon: mobile_icon)
224+
end
225+
end
226+
end
227+
228+
def with_menu_item(id:, **system_arguments, &block)
229+
system_arguments = {
230+
**system_arguments,
231+
"data-for": id,
232+
"data-action": "click:page-header#menuItemClick"
233+
}
234+
235+
@mobile_action_menu.with_item(
236+
value: "",
237+
**system_arguments,
238+
&block
239+
)
240+
end
241+
150242
# transform anchor tag strings to {href, text} objects
151243
# e.g "\u003ca href=\"/admin\"\u003eAdministration\u003c/a\u003e"
152244
def anchor_string_to_object(html_string)
@@ -161,6 +253,30 @@ def anchor_string_to_object(html_string)
161253
def anchor_tag_string?(item)
162254
item.is_a?(String) && item.start_with?("\u003c")
163255
end
256+
257+
# A Helper class to create ActionMenus inside the PageHeader action slot
258+
class PageHeaderActionMenu < Primer::Component
259+
# @param menu_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionMenu) %>.
260+
# @param button_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %> or <%= link_to_component(Primer::Beta::IconButton) %>, depending on the value of the `icon:` argument.
261+
def initialize(menu_arguments: {}, button_arguments: {})
262+
@menu = Primer::Alpha::ActionMenu.new(**menu_arguments)
263+
@button = @menu.with_show_button(icon: "triangle-down", **button_arguments)
264+
end
265+
266+
def render_in(view_context, &block)
267+
super(view_context) do
268+
block.call(@menu, @button)
269+
end
270+
end
271+
272+
def before_render
273+
content
274+
end
275+
276+
def call
277+
render(@menu)
278+
end
279+
end
164280
end
165281
end
166282
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {controller} from '@github/catalyst'
2+
3+
@controller
4+
class PageHeaderElement extends HTMLElement {
5+
menuItemClick(event: Event) {
6+
const currentTarget = event.currentTarget as HTMLButtonElement
7+
8+
const id = currentTarget?.getAttribute('data-for')
9+
10+
if (id) {
11+
document.getElementById(id)?.click()
12+
}
13+
}
14+
}
15+
16+
declare global {
17+
interface Window {
18+
PageHeaderElement: typeof PageHeaderElement
19+
}
20+
}
21+
22+
if (!window.customElements.get('page-header')) {
23+
window.PageHeaderElement = PageHeaderElement
24+
window.customElements.define('page-header', PageHeaderElement)
25+
}

app/components/primer/primer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ import '../../../lib/primer/forms/primer_multi_input'
2222
import '../../../lib/primer/forms/primer_text_field'
2323
import '../../../lib/primer/forms/toggle_switch_input'
2424
import './alpha/action_menu/action_menu_element'
25+
import './open_project/page_header_element'

previews/primer/alpha/action_menu_preview.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def opens_dialog(menu_id: "menu-1")
326326
# @label In Scoll container
327327
#
328328
def in_scroll_container
329-
render_with_template()
329+
render_with_template
330330
end
331331

332332

previews/primer/open_project/border_grid_preview.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def playground(spacious: false)
2121
# @label Default Options
2222
#
2323
# @snapshot
24-
def default()
24+
def default
2525
render(Primer::OpenProject::BorderGrid.new) do |grid|
2626
grid.with_row { "Block 1" }
2727
grid.with_row { "Block 2" }
@@ -30,7 +30,7 @@ def default()
3030
end
3131

3232
# @label Spacious
33-
def spacious()
33+
def spacious
3434
render(Primer::OpenProject::BorderGrid.new(spacious: true)) do |grid|
3535
grid.with_row { "Block 1" }
3636
grid.with_row { "Block 2" }

previews/primer/open_project/input_group_preview.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class InputGroupPreview < ViewComponent::Preview
1111
# @snapshot
1212
def default
1313
render(Primer::OpenProject::InputGroup.new) do |menu|
14-
menu.with_text_input(name: 'a name', label: 'My input group', value: "Copyable value")
14+
menu.with_text_input(name: "a name", label: "My input group", value: "Copyable value")
1515
menu.with_trailing_action_clipboard_copy_button(id: "button", value: "Copyable value", aria: { label: "Copy some text" })
1616
end
1717
end
@@ -24,13 +24,13 @@ def default
2424
# @param input_width [Symbol] select [auto, small, medium, large, xlarge, xxlarge]
2525
def playground(
2626
trailing_action: :clipboardCopy,
27-
value: 'Copyable value',
27+
value: "Copyable value",
2828
visually_hide_label: false,
2929
readonly: true,
3030
input_width: :medium
3131
)
3232
render(Primer::OpenProject::InputGroup.new(input_width: input_width)) do |menu|
33-
menu.with_text_input(name: 'Test', label: 'My input group', visually_hide_label: visually_hide_label, value: value, readonly: readonly)
33+
menu.with_text_input(name: "Test", label: "My input group", visually_hide_label: visually_hide_label, value: value, readonly: readonly)
3434

3535
case trailing_action
3636
when :icon
@@ -46,15 +46,15 @@ def playground(
4646
# @label With icon button
4747
def icon_button
4848
render(Primer::OpenProject::InputGroup.new) do |menu|
49-
menu.with_text_input(name: 'a name', label: 'My input group', value: "Some value")
49+
menu.with_text_input(name: "a name", label: "My input group", value: "Some value")
5050
menu.with_trailing_action_icon(icon: :check, aria: { label: "Successful" })
5151
end
5252
end
5353

5454
# @label With a small input
5555
def small_input_width
5656
render(Primer::OpenProject::InputGroup.new(input_width: :small)) do |menu|
57-
menu.with_text_input(name: 'a name', label: 'My input group', value: "Some value")
57+
menu.with_text_input(name: "a name", label: "My input group", value: "Some value")
5858
menu.with_trailing_action_clipboard_copy_button(id: "button-4", value: "Some value", aria: { label: "Copy some text" })
5959
end
6060
end

0 commit comments

Comments
 (0)