Skip to content

Commit

Permalink
Merge pull request #3445 from alphagov/filters-final
Browse files Browse the repository at this point in the history
Add a `filter_panel` component for new UI
  • Loading branch information
csutter authored Sep 12, 2024
2 parents dc2c40d + 5b81556 commit d60df01
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 0 deletions.
38 changes: 38 additions & 0 deletions app/assets/javascripts/components/filter-panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
'use strict'

class FilterPanel {
constructor ($module) {
this.$module = $module
this.$button = this.$module.querySelector('.app-c-filter-panel__button')
this.$content = this.$module.querySelector('.app-c-filter-panel__content')
}

init () {
if (this.$module.getAttribute('open')) {
this.$button.setAttribute('aria-expanded', 'true')
} else {
this.$button.setAttribute('aria-expanded', 'false')
this.$content.setAttribute('hidden', '')
}

this.$button.addEventListener('click', this.onButtonClick.bind(this))
}

onButtonClick (event) {
event.preventDefault()
this.toggle()
}

toggle () {
const newState = this.$button.getAttribute('aria-expanded') !== 'true'
this.$button.setAttribute('aria-expanded', newState)
this.$content.toggleAttribute('hidden')
}
}

Modules.FilterPanel = FilterPanel
})(window.GOVUK.Modules)
55 changes: 55 additions & 0 deletions app/assets/stylesheets/components/_filter-panel.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@import "govuk_publishing_components/individual_component_support";

.app-c-filter-panel__content {
background-color: govuk-colour("light-grey");
padding: govuk-spacing(3);
margin-top: govuk-spacing(2);
}

.app-c-filter-panel__header {
display: flex;
flex-wrap: wrap;
place-content: space-between;
align-items: center;
gap: govuk-spacing(2);

.gem-c-heading {
font-weight: normal;
}

.app-c-filter-panel__button {
background-color: transparent;
color: $govuk-link-colour;
text-decoration: none;
border-style: none;
@include govuk-font(19);

&:focus,
&:focus-visible {
background-color: $govuk-focus-colour;
@include govuk-link-hover-decoration;
@include govuk-focused-text;
}

&:hover {
cursor: pointer;
color: $govuk-link-hover-colour;
}
}

.app-c-filter-panel__button[aria-expanded="true"] .app-c-filter-panel__button-icon {
transform: translateY(0) rotate(-135deg);
}

.app-c-filter-panel__button-icon {
border: solid currentcolor;
border-width: 0 2px 2px 0;
height: 0.5rem;
pointer-events: none;
transform: translateY(-50%) rotate(45deg);
width: 0.5rem;
display: inline-block;
vertical-align: baseline;
margin-right: govuk-spacing(1);
}
}
43 changes: 43 additions & 0 deletions app/views/components/_filter_panel.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<% add_app_component_stylesheet("filter-panel") %>
<%
raise ArgumentError, "button_text is required" unless local_assigns[:button_text]
raise ArgumentError, "result_count is required" unless local_assigns[:result_count]

id_suffix = SecureRandom.hex(4)
panel_content_id = "filter-panel-#{id_suffix}"
button_id = "filter-button-#{id_suffix}"

component_helper = GovukPublishingComponents::Presenters::ComponentWrapperHelper.new(local_assigns)
shared_helper = GovukPublishingComponents::Presenters::SharedHelper.new(local_assigns)
component_helper.add_data_attribute({ module: "filter-panel" })
component_helper.add_class("app-c-filter-panel")
component_helper.add_class(shared_helper.get_margin_bottom) if local_assigns[:margin_bottom]
%>

<%= tag.div(**component_helper.all_attributes) do %>
<div class="app-c-filter-panel__header">
<%= tag.button(
id: button_id,
class: "app-c-filter-panel__button govuk-link",
aria: { expanded: "false", controls: panel_content_id }
) do %>
<span class="app-c-filter-panel__button-icon"></span>
<%= button_text %>
<% end %>

<%= render "govuk_publishing_components/components/heading", {
text: pluralize(number_with_delimiter(result_count), "result"),
heading_level: 2,
font_size: "s",
} %>
</div>

<%= tag.div(
class: "app-c-filter-panel__content",
id: panel_content_id,
role: "region",
aria: { labelledby: button_id },
) do %>
<%= yield %>
<% end %>
<% end %>
42 changes: 42 additions & 0 deletions app/views/components/docs/filter_panel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Filter panel
description: Displays a result count and a toggleable panel of filters and sort options.
uses_component_wrapper_helper: true
accessibility_criteria: |
The component must:
- accept focus
- be focusable with a keyboard
- be usable with a keyboard
- be usable with touch
- indicate when it has focus
- toggle the visibility of the panel when interacted with
- indicate the expanded state when panel is visible
- indicate the collapsed state when panel is hidden
- be visible by default without Javascript enabled
examples:
default:
data:
result_count: 123456
button_text: Filter and sort
block: |
<p class="govuk-body">
I can contain arbitrary content, usually a set of filters and sort options.
</p>
open:
data:
result_count: 1989
button_text: Open sesame
open: true
block: |
<p class="govuk-body">
I am open by default!
</p>
with_margin_bottom:
description: |
Allows the spacing at the bottom of the component to be adjusted.
This accepts a number from `0` to `9` (`0px` to `60px`) using the [GOV.UK Frontend spacing scale](https://design-system.service.gov.uk/styles/spacing/#the-responsive-spacing-scale). It defaults to having no margin bottom.
data:
result_count: 1
button_text: Loooooads of space
margin_bottom: 9
8 changes: 8 additions & 0 deletions app/views/finders/show_all_content_finder.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@
<%= render 'spelling_suggestion' %>
</div>

<%= render "components/filter_panel", {
button_text: "Filter and sort",
result_count: result_set_presenter.total_count,
margin_bottom: 3,
} do %>
TODO: Add filter panel content here
<% end %>

<% if result_set_presenter.total_count.positive? %>
<%= render "govuk_publishing_components/components/document_list", {
disable_ga4: true,
Expand Down
1 change: 1 addition & 0 deletions config/initializers/dartsass.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
APP_STYLESHEETS = {
"application.scss" => "application.css",
"components/_expander.scss" => "components/_expander.css",
"components/_filter-panel.scss" => "components/_filter-panel.css",
"components/_mobile-filters.scss" => "components/_mobile-filters.css",
"views/_search.scss" => "views/_search.css",
}.freeze
Expand Down
67 changes: 67 additions & 0 deletions spec/components/filter_panel_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require "spec_helper"

describe "Filter panel component", type: :view do
def component_name
"filter_panel"
end

def render_component(locals, &block)
if block_given?
render("components/#{component_name}", locals, &block)
else
render "components/#{component_name}", locals
end
end

it "raises an error if button_text option is omitted" do
expect { render_component(result_count: 42) }.to raise_error(/button_text/)
end

it "raises an error if result_count option is omitted" do
expect { render_component(button_text: "Oops") }.to raise_error(/result_count/)
end

it "renders the button with the correct text" do
render_component(button_text: "Filtern und sortieren", result_count: 42)

assert_select ".app-c-filter-panel button", text: "Filtern und sortieren"
end

it "renders the correct result count heading for a single result" do
render_component(button_text: "Filter", result_count: 1)

assert_select ".app-c-filter-panel h2", text: "1 result"
end

it "renders the correct result count heading for a small number" do
render_component(button_text: "Filter", result_count: 84)

assert_select ".app-c-filter-panel h2", text: "84 results"
end

it "renders the correct result count heading for a large number" do
render_component(button_text: "Filter", result_count: 12_345_678)

assert_select ".app-c-filter-panel h2", text: "12,345,678 results"
end

it "renders the passed block into the content area" do
render_component(button_text: "Filter", result_count: 42) do
tag.p("Hello, world!")
end

assert_select ".app-c-filter-panel .app-c-filter-panel__content p", text: "Hello, world!"
end

it "does not render the content hidden to begin with" do
render_component(button_text: "Filter", result_count: 42)

assert_select ".app-c-filter-panel[hidden]", false
end

it "respects the standard 'open' option" do
render_component(button_text: "Filter", result_count: 42, open: true)

assert_select ".app-c-filter-panel[open=open]"
end
end
77 changes: 77 additions & 0 deletions spec/javascripts/components/filter-panel-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
describe('Filter panel module', () => {
'use strict'

let filterPanel, fixture

const loadFilterPanelComponent = (markup) => {
fixture = document.createElement('div')
document.body.appendChild(fixture)
fixture.innerHTML = markup
filterPanel = new GOVUK.Modules.FilterPanel(fixture.querySelector('.app-c-filter-panel'))
}

const html = `<div data-module="filter-panel" class="app-c-filter-panel">
<div class="app-c-filter-panel__header">
<button class="app-c-filter-panel__button govuk-link" aria-controls="filter-panel" id="filters-button">
<span class="app-c-filter-panel__button-icon"></span>
Filter and sort
</button>
<h2 class="gem-c-heading govuk-heading-s">
123,456 results
</h2>
</div>
<div class="app-c-filter-panel__content" id="filter-panel" role="region" aria-labelledby="filters-button">
<p class="govuk-body">
I can contain arbitrary content, usually a set of filters and sort options.
</p>
</div>
</div>`

beforeEach(() => {
loadFilterPanelComponent(html)
filterPanel.init()
})

afterEach(() => {
fixture.remove()
})

it('is labelled by the open/close button', () => {
expect(filterPanel.$content.getAttribute('aria-labelledby')).toBe(filterPanel.$button.id)
})

it('closes the panel on initialisation', () => {
expect(filterPanel.$button.getAttribute('aria-expanded')).toBe('false')
expect(filterPanel.$content.hasAttribute('hidden')).toBe(true)
})

it('does not close the panel on initialisation if the open attribute is set', () => {
loadFilterPanelComponent(html)
filterPanel.$module.setAttribute('open', 'open')
filterPanel.init()

expect(filterPanel.$button.getAttribute('aria-expanded')).toBe('true')
expect(filterPanel.$content.hasAttribute('hidden')).toBe(false)
})

it('sets the correct attributes when panel is opened', () => {
filterPanel.$button.click()
expect(filterPanel.$button.getAttribute('aria-expanded')).toBe('true')
expect(filterPanel.$content.hasAttribute('hidden')).toBe(false)
})

it('sets the correct attributes when panel is closed', () => {
filterPanel.$button.click()
filterPanel.$button.click()
expect(filterPanel.$button.getAttribute('aria-expanded')).toBe('false')
expect(filterPanel.$content.hasAttribute('hidden')).toBe(true)
expect(document.activeElement).not.toBe(filterPanel.$button)
})

it('prevents any default behaviour of the panel open/close button', () => {
filterPanel.$button.addEventListener('click', (event) => {
expect(event.defaultPrevented).toBe(true)
})
filterPanel.$button.click()
})
})

0 comments on commit d60df01

Please sign in to comment.