From 155639d71f2144cf0d4768ecc278ca81d0938d1a Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Thu, 31 Aug 2023 15:52:24 +0100 Subject: [PATCH 1/9] Rename helper I assume that this name was helpful at the time it was chosen but I'm not sure it is anymore. "Component" means a lot of different things and right now within the Signon codebase I think that we're converging on it referring to GOV.UK Publishing Components and its ecosystem --- app/helpers/{component_helper.rb => navigation_items_helper.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/helpers/{component_helper.rb => navigation_items_helper.rb} (97%) diff --git a/app/helpers/component_helper.rb b/app/helpers/navigation_items_helper.rb similarity index 97% rename from app/helpers/component_helper.rb rename to app/helpers/navigation_items_helper.rb index a95e26bd8..90745ab91 100644 --- a/app/helpers/component_helper.rb +++ b/app/helpers/navigation_items_helper.rb @@ -1,4 +1,4 @@ -module ComponentHelper +module NavigationItemsHelper def navigation_items return [] unless current_user From b85e1465d701461300753cc810531500442ea0ce Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Mon, 4 Sep 2023 11:53:05 +0100 Subject: [PATCH 2/9] Copy Table component from gem And make some small changes to turn it into an app-based component. We're trying to make our tables more usable on small screens. Since this goal doesn't appear to contradict the existing Table component's goals, we'd like to merge those improvements into the gem. But first of all, we need to develop and test the changes, so we're planning to: 1. ~~duplicate the gem's version of the component~~ 2. make the changes locally 3. test them in our app, probably in production 4. open a pull request to merge the changes into the gem 5. switch the app back to using the gem's version It's unclear what the cleanest strategy is to achieve this copying and modifying. For instance, I've included CSS and JavaScript here although we might have got away with just depending on the gem's versions? And a hundred other dilemmas. Changes I've already made to the copied files (to make the app not crash and to turn this into an app-based component): - change helper name and namespacing, and make class a module -- the result is an unpleasant compromise between the gem's approach and how I'd expect the app to do this if starting from scratch - remove the `add_gem_component_stylesheet` line from the partial and don't replace it with `add_app_component_stylesheet` -- I'm not sure what the app one is meant to do, because it didn't appear to do what adding `@import "components/*";` to application.scss did - rename 'gem-c-table' and related CSS classes and JavaScript selectors to 'app-c-table', etc - remove inline warning comment from helper -- although part of me wants to add warning comments to all of these files that I've copied - add 1 Rubocop and 1 yarn stylelint ignore comment -- it's existing code and we want to keep it as close as possible to the original --- app/assets/javascripts/components/table.js | 52 ++++++ app/assets/stylesheets/components/_table.scss | 129 +++++++++++++ app/helpers/components/table_helper.rb | 69 +++++++ app/views/components/_table.html.erb | 78 ++++++++ app/views/components/docs/table.yml | 171 ++++++++++++++++++ 5 files changed, 499 insertions(+) create mode 100644 app/assets/javascripts/components/table.js create mode 100644 app/assets/stylesheets/components/_table.scss create mode 100644 app/helpers/components/table_helper.rb create mode 100644 app/views/components/_table.html.erb create mode 100644 app/views/components/docs/table.yml diff --git a/app/assets/javascripts/components/table.js b/app/assets/javascripts/components/table.js new file mode 100644 index 000000000..720eb3f2c --- /dev/null +++ b/app/assets/javascripts/components/table.js @@ -0,0 +1,52 @@ +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + function Table ($module) { + this.$module = $module + this.searchInput = $module.querySelector('input[name="filter"]') + this.tableRows = $module.querySelectorAll('.js-govuk-table__row') + this.filter = $module.querySelector('.js-app-c-table__filter') + this.filterCount = this.filter.querySelector('.js-filter-count') + this.message = $module.querySelector('.js-app-c-table__message') + this.hiddenClass = 'govuk-!-display-none' + this.filterCountText = this.filterCount.getAttribute('data-count-text') + this.tableRowsContent = [] + + for (var i = 0; i < this.tableRows.length; i++) { + this.tableRowsContent.push(this.tableRows[i].textContent.toUpperCase()) + } + } + + Table.prototype.init = function () { + this.$module.updateRows = this.updateRows.bind(this) + this.filter.classList.remove(this.hiddenClass) + this.searchInput.addEventListener('input', this.$module.updateRows) + } + + // Reads value of input and filters content + Table.prototype.updateRows = function () { + var value = this.searchInput.value + var hiddenRows = 0 + var length = this.tableRows.length + + for (var i = 0; i < length; i++) { + if (this.tableRowsContent[i].includes(value.toUpperCase())) { + this.tableRows[i].classList.remove(this.hiddenClass) + } else { + this.tableRows[i].classList.add(this.hiddenClass) + hiddenRows++ + } + } + + this.filterCount.textContent = (length - hiddenRows) + ' ' + this.filterCountText + + if (length === hiddenRows) { + this.message.classList.remove(this.hiddenClass) + } else { + this.message.classList.add(this.hiddenClass) + } + } + + Modules.Table = Table +})(window.GOVUK.Modules) diff --git a/app/assets/stylesheets/components/_table.scss b/app/assets/stylesheets/components/_table.scss new file mode 100644 index 000000000..a0c7fcd12 --- /dev/null +++ b/app/assets/stylesheets/components/_table.scss @@ -0,0 +1,129 @@ +@import "govuk_publishing_components/individual_component_support"; +@import "govuk/components/table/table"; + +$table-border-width: 1px; +$table-border-colour: govuk-colour("mid-grey", $legacy: "grey-2"); +$table-header-border-width: 2px; +$table-header-background-colour: govuk-colour("light-grey", $legacy: "grey-3"); +$sort-link-active-colour: govuk-colour("white"); +$sort-link-arrow-size: 14px; +$sort-link-arrow-size-small: 8px; +$sort-link-arrow-spacing: $sort-link-arrow-size / 2; +$table-row-hover-background-colour: rgba(43, 140, 196, .2); +$table-row-even-background-colour: govuk-colour("light-grey", $legacy: "grey-4"); + +/* stylelint-disable */ +.govuk-table__cell:empty, +.govuk-table__cell--empty { + color: $govuk-secondary-text-colour; +} + +.govuk-table--sortable { + outline: $table-border-width solid $table-border-colour; + outline-offset: 0; + + .govuk-table__header { + padding: govuk-spacing(2); + border-right: $table-header-border-width solid govuk-colour("white"); + border-bottom: $table-header-border-width solid govuk-colour("white"); + background: $table-header-background-colour; + font-weight: normal; + + &:last-child { + border-right: 0; + } + + .app-table__sort-link { + @include govuk-link-style-no-visited-state; + position: relative; + padding-right: $sort-link-arrow-size; + color: $govuk-link-colour; + text-decoration: none; + } + + .app-table__sort-link:focus { + @include govuk-focused-text; + } + + .app-table__sort-link:after { + content: ""; + position: absolute; + top: 5px; + right: 0; + @include govuk-shape-arrow($direction: up, $base: $sort-link-arrow-size-small, $display: block); + } + + .app-table__sort-link:before { + content: ""; + position: absolute; + top: 13px; + right: 0; + @include govuk-shape-arrow($direction: down, $base: $sort-link-arrow-size-small, $display: block); + } + } + + .govuk-table__header--active { + color: $sort-link-active-colour; + background: $govuk-link-colour; + + .app-table__sort-link { + padding-right: govuk-spacing(4); + + &:link, + &:visited, + &:hover, + &:active { + color: $sort-link-active-colour; + } + + &:focus { + color: $govuk-focus-text-colour; + } + } + + .app-table__sort-link--ascending:before, + .app-table__sort-link--descending:before { + content: none; + } + + .app-table__sort-link--ascending:after { + content: ""; + position: absolute; + top: $sort-link-arrow-spacing; + right: 0; + margin-left: govuk-spacing(1); + + @include govuk-shape-arrow($direction: up, $base: $sort-link-arrow-size, $display: inline-block); + } + + .app-table__sort-link--descending:after { + content: ""; + position: absolute; + top: $sort-link-arrow-spacing; + right: 0; + margin-left: govuk-spacing(1); + + @include govuk-shape-arrow($direction: down, $base: $sort-link-arrow-size, $display: inline-block); + } + } + + .govuk-table__row { + &:hover { + background-color: $table-row-hover-background-colour; + } + + &:nth-child(even) { + background-color: $table-row-even-background-colour; + + &:hover { + background-color: $table-row-hover-background-colour; + } + } + } + + .govuk-table__cell { + padding: govuk-spacing(2); + border: 0; + } +} +/* stylelint-enable */ diff --git a/app/helpers/components/table_helper.rb b/app/helpers/components/table_helper.rb new file mode 100644 index 000000000..ee55d9dd4 --- /dev/null +++ b/app/helpers/components/table_helper.rb @@ -0,0 +1,69 @@ +module Components + module TableHelper + def self.helper(context, caption = nil, opt = {}) + builder = TableBuilder.new(context.tag) + + classes = %w[app-c-table govuk-table] + classes << "govuk-table--sortable" if opt[:sortable] + + caption_classes = %w[govuk-table__caption] + caption_classes << opt[:caption_classes] if opt[:caption_classes] + + context.tag.table class: classes, id: opt[:table_id] do + context.concat context.tag.caption caption, class: caption_classes + yield(builder) + end + end + + class TableBuilder + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + + attr_reader :tag + + def initialize(tag) + # rubocop:disable Rails/HelperInstanceVariable + @tag = tag + # rubocop:enable Rails/HelperInstanceVariable + end + + def head + tag.thead class: "govuk-table__head" do + tag.tr class: "govuk-table__row" do + yield(self) + end + end + end + + def body + tag.tbody class: "govuk-table__body" do + yield(self) + end + end + + def row + tag.tr class: "govuk-table__row js-govuk-table__row" do + yield(self) + end + end + + def header(str, opt = {}) + classes = %w[govuk-table__header] + classes << "govuk-table__header--#{opt[:format]}" if opt[:format] + classes << "govuk-table__header--active" if opt[:sort_direction] + link_classes = %w[app-table__sort-link] + link_classes << "app-table__sort-link--#{opt[:sort_direction]}" if opt[:sort_direction] + str = link_to str, opt[:href], class: link_classes, data: opt[:data_attributes] if opt[:href] + tag.th str, class: classes, scope: opt[:scope] || "col" + end + + def cell(str, opt = {}) + classes = %w[govuk-table__cell] + classes << "govuk-table__cell--#{opt[:format]}" if opt[:format] + classes << "govuk-table__cell--empty" unless str + str ||= "Not set" + tag.td str, class: classes + end + end + end +end diff --git a/app/views/components/_table.html.erb b/app/views/components/_table.html.erb new file mode 100644 index 000000000..ba355b7fa --- /dev/null +++ b/app/views/components/_table.html.erb @@ -0,0 +1,78 @@ +<% + caption ||= nil + head ||= [] + rows ||= [] + first_cell_is_header ||= false + caption_classes ||= nil + sortable ||= false + filterable ||= false + label ||= t("components.table.filter_label") + + table_id = "table-id-#{SecureRandom.hex(4)}" + filter_count_id = "filter-count-id-#{SecureRandom.hex(4)}" +%> + +<% @table = capture do %> + <%= Components::TableHelper.helper(self, caption, { + sortable: sortable, + filterable: filterable, + caption_classes: caption_classes, + table_id: table_id + }) do |t| %> + + <% if head.any? %> + <%= t.head do %> + <% head.each_with_index do |item, cellindex| %> + <%= t.header item[:text], { + format: item[:format], + href: item[:href], + data_attributes: item[:data_attributes], + sort_direction: item[:sort_direction] + } %> + <% end %> + <% end %> + <% end %> + + <%= t.body do %> + <% rows.each_with_index do |row, rowindex| %> + <%= t.row do %> + <% row.each_with_index do |cell, cellindex| %> + <% if cellindex == 0 && first_cell_is_header %> + <%= t.header cell[:text], { + scope: "row", + format: cell[:format] + } %> + <% else %> + <%= t.cell cell[:text], { + format: cell[:format] + } %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> + +<% if filterable %> +
+
+ <%= render "govuk_publishing_components/components/input", { + label: { + text: label + }, + name: "filter", + controls: table_id, + aria_described_by: filter_count_id, + } %> + +

">

+
+ + <%= @table %> + +

<%= t("components.table.filter_message") %>

+
+<% else %> + <%= @table %> +<% end %> diff --git a/app/views/components/docs/table.yml b/app/views/components/docs/table.yml new file mode 100644 index 000000000..9c06f5f89 --- /dev/null +++ b/app/views/components/docs/table.yml @@ -0,0 +1,171 @@ +name: Table +description: A table component to make information easier to compare and scan for users. +accessibility_criteria: | + Accessible tables need HTML markup that indicates header cells and data cells and defines their relationship. Assistive technologies use this information to provide context to users. + Header cells must be marked up with ``, and data cells with `` to make tables accessible. + For more complex tables, explicit associations is needed using scope attributes. +shared_accessibility_criteria: + - link +type: helper +examples: + default: + data: + rows: + - + - text: January + - text: £85 + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £165 + format: numeric + - text: £125 + format: numeric + with_head: + data: + head: + - text: Month you apply + - text: Rate for bicycles + format: numeric + - text: Rate for vehicles + format: numeric + rows: + - + - text: January + - text: £85 + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £165 + format: numeric + - text: £125 + format: numeric + with_head_and_caption: + data: + caption: 'Caption 1: Months and rates' + caption_classes: govuk-heading-m + first_cell_is_header: true + head: + - text: Month you apply + - text: Rate for bicycles + format: numeric + - text: Rate for vehicles + format: numeric + rows: + - + - text: January + - text: £85 + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £165 + format: numeric + - text: £125 + format: numeric + with_sortable_head: + description: | + This option allows links to be added to the table headers for sorting. Sorting must be handled server side, it is not done by the component. + + The example shown applies a tracking attribute specifically for use by Google Tag Manager in [Content Publisher](https://github.com/alphagov/content-publisher). + + Other data attributes can also be applied as required. Note that the component does not include built in tracking. If this is required consider using the [track click script](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/analytics/track-click.md). + data: + sortable: true + head: + - text: Month you apply + - text: Rate for bicycles + format: numeric + sort_direction: descending + href: /?sort_direction=desc + data_attributes: + tracking: "UTM-123A" + - text: Rate for vehicles + format: numeric + href: /?sort_direction=desc + data_attributes: + tracking: "UTM-123B" + rows: + - + - text: January + - text: + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £125 + format: numeric + - text: £60 + format: numeric + - + - text: April + - text: £135 + format: numeric + - text: £62 + format: numeric + - + - text: May + - text: £150 + format: numeric + - text: £80 + format: numeric + with_filter: + description: This option allows table rows to be filtered by user input. Since this filtering is implemented client-side the filter section is not displayed by default but displays only when JavaScript is enabled. The label for the input field can be set when the coponent is rendered via the `label` key. if this is not set a fallback value will display. + data: + filterable: true + label: Filter months + head: + - text: Month you apply + - text: Rate for bicycles + format: numeric + - text: Rate for vehicles + format: numeric + rows: + - + - text: January + - text: £85 + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £165 + format: numeric + - text: £125 + format: numeric From eab1d9ac592ad8cb835a390975c4c044ab7ff2dc Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Mon, 4 Sep 2023 16:05:34 +0100 Subject: [PATCH 3/9] Load locally-defined components' JavaScript --- app/assets/javascripts/application.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 325bdc298..5bd8106c0 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -2,3 +2,6 @@ //= require govuk_publishing_components/all_components //= require_tree ./modules //= require rails-ujs + +// Components from this application +//= require_tree ./components From b21733bbe4d77eaf35d56809d6fb130bde351450 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 1 Sep 2023 17:08:12 +0100 Subject: [PATCH 4/9] Allow table helper #cell to take a block Instead of a text argument, similar to how a lot of view helpers work. But I'm going to avoid offering any flexibility in the order of arguments (that's a common view helper thing when there's a choice between text argument or block, right?). We'll just ignore the value supplied for that first argument if there is a block --- app/helpers/components/table_helper.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/helpers/components/table_helper.rb b/app/helpers/components/table_helper.rb index ee55d9dd4..f3af21ead 100644 --- a/app/helpers/components/table_helper.rb +++ b/app/helpers/components/table_helper.rb @@ -57,12 +57,17 @@ def header(str, opt = {}) tag.th str, class: classes, scope: opt[:scope] || "col" end - def cell(str, opt = {}) + def cell(str, opt = {}, &block) classes = %w[govuk-table__cell] classes << "govuk-table__cell--#{opt[:format]}" if opt[:format] - classes << "govuk-table__cell--empty" unless str + classes << "govuk-table__cell--empty" unless str || block_given? str ||= "Not set" - tag.td str, class: classes + + if block_given? + tag.td class: classes, &block + else + tag.td str, class: classes + end end end end From 4f09fbc76314b2e6a6a129c717a2fa6d543a8f6c Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Tue, 5 Sep 2023 13:44:58 +0100 Subject: [PATCH 5/9] Allow TableHelper to take arbitrary custom classes --- app/helpers/components/table_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/components/table_helper.rb b/app/helpers/components/table_helper.rb index f3af21ead..f64a8f152 100644 --- a/app/helpers/components/table_helper.rb +++ b/app/helpers/components/table_helper.rb @@ -5,6 +5,7 @@ def self.helper(context, caption = nil, opt = {}) classes = %w[app-c-table govuk-table] classes << "govuk-table--sortable" if opt[:sortable] + classes << opt[:classes] if opt[:classes] caption_classes = %w[govuk-table__caption] caption_classes << opt[:caption_classes] if opt[:caption_classes] From 3d1981f8fcd16525f310b943fba81fbad5fc1834 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Mon, 4 Sep 2023 16:32:35 +0100 Subject: [PATCH 6/9] Declare role attribute for s, s and s To improve usability, we're going to introduce an alternate table layout for small screens and in the process we're going to change the display styling for s and s. According [Adam Fenwick's write up of his Responsive Accessible Table](https://www.afenwick.com/blog/2021/responsive-accessible-table/), this "means a screen reader is no longer aware these are still table elements" but with these HTML roles in place, "no matter what we do to these elements, a screen reader always recognises them as their default". --- app/helpers/components/table_helper.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/helpers/components/table_helper.rb b/app/helpers/components/table_helper.rb index f64a8f152..982601420 100644 --- a/app/helpers/components/table_helper.rb +++ b/app/helpers/components/table_helper.rb @@ -30,7 +30,7 @@ def initialize(tag) def head tag.thead class: "govuk-table__head" do - tag.tr class: "govuk-table__row" do + tag.tr class: "govuk-table__row", role: "row" do yield(self) end end @@ -43,7 +43,7 @@ def body end def row - tag.tr class: "govuk-table__row js-govuk-table__row" do + tag.tr class: "govuk-table__row js-govuk-table__row", role: "row" do yield(self) end end @@ -55,7 +55,7 @@ def header(str, opt = {}) link_classes = %w[app-table__sort-link] link_classes << "app-table__sort-link--#{opt[:sort_direction]}" if opt[:sort_direction] str = link_to str, opt[:href], class: link_classes, data: opt[:data_attributes] if opt[:href] - tag.th str, class: classes, scope: opt[:scope] || "col" + tag.th str, class: classes, scope: opt[:scope] || "col", role: "columnheader" end def cell(str, opt = {}, &block) @@ -65,9 +65,9 @@ def cell(str, opt = {}, &block) str ||= "Not set" if block_given? - tag.td class: classes, &block + tag.td class: classes, role: "cell", &block else - tag.td str, class: classes + tag.td str, class: classes, role: "cell" end end end From 7150f8e8b951c93e8614f218e28bb1b2309d52e2 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Tue, 5 Sep 2023 11:43:14 +0100 Subject: [PATCH 7/9] Force long table cell text to break The resulting line-broken text isn't necessarily pretty, but this does solve the problem on the API Users page where long email addresses can result in the being wider than its container (div.govuk-width-container). I'm not sure how widely useful this is and therefore how to handle this when we're ready to merge our other changes back into the components gem. --- app/assets/stylesheets/components/_table.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/components/_table.scss b/app/assets/stylesheets/components/_table.scss index a0c7fcd12..bf0410417 100644 --- a/app/assets/stylesheets/components/_table.scss +++ b/app/assets/stylesheets/components/_table.scss @@ -127,3 +127,9 @@ $table-row-even-background-colour: govuk-colour("light-grey", $legacy: "grey-4") } } /* stylelint-enable */ + +.app-c-table { + tbody tr td { + word-break: break-word; + } +} From 2f7ec31c4f436a54bb98525c7c4a3c46fe4c8840 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Tue, 5 Sep 2023 11:56:43 +0100 Subject: [PATCH 8/9] `vertical_on_small_screen` Table component option Borrowing liberally from [Adam Fenwick's Responsive Accessible Table example](https://www.afenwick.com/blog/2021/responsive-accessible-table/). On tablet-sized screens and smaller, it switches the layout of the table into a vertical list of cards, which we believe is more usable for all. I've strayed from the example in the choice of screen size, because the specific case that I was using to test this (API Users index page) seems usable as a regular table on screens a fair bit smaller than "desktop". If that's not true for other tables in the app, we can easily change the hard-coded media queries or even supply the breakpoint as an argument to the component. I've essentially appended the new styling to the end of the existing component CSS because those existing styles apply more generally to all govuk-tables (i.e. they didn't specify the app-c-table class or prefix). I think it'll make more sense to merge the styles together when we're ready to merge our changes into the gem. --- app/assets/stylesheets/components/_table.scss | 73 ++++++++++++++++++- app/views/components/_table.html.erb | 12 ++- app/views/components/docs/table.yml | 29 ++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/components/_table.scss b/app/assets/stylesheets/components/_table.scss index bf0410417..3390b5345 100644 --- a/app/assets/stylesheets/components/_table.scss +++ b/app/assets/stylesheets/components/_table.scss @@ -5,6 +5,7 @@ $table-border-width: 1px; $table-border-colour: govuk-colour("mid-grey", $legacy: "grey-2"); $table-header-border-width: 2px; $table-header-background-colour: govuk-colour("light-grey", $legacy: "grey-3"); +$vertical-row-bottom-border-width: 3px; $sort-link-active-colour: govuk-colour("white"); $sort-link-arrow-size: 14px; $sort-link-arrow-size-small: 8px; @@ -129,7 +130,77 @@ $table-row-even-background-colour: govuk-colour("light-grey", $legacy: "grey-4") /* stylelint-enable */ .app-c-table { - tbody tr td { + .govuk-table__cell { word-break: break-word; } } + +.app-c-table--vertical { + .govuk-table__head { + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + width: 1px; + } + + .govuk-table__body .govuk-table__row { + display: block; + } + + .govuk-table__cell { + display: flex; + justify-content: space-between; + min-width: 1px; + text-align: right; + } + + @include govuk-media-query($until: tablet) { + .govuk-table__cell { + padding-right: 0; + } + + .govuk-table__cell:last-child { + border-bottom: 0 + } + + .govuk-table__body .govuk-table__row { + border-bottom: $vertical-row-bottom-border-width solid $table-border-colour; + } + } + + .app-c-table__duplicate-heading { + font-weight: 700; + padding-right: 1em; + text-align: left; + word-break: initial; + } + + @include govuk-media-query($from: tablet) { + .govuk-table__head { + clip: auto; + -webkit-clip-path: none; + clip-path: none; + display: table-header-group; + height: auto; + overflow: auto; + position: relative; + width: auto; + } + + .govuk-table__body .govuk-table__row { + display: table-row; + } + + .govuk-table__cell { + display: table-cell; + text-align: left; + } + + .app-c-table__duplicate-heading { + display: none; + } + } +} diff --git a/app/views/components/_table.html.erb b/app/views/components/_table.html.erb index ba355b7fa..5ec626729 100644 --- a/app/views/components/_table.html.erb +++ b/app/views/components/_table.html.erb @@ -6,10 +6,12 @@ caption_classes ||= nil sortable ||= false filterable ||= false + vertical_on_small_screen ||= false label ||= t("components.table.filter_label") table_id = "table-id-#{SecureRandom.hex(4)}" filter_count_id = "filter-count-id-#{SecureRandom.hex(4)}" + classes = "app-c-table--vertical" if vertical_on_small_screen %> <% @table = capture do %> @@ -17,7 +19,8 @@ sortable: sortable, filterable: filterable, caption_classes: caption_classes, - table_id: table_id + table_id: table_id, + classes: classes, }) do |t| %> <% if head.any? %> @@ -42,6 +45,13 @@ scope: "row", format: cell[:format] } %> + <% elsif vertical_on_small_screen && head.any? %> + <%= t.cell nil, { format: cell[:format] } do %> + + <%= cell[:text] %> + <% end %> <% else %> <%= t.cell cell[:text], { format: cell[:format] diff --git a/app/views/components/docs/table.yml b/app/views/components/docs/table.yml index 9c06f5f89..19e87012a 100644 --- a/app/views/components/docs/table.yml +++ b/app/views/components/docs/table.yml @@ -169,3 +169,32 @@ examples: format: numeric - text: £125 format: numeric + vertical_layout_on_small_screens: + description: This option only kicks-in on tablet-sized screens and smaller. It switches the layout of the table into a vertical list of cards. In this new layout, the table's main header is hidden but a copy of the relevant heading text is embedded into each table cell. + data: + vertical_on_small_screen: true + head: + - text: Month you apply + - text: Rate for bicycles + format: numeric + - text: Rate for vehicles + format: numeric + rows: + - + - text: January + - text: £85 + format: numeric + - text: £95 + format: numeric + - + - text: February + - text: £75 + format: numeric + - text: £55 + format: numeric + - + - text: March + - text: £165 + format: numeric + - text: £125 + format: numeric From f0a8f53f820aeb7542e217fc468038f29bdd8e03 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Mon, 4 Sep 2023 16:06:01 +0100 Subject: [PATCH 9/9] Set API Users table to `vertical_on_small_screen` Using the app's new version of the Table component, this lays the table's contents out vertically on screens tablet-sized and below. In the related controller test, I've switched to using a regexp instead of a string to match the contents of the table cells. The implementation of `vertical_on_small_screen` embeds some markup in each table cell, which would break these strict matchers. --- app/views/api_users/index.html.erb | 3 ++- test/controllers/api_users_controller_test.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/api_users/index.html.erb b/app/views/api_users/index.html.erb index 8f9ec32f5..32fc2851c 100644 --- a/app/views/api_users/index.html.erb +++ b/app/views/api_users/index.html.erb @@ -7,10 +7,11 @@ } %> -<%= render "govuk_publishing_components/components/table", { +<%= render "components/table", { caption: "API users", caption_classes: "govuk-visually-hidden", filterable: true, + vertical_on_small_screen: true, label: "Filter API users", head: [ { diff --git a/test/controllers/api_users_controller_test.rb b/test/controllers/api_users_controller_test.rb index 533bc4585..df6819e71 100644 --- a/test/controllers/api_users_controller_test.rb +++ b/test/controllers/api_users_controller_test.rb @@ -30,13 +30,13 @@ class ApiUsersControllerTest < ActionController::TestCase should "list api users" do create(:api_user, email: "api_user@email.com") get :index - assert_select "td", "api_user@email.com" + assert_select "td", /api_user@email.com/ end should "not list web users" do create(:user, email: "web_user@email.com") get :index - assert_select "td", count: 0, text: "web_user@email.com" + assert_select "td", count: 0, text: /web_user@email.com/ end end