-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3445 from alphagov/filters-final
Add a `filter_panel` component for new UI
- Loading branch information
Showing
8 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |