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 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..3390b5345 --- /dev/null +++ b/app/assets/stylesheets/components/_table.scss @@ -0,0 +1,206 @@ +@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"); +$vertical-row-bottom-border-width: 3px; +$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 */ + +.app-c-table { + .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/helpers/components/table_helper.rb b/app/helpers/components/table_helper.rb new file mode 100644 index 000000000..982601420 --- /dev/null +++ b/app/helpers/components/table_helper.rb @@ -0,0 +1,75 @@ +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] + classes << opt[:classes] if opt[:classes] + + 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", role: "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", role: "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", role: "columnheader" + end + + 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 || block_given? + str ||= "Not set" + + if block_given? + tag.td class: classes, role: "cell", &block + else + tag.td str, class: classes, role: "cell" + end + end + end + end +end 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 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/app/views/components/_table.html.erb b/app/views/components/_table.html.erb new file mode 100644 index 000000000..5ec626729 --- /dev/null +++ b/app/views/components/_table.html.erb @@ -0,0 +1,88 @@ +<% + caption ||= nil + head ||= [] + rows ||= [] + first_cell_is_header ||= false + 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 %> + <%= Components::TableHelper.helper(self, caption, { + sortable: sortable, + filterable: filterable, + caption_classes: caption_classes, + table_id: table_id, + classes: classes, + }) 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] + } %> + <% 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] + } %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> + <% end %> +<% end %> + +<% if filterable %> +">
+