diff --git a/app/components/open_project/common/border_box_list_component.rb b/app/components/open_project/common/border_box_list_component.rb index af9f1c8c674..7dbf1e2fbba 100644 --- a/app/components/open_project/common/border_box_list_component.rb +++ b/app/components/open_project/common/border_box_list_component.rb @@ -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 diff --git a/app/components/open_project/common/border_box_list_component/header.html.erb b/app/components/open_project/common/border_box_list_component/header.html.erb index b8a3fcd402a..9bc37cafcc0 100644 --- a/app/components/open_project/common/border_box_list_component/header.html.erb +++ b/app/components/open_project/common/border_box_list_component/header.html.erb @@ -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 %> diff --git a/app/components/open_project/common/border_box_list_component/header.rb b/app/components/open_project/common/border_box_list_component/header.rb index def4041f1cb..6d5a516b77b 100644 --- a/app/components/open_project/common/border_box_list_component/header.rb +++ b/app/components/open_project/common/border_box_list_component/header.rb @@ -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. diff --git a/lookbook/previews/open_project/common/border_box_list_component_preview.rb b/lookbook/previews/open_project/common/border_box_list_component_preview.rb index 0714463cfe6..4a406b87055 100644 --- a/lookbook/previews/open_project/common/border_box_list_component_preview.rb +++ b/lookbook/previews/open_project/common/border_box_list_component_preview.rb @@ -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 diff --git a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb index 0daf4ee3277..5122de033ed 100644 --- a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb +++ b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb @@ -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 diff --git a/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb b/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb index f51b2f0090b..d9246595ff8 100644 --- a/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb @@ -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 diff --git a/spec/components/open_project/common/border_box_list_component_spec.rb b/spec/components/open_project/common/border_box_list_component_spec.rb index 0b6075f87c9..c072dc586cf 100644 --- a/spec/components/open_project/common/border_box_list_component_spec.rb +++ b/spec/components/open_project/common/border_box_list_component_spec.rb @@ -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