mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Add BorderBoxList collapsible header option
Render plain BorderBoxList headers by default while keeping the CollapsibleHeader toggle available behind an explicit option. Align the plain header branch with CollapsibleHeader markup so later Backlogs header restyling can reuse the same structure.
This commit is contained in:
@@ -38,7 +38,9 @@ module OpenProject
|
||||
class BorderBoxListComponent < ApplicationComponent
|
||||
include OpPrimer::ComponentHelpers
|
||||
|
||||
attr_reader :container, :current_user, :header_id, :footer_id
|
||||
attr_reader :container, :collapsible, :current_user, :header_id, :footer_id
|
||||
|
||||
alias_method :collapsible?, :collapsible
|
||||
|
||||
# Optional header row.
|
||||
#
|
||||
@@ -55,6 +57,7 @@ module OpenProject
|
||||
system_arguments[:id] = header_id
|
||||
system_arguments[:list_id] = list_id
|
||||
system_arguments[:interactive] = interactive?
|
||||
system_arguments[:collapsible] = collapsible?
|
||||
|
||||
Header.new(**system_arguments)
|
||||
}
|
||||
@@ -157,14 +160,23 @@ module OpenProject
|
||||
# `dom_target` to derive DOM ids for the list and related controls.
|
||||
# @param interactive [Boolean] whether dynamic list updates should be
|
||||
# announced politely to assistive technology.
|
||||
# @param collapsible [Boolean] whether the header renders a collapsible
|
||||
# toggle. Defaults to `false`.
|
||||
# @param current_user [User] user context passed to work-package items.
|
||||
# @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox`.
|
||||
# Pass `id:` to set the box id; related ids are derived from it.
|
||||
def initialize(container:, interactive: false, current_user: User.current, **system_arguments)
|
||||
def initialize(
|
||||
container:,
|
||||
interactive: false,
|
||||
collapsible: false,
|
||||
current_user: User.current,
|
||||
**system_arguments
|
||||
)
|
||||
super()
|
||||
|
||||
@container = container
|
||||
@interactive = interactive
|
||||
@collapsible = collapsible
|
||||
@current_user = current_user
|
||||
@system_arguments = system_arguments.except(:list_id)
|
||||
|
||||
@@ -193,7 +205,7 @@ module OpenProject
|
||||
return unless header?
|
||||
|
||||
header.resolve_count!(items.size)
|
||||
return unless footer?
|
||||
return unless collapsible? && footer?
|
||||
|
||||
header.collapsible_id = [list_id, footer_id].compact.join(" ")
|
||||
end
|
||||
|
||||
@@ -15,22 +15,38 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%= grid_layout("op-border-box-list-header", tag: :div) do |grid| %>
|
||||
<% grid.with_area(:collapsible) do %>
|
||||
<%=
|
||||
render(
|
||||
Primer::OpenProject::BorderBox::CollapsibleHeader.new(
|
||||
collapsible_id:,
|
||||
collapsed:,
|
||||
multi_line: true
|
||||
)
|
||||
) do |collapsible|
|
||||
%>
|
||||
<% collapsible.with_title(tag: title_tag) { title } %>
|
||||
<% if render_count? %>
|
||||
<% collapsible.with_count(**counter_arguments) %>
|
||||
<% if collapsible? %>
|
||||
<%=
|
||||
render(
|
||||
Primer::OpenProject::BorderBox::CollapsibleHeader.new(
|
||||
collapsible_id:,
|
||||
collapsed:,
|
||||
multi_line: true
|
||||
)
|
||||
) do |collapsible|
|
||||
%>
|
||||
<% collapsible.with_title(tag: title_tag) { title } %>
|
||||
<% if render_count? %>
|
||||
<% collapsible.with_count(**counter_arguments) %>
|
||||
<% end %>
|
||||
<% if description? %>
|
||||
<% collapsible.with_description do %>
|
||||
<%= description %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if description? %>
|
||||
<% collapsible.with_description do %>
|
||||
<%= description %>
|
||||
<% else %>
|
||||
<%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader CollapsibleHeader--multi-line")) do %>
|
||||
<%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader-title-line")) do %>
|
||||
<%= render(Primer::Beta::Truncate.new(tag: title_tag, classes: "CollapsibleHeader-title Box-title")) { title } %>
|
||||
<% if render_count? %>
|
||||
<%= render(Primer::Beta::Counter.new(**counter_arguments, classes: class_names(counter_arguments[:classes], "CollapsibleHeader-count"))) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if description? %>
|
||||
<%= render(Primer::Beta::Text.new(color: :subtle, trim: true, classes: "CollapsibleHeader-description")) do %>
|
||||
<%= description %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -81,7 +81,8 @@ module OpenProject
|
||||
:title_tag,
|
||||
:list_id,
|
||||
:interactive,
|
||||
:collapsed
|
||||
:collapsed,
|
||||
:collapsible
|
||||
|
||||
attr_writer :collapsible_id
|
||||
|
||||
@@ -99,6 +100,9 @@ module OpenProject
|
||||
# @param interactive [Boolean] whether counter updates should be
|
||||
# announced politely to assistive technology.
|
||||
# @param collapsed [Boolean] whether the collapsible header starts closed.
|
||||
# @param collapsible [Boolean] whether the header renders a collapsible
|
||||
# toggle. Defaults to `true`. Pass `false` to render a plain title
|
||||
# without a toggle button.
|
||||
# @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox#with_header`.
|
||||
def initialize(
|
||||
title:,
|
||||
@@ -109,6 +113,7 @@ module OpenProject
|
||||
list_id: nil,
|
||||
interactive: false,
|
||||
collapsed: false,
|
||||
collapsible: true,
|
||||
**system_arguments
|
||||
)
|
||||
super()
|
||||
@@ -122,9 +127,15 @@ module OpenProject
|
||||
@interactive = interactive
|
||||
@collapsible_id = list_id
|
||||
@collapsed = collapsed
|
||||
@collapsible = collapsible
|
||||
@system_arguments = system_arguments
|
||||
end
|
||||
|
||||
# @return [Boolean] whether a collapsible toggle should be rendered.
|
||||
def collapsible?
|
||||
collapsible
|
||||
end
|
||||
|
||||
# Resolves inferred counts after the list slots have been captured.
|
||||
#
|
||||
# @param item_count [Integer] number of rendered item slots.
|
||||
|
||||
@@ -34,10 +34,12 @@ module OpenProject
|
||||
class BorderBoxListComponentPreview < ViewComponent::Preview
|
||||
# @label Default
|
||||
# @param interactive toggle
|
||||
def default(interactive: false)
|
||||
# @param collapsible toggle
|
||||
def default(interactive: false, collapsible: false)
|
||||
render OpenProject::Common::BorderBoxListComponent.new(
|
||||
container: "border-box-list-preview",
|
||||
interactive: boolean_preview_param(interactive)
|
||||
interactive: boolean_preview_param(interactive),
|
||||
collapsible: boolean_preview_param(collapsible)
|
||||
) do |list|
|
||||
list.with_header(title: "Things we're building", count: true) do |header|
|
||||
header.with_description { "There's lots to look forward to" }
|
||||
@@ -61,35 +63,30 @@ module OpenProject
|
||||
|
||||
# @label With work package items
|
||||
# @param interactive toggle
|
||||
def with_work_package_items(interactive: false)
|
||||
# @param collapsible toggle
|
||||
def with_work_package_items(interactive: false, collapsible: false)
|
||||
work_packages = WorkPackage.includes(:project).limit(2).to_a
|
||||
return preview_message("No work packages in the database.") if work_packages.empty?
|
||||
|
||||
render OpenProject::Common::BorderBoxListComponent.new(
|
||||
container: "border-box-list-work-package-preview",
|
||||
interactive: boolean_preview_param(interactive)
|
||||
interactive: boolean_preview_param(interactive),
|
||||
collapsible: boolean_preview_param(collapsible)
|
||||
) do |list|
|
||||
list.with_header(title: "Work packages", count: true)
|
||||
|
||||
work_packages.each do |work_package|
|
||||
list.with_work_package_item(work_package:) do |item|
|
||||
item.with_menu(button_aria_label: "Work package actions") do |menu|
|
||||
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}") do |menu_item|
|
||||
menu_item.with_leading_visual_icon(icon: :link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
render_work_package_items(list, work_packages)
|
||||
end
|
||||
end
|
||||
|
||||
# @label Playground
|
||||
# @param collapsible toggle
|
||||
# @param title_tag [Symbol] select [h2, h3, h4, h5]
|
||||
# @param count [Symbol] select [inferred, hidden, explicit, zero]
|
||||
# @param count_scheme [Symbol] select [primary, secondary]
|
||||
# @param hide_zero_count toggle
|
||||
# @param interactive toggle
|
||||
def playground(
|
||||
collapsible: false,
|
||||
title_tag: :h4,
|
||||
count: :inferred,
|
||||
count_scheme: :primary,
|
||||
@@ -98,7 +95,8 @@ module OpenProject
|
||||
)
|
||||
render OpenProject::Common::BorderBoxListComponent.new(
|
||||
container: "border-box-list-playground-preview",
|
||||
interactive: boolean_preview_param(interactive)
|
||||
interactive: boolean_preview_param(interactive),
|
||||
collapsible: boolean_preview_param(collapsible)
|
||||
) do |list|
|
||||
list.with_header(
|
||||
title: "Playground list",
|
||||
@@ -122,10 +120,12 @@ module OpenProject
|
||||
# @label Empty state
|
||||
# List with a header and an empty state (Blankslate), no items.
|
||||
# @param interactive toggle
|
||||
def empty_state(interactive: false)
|
||||
# @param collapsible toggle
|
||||
def empty_state(interactive: false, collapsible: false)
|
||||
render OpenProject::Common::BorderBoxListComponent.new(
|
||||
container: "border-box-list-empty-preview",
|
||||
interactive: boolean_preview_param(interactive)
|
||||
interactive: boolean_preview_param(interactive),
|
||||
collapsible: boolean_preview_param(collapsible)
|
||||
) do |list|
|
||||
list.with_header(title: "Empty list", count: 0)
|
||||
list.with_empty_state(
|
||||
@@ -159,6 +159,18 @@ module OpenProject
|
||||
blankslate.with_heading(tag: :h4).with_content(text)
|
||||
end
|
||||
end
|
||||
|
||||
def render_work_package_items(list, work_packages)
|
||||
work_packages.each do |work_package|
|
||||
list.with_work_package_item(work_package:) do |item|
|
||||
item.with_menu(button_aria_label: "Work package actions") do |menu|
|
||||
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}") do |menu_item|
|
||||
menu_item.with_leading_visual_icon(icon: :link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,7 +76,6 @@ module Backlogs
|
||||
title:,
|
||||
count: work_packages.size,
|
||||
count_label: default_count_label(count),
|
||||
collapsed: folded?,
|
||||
**system_arguments,
|
||||
&
|
||||
)
|
||||
@@ -84,7 +83,6 @@ module Backlogs
|
||||
title:,
|
||||
count:,
|
||||
count_label:,
|
||||
collapsed:,
|
||||
**system_arguments,
|
||||
&
|
||||
)
|
||||
@@ -120,10 +118,6 @@ module Backlogs
|
||||
}
|
||||
end
|
||||
|
||||
def folded?
|
||||
current_user.pref[:backlogs_versions_default_fold_state] == "closed"
|
||||
end
|
||||
|
||||
def default_count_label(count)
|
||||
return unless count
|
||||
|
||||
|
||||
@@ -151,32 +151,6 @@ RSpec.describe Backlogs::WorkPackageCardListComponent, type: :component do
|
||||
)
|
||||
end
|
||||
|
||||
context "when the user prefers closed folds" do
|
||||
before do
|
||||
user.pref[:backlogs_versions_default_fold_state] = "closed"
|
||||
end
|
||||
|
||||
it "renders the header as collapsed" do
|
||||
expect(rendered_component).to have_css(
|
||||
".CollapsibleHeader-triggerArea",
|
||||
aria: { expanded: "false" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user prefers open folds" do
|
||||
before do
|
||||
user.pref[:backlogs_versions_default_fold_state] = "open"
|
||||
end
|
||||
|
||||
it "renders the header as expanded" do
|
||||
expect(rendered_component).to have_css(
|
||||
".CollapsibleHeader-triggerArea",
|
||||
aria: { expanded: "true" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with work packages" do
|
||||
let(:header_arguments) { { title: "Sprint 1" } }
|
||||
let(:work_packages) do
|
||||
|
||||
@@ -396,7 +396,7 @@ RSpec.describe OpenProject::Common::BorderBoxListComponent, type: :component do
|
||||
describe "header collapsible behavior" do
|
||||
it "sets collapsible_id from list and footer ids" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "collapse-test")
|
||||
described_class.new(container: "collapse-test", collapsible: true)
|
||||
) do |list|
|
||||
list.with_header(title: "Collapsible")
|
||||
list.with_item { "row" }
|
||||
@@ -413,7 +413,7 @@ RSpec.describe OpenProject::Common::BorderBoxListComponent, type: :component do
|
||||
|
||||
it "sets collapsible_id from list id only when no footer" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "collapse-no-footer")
|
||||
described_class.new(container: "collapse-no-footer", collapsible: true)
|
||||
) do |list|
|
||||
list.with_header(title: "No footer")
|
||||
list.with_item { "row" }
|
||||
@@ -671,7 +671,7 @@ RSpec.describe OpenProject::Common::BorderBoxListComponent, type: :component do
|
||||
|
||||
it "derives the header ids when explicit slot ids are provided" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "ignored", id: "explicit-box")
|
||||
described_class.new(container: "ignored", id: "explicit-box", collapsible: true)
|
||||
) do |list|
|
||||
list.with_header(title: "Header", id: "explicit-header", list_id: "explicit-list")
|
||||
list.with_item { "row" }
|
||||
@@ -697,7 +697,7 @@ RSpec.describe OpenProject::Common::BorderBoxListComponent, type: :component do
|
||||
|
||||
it "derives the footer id from the explicit box id" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "ignored", id: "explicit-box")
|
||||
described_class.new(container: "ignored", id: "explicit-box", collapsible: true)
|
||||
) do |list|
|
||||
list.with_header(title: "Header")
|
||||
list.with_item { "row" }
|
||||
@@ -737,4 +737,34 @@ RSpec.describe OpenProject::Common::BorderBoxListComponent, type: :component do
|
||||
expect { described_class.new }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "collapsible" do
|
||||
it "renders a non-collapsible header by default" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "no-collapse")
|
||||
) do |list|
|
||||
list.with_header(title: "Non-collapsible header", count: 3) do |header|
|
||||
header.with_description { "Description text" }
|
||||
end
|
||||
list.with_item { "row" }
|
||||
end
|
||||
|
||||
expect(rendered).to have_heading("Non-collapsible header", level: 4)
|
||||
expect(rendered).to have_css(".Counter", text: "3")
|
||||
expect(rendered).to have_text("Description text")
|
||||
expect(rendered).to have_no_css("collapsible-header")
|
||||
expect(rendered).to have_no_css("[aria-controls]")
|
||||
end
|
||||
|
||||
it "renders a collapsible header when collapsible is true" do
|
||||
rendered = render_inline(
|
||||
described_class.new(container: "explicit-collapse", collapsible: true)
|
||||
) do |list|
|
||||
list.with_header(title: "Collapsible header")
|
||||
list.with_item { "row" }
|
||||
end
|
||||
|
||||
expect(rendered).to have_css("collapsible-header")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user