diff --git a/app/components/op_primer/border_box_row_component.html.erb b/app/components/op_primer/border_box_row_component.html.erb index f2ffcf6ad838..0b350142c64f 100644 --- a/app/components/op_primer/border_box_row_component.html.erb +++ b/app/components/op_primer/border_box_row_component.html.erb @@ -34,11 +34,17 @@ See COPYRIGHT and LICENSE files for more details. classes: "#{table.grid_class} #{row_css_class}", ) do %> <% columns.each do |column| %> - <%= render(Primer::BaseComponent.new(tag: :div, classes: column_css_class(column), **column_args(column))) { column_value(column) } %> + <%= render(Primer::BaseComponent.new(tag: :div, classes: grid_column_classes(column), **column_args(column))) do %> + <% label = mobile_label(column) %> + <% if label.present? %> + <%= render(Primer::Beta::Text.new(classes: "op-border-box-grid--row-label")) { "#{label}: " } %> + <% end %> + <%= column_value(column) %> + <% end %> <% end %> <% if table.has_actions? %> - <%= flex_layout(align_items: :center, justify_content: :flex_end) do |flex| %> + <%= flex_layout(classes: "op-border-box-grid--row-action") do |flex| %> <% button_links.each_with_index do |link, i| %> <% args = i == (button_links.count - 1) ? {} : { mr: 1 } %> <% flex.with_column(**args) { link } %> diff --git a/app/components/op_primer/border_box_row_component.rb b/app/components/op_primer/border_box_row_component.rb index 5c3b21120158..fabc5e2be67e 100644 --- a/app/components/op_primer/border_box_row_component.rb +++ b/app/components/op_primer/border_box_row_component.rb @@ -32,6 +32,25 @@ module OpPrimer class BorderBoxRowComponent < RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent include ComponentHelpers + def mobile_label(column) + return unless table.mobile_labels.include?(column) + + table.column_title(column) + end + + def visible_on_mobile?(column) + table.mobile_columns.include?(column) + end + + def grid_column_classes(column) + classes = ["op-border-box-grid--row-item"] + classes << column_css_class(column) + classes << "op-border-box-grid--wide-column" if table.wide_column?(column) + classes << "op-border-box-grid--no-mobile" unless visible_on_mobile?(column) + + classes.compact.join(" ") + end + def column_args(_column) {} end diff --git a/app/components/op_primer/border_box_table_component.html.erb b/app/components/op_primer/border_box_table_component.html.erb index 2e150ac4870e..276661e4343c 100644 --- a/app/components/op_primer/border_box_table_component.html.erb +++ b/app/components/op_primer/border_box_table_component.html.erb @@ -34,13 +34,17 @@ See COPYRIGHT and LICENSE files for more details. test_selector:, )) do |component| component.with_header(classes: grid_class, color: :muted) do + concat render(Primer::Beta::Text.new(classes: "op-border-box-grid--mobile-heading", + font_weight: :semibold)) { mobile_title } headers.each do |name, args| - caption = args.delete(:caption) - concat render(Primer::Beta::Text.new(font_weight: :semibold, **header_args(name))) { caption } + concat render(Primer::Beta::Text.new(classes: header_classes(name), + font_weight: :semibold, + **header_args(name))) { args[:caption] } end if has_actions? - concat render(Primer::BaseComponent.new(tag: :div)) + concat render(Primer::BaseComponent.new(classes: heading_class, + tag: :div)) end end diff --git a/app/components/op_primer/border_box_table_component.rb b/app/components/op_primer/border_box_table_component.rb index b1cb1b9463e8..9aed4a5d22b0 100644 --- a/app/components/op_primer/border_box_table_component.rb +++ b/app/components/op_primer/border_box_table_component.rb @@ -32,10 +32,71 @@ module OpPrimer class BorderBoxTableComponent < TableComponent include ComponentHelpers + class << self + # Declares columns to be shown in the mobile table + # + # Use it in subclasses like so: + # + # columns :name, :description + # + # mobile_columns :name + # + # This results in the description columns to be hidden on mobile + def mobile_columns(*names) + return @mobile_columns || columns if names.empty? + + @mobile_columns = names.map(&:to_sym) + end + + # Declares which columns to be rendered with a label + # + # mobile_labels :name + # + # This results in the description columns to be hidden on mobile + def mobile_labels(*names) + return @mobile_labels if names.empty? + + @mobile_labels = names.map(&:to_sym) + end + + # Declare wide columns, that will result in a grid column span of 3 + # + # column_grid_span :title + # + def wide_columns(*names) + return Array(@wide_columns) if names.empty? + + @wide_columns = names.map(&:to_sym) + end + end + + delegate :mobile_columns, :mobile_labels, + to: :class + + def wide_column?(column) + self.class.wide_columns.include?(column) + end + def header_args(_column) {} end + def column_title(name) + header = headers.find { |h| h[0] == name } + header ? header[1][:caption] : nil + end + + def header_classes(column) + classes = [heading_class] + classes << "op-border-box-grid--wide-column" if wide_column?(column) + + classes.join(" ") + end + + def heading_class + "op-border-box-grid--heading" + end + # Default grid class with equal weights def grid_class "op-border-box-grid" @@ -57,6 +118,10 @@ def render_blank_slate end end + def mobile_title + raise ArgumentError, "Need to provide a mobile table title" + end + def blank_title I18n.t(:label_nothing_display) end diff --git a/app/components/op_primer/border_box_table_component.sass b/app/components/op_primer/border_box_table_component.sass index 254c212dffc7..785d88c3c70a 100644 --- a/app/components/op_primer/border_box_table_component.sass +++ b/app/components/op_primer/border_box_table_component.sass @@ -1,7 +1,39 @@ +@import "helpers" + .op-border-box-grid display: grid - justify-content: flex-start - align-items: center - // Distribute columns evenly by default - grid-auto-columns: minmax(0, 1fr) - grid-auto-flow: column + + &--row-action + align-items: center + justify-content: flex-end + +@media screen and (min-width: $breakpoint-md) + .op-border-box-grid + // Distribute columns evenly on desktop + grid-auto-columns: minmax(0, 1fr) + grid-auto-flow: column + justify-content: flex-start + align-items: center + + &--mobile-heading, + &--row-label + display: none + + &--wide-column + grid-column: span 3 + +@media screen and (max-width: $breakpoint-md) + .op-border-box-grid + grid-template-columns: 1fr auto + grid-auto-flow: row + + &--heading, + &--no-mobile + display: none + + &--row-item + grid-column: 1 + + &--row-action + grid-column: 2 + grid-row: 1 diff --git a/lookbook/docs/patterns/12-tables.md.erb b/lookbook/docs/patterns/12-tables.md.erb index fd1a2443aa67..f867915bc01a 100644 --- a/lookbook/docs/patterns/12-tables.md.erb +++ b/lookbook/docs/patterns/12-tables.md.erb @@ -33,6 +33,49 @@ end <%= embed OpPrimer::BorderBoxTableComponentPreview, :default, panels: %i[source] %> + + +### Border Box Table specific options + +#### Mobile behavior + +On mobile, the BorderBoxTable does not scroll, but collapse into two columns (content columns and actions). You can control which columns are shown using the `mobile_columns` method. + +As the columns are collapsed, some information might need an additional label, which you can prepend with `mobile_labels`. + +```ruby +# On desktop, show these columns +columns :title, :users, :created_at + +# On mobile, only two columns are visible +mobile_columns :title, :users + +# For users, show the label using the header caption +mobile_labels :users +``` + + + +#### Mobile headers + +On mobile, the usual table headers are replaced with a single `mobile_title` property that you have to set on the table. + +#### Wide columns + +To use wider columns on desktop, use the `wide_columns` helper: + +```ruby +# On desktop, show these columns +columns :title, :users, :created_at + +# Make title column wider than the others (factor 3 using grid span) +wide_columns :title +``` + + + + + ## Best practices **Do** diff --git a/lookbook/previews/op_primer/border_box_table_component_preview/custom_column_widths.html.erb b/lookbook/previews/op_primer/border_box_table_component_preview/custom_column_widths.html.erb index b4b732c3cac7..10cb8864e810 100644 --- a/lookbook/previews/op_primer/border_box_table_component_preview/custom_column_widths.html.erb +++ b/lookbook/previews/op_primer/border_box_table_component_preview/custom_column_widths.html.erb @@ -1,16 +1,15 @@ <%= table = Class.new(OpPrimer::BorderBoxTableComponent) do columns :foo, :bar + + wide_columns :foo + def self.name "MyTable" end - def header_args(column) - if column == :foo - { style: "grid-column: span 3" } - else - super - end + def mobile_title + "Mobile foo" end def row_class @@ -19,14 +18,6 @@ "MyRow" end - def column_args(column) - if column == :foo - { style: "grid-column: span 3" } - else - super - end - end - def foo "foo" end diff --git a/lookbook/previews/op_primer/border_box_table_component_preview/default.html.erb b/lookbook/previews/op_primer/border_box_table_component_preview/default.html.erb index 79a4d5a8d378..9d31891f044b 100644 --- a/lookbook/previews/op_primer/border_box_table_component_preview/default.html.erb +++ b/lookbook/previews/op_primer/border_box_table_component_preview/default.html.erb @@ -1,10 +1,17 @@ <%= table = Class.new(OpPrimer::BorderBoxTableComponent) do columns :foo, :bar + + mobile_labels :bar + def self.name "MyTable" end + def mobile_title + "Mobile foo" + end + ## This method is just a hack used for the preview ## Create your components under your namespace like so instead ## MyNamespace::TableComponent @@ -33,7 +40,7 @@ def foo concat render(Primer::Beta::Link.new(scheme: :primary, href: "#")) { "Some link" } - concat render(Primer::Beta::Text.new(tag: :p, font_size: :small, color: :subtle)) { "Hello there" } + render(Primer::Beta::Text.new(tag: :p, font_size: :small, color: :subtle)) { "Hello there" } end def bar diff --git a/lookbook/previews/op_primer/border_box_table_component_preview/with_action_menu.html.erb b/lookbook/previews/op_primer/border_box_table_component_preview/with_action_menu.html.erb index 68c0415a5c04..0190be02dca5 100644 --- a/lookbook/previews/op_primer/border_box_table_component_preview/with_action_menu.html.erb +++ b/lookbook/previews/op_primer/border_box_table_component_preview/with_action_menu.html.erb @@ -1,10 +1,15 @@ <%= table = Class.new(OpPrimer::BorderBoxTableComponent) do columns :foo, :bar + def self.name "MyTable" end + def mobile_title + "Mobile foo" + end + def row_class @clazz ||= Class.new(OpPrimer::BorderBoxRowComponent) do def self.name @@ -33,7 +38,8 @@ def foo concat render(Primer::Beta::Link.new(scheme: :primary, href: "#")) { "Some link" } - concat render(Primer::Beta::Text.new(tag: :p, font_size: :small, color: :subtle)) { "Hello there" } + + render(Primer::Beta::Text.new(tag: :p, font_size: :small, color: :subtle)) { "Hello there" } end def bar diff --git a/modules/auth_saml/app/components/saml/providers/row_component.rb b/modules/auth_saml/app/components/saml/providers/row_component.rb index 96c140e7d581..b198e02fd6c6 100644 --- a/modules/auth_saml/app/components/saml/providers/row_component.rb +++ b/modules/auth_saml/app/components/saml/providers/row_component.rb @@ -5,14 +5,6 @@ def provider model end - def column_args(column) - if column == :name - { style: "grid-column: span 3" } - else - super - end - end - def name concat render(Primer::Beta::Link.new( font_weight: :bold, @@ -20,23 +12,11 @@ def name )) { provider.display_name || provider.name } render_availability_label - render_idp_sso_service_url end def render_availability_label unless provider.available? - concat render(Primer::Beta::Label.new(ml: 2, scheme: :attention, size: :medium)) { t(:label_incomplete) } - end - end - - def render_idp_sso_service_url - if provider.idp_sso_service_url - concat render(Primer::Beta::Text.new( - tag: :p, - classes: "-break-word", - font_size: :small, - color: :subtle - )) { provider.idp_sso_service_url } + render(Primer::Beta::Label.new(ml: 2, scheme: :attention, size: :medium)) { t(:label_incomplete) } end end diff --git a/modules/auth_saml/app/components/saml/providers/table_component.rb b/modules/auth_saml/app/components/saml/providers/table_component.rb index fd9d403a57ea..a1134e62b348 100644 --- a/modules/auth_saml/app/components/saml/providers/table_component.rb +++ b/modules/auth_saml/app/components/saml/providers/table_component.rb @@ -3,18 +3,16 @@ module Providers class TableComponent < ::OpPrimer::BorderBoxTableComponent columns :name, :users, :creator, :created_at + mobile_columns :name, :users + + mobile_labels :users + + wide_columns :name + def initial_sort %i[id asc] end - def header_args(column) - if column == :name - { style: "grid-column: span 3" } - else - super - end - end - def sortable? false end @@ -23,6 +21,10 @@ def empty_row_message I18n.t "saml.providers.no_results_table" end + def mobile_title + I18n.t("saml.providers.plural") + end + def headers [ [:name, { caption: I18n.t("attributes.name") }], diff --git a/modules/meeting/app/components/meetings/row_component.rb b/modules/meeting/app/components/meetings/row_component.rb index 8878632c2d2e..0e5778dfbfe9 100644 --- a/modules/meeting/app/components/meetings/row_component.rb +++ b/modules/meeting/app/components/meetings/row_component.rb @@ -30,14 +30,6 @@ module Meetings class RowComponent < ::OpPrimer::BorderBoxRowComponent - def column_args(column) - if column == :title - { style: "grid-column: span 2" } - else - super - end - end - def project_name helpers.link_to_project model.project, {}, {}, false end diff --git a/modules/meeting/app/components/meetings/table_component.rb b/modules/meeting/app/components/meetings/table_component.rb index 0759ad0e5f63..8c97fac4fae5 100644 --- a/modules/meeting/app/components/meetings/table_component.rb +++ b/modules/meeting/app/components/meetings/table_component.rb @@ -32,7 +32,11 @@ module Meetings class TableComponent < ::OpPrimer::BorderBoxTableComponent options :current_project # used to determine if displaying the projects column - columns :title, :project_name, :start_time, :duration, :location + columns :title, :start_time, :project_name, :duration, :location + + mobile_columns :title, :start_time, :project_name + + mobile_labels :project_name def sortable? true @@ -46,19 +50,15 @@ def has_actions? true end - def header_args(column) - if column == :title - { style: "grid-column: span 2" } - else - super - end + def mobile_title + I18n.t(:label_meeting_plural) end def headers @headers ||= [ [:title, { caption: Meeting.human_attribute_name(:title) }], - current_project.blank? ? [:project_name, { caption: Meeting.human_attribute_name(:project) }] : nil, [:start_time, { caption: I18n.t(:label_meeting_date_and_time) }], + current_project.blank? ? [:project_name, { caption: Meeting.human_attribute_name(:project) }] : nil, [:duration, { caption: Meeting.human_attribute_name(:duration) }], [:location, { caption: Meeting.human_attribute_name(:location) }] ].compact diff --git a/modules/openid_connect/app/components/openid_connect/providers/row_component.rb b/modules/openid_connect/app/components/openid_connect/providers/row_component.rb index c7e87dc5c26a..4569dc476523 100644 --- a/modules/openid_connect/app/components/openid_connect/providers/row_component.rb +++ b/modules/openid_connect/app/components/openid_connect/providers/row_component.rb @@ -5,14 +5,6 @@ def provider model end - def column_args(column) - if column == :name - { style: "grid-column: span 3" } - else - super - end - end - def name concat(provider_name) unless provider.configured? diff --git a/modules/openid_connect/app/components/openid_connect/providers/table_component.rb b/modules/openid_connect/app/components/openid_connect/providers/table_component.rb index 9f23c1b974dc..c768461551f2 100644 --- a/modules/openid_connect/app/components/openid_connect/providers/table_component.rb +++ b/modules/openid_connect/app/components/openid_connect/providers/table_component.rb @@ -3,18 +3,16 @@ module Providers class TableComponent < ::OpPrimer::BorderBoxTableComponent columns :name, :type, :users, :creator, :created_at + wide_columns :name + + mobile_columns :name, :type, :users + + mobile_labels :users + def initial_sort %i[id asc] end - def header_args(column) - if column == :name - { style: "grid-column: span 3" } - else - super - end - end - def has_actions? false end @@ -27,6 +25,10 @@ def empty_row_message I18n.t "openid_connect.providers.no_results_table" end + def mobile_title + I18n.t("openid_connect.providers.plural") + end + def headers [ [:name, { caption: I18n.t("attributes.name") }],