Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Give accordion feature parity with nunjucks macros #203

Merged
merged 7 commits into from
Jul 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Style/TrailingCommaInArguments:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Style/Lambda:
Enabled: false
Style/StringConcatenation:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Expand Down
12 changes: 1 addition & 11 deletions app/components/govuk_component/accordion_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
<%= tag.div(id: @id, class: classes, data: { module: 'govuk-accordion' }, **html_attributes) do %>
<% sections.each do |section| %>
<%= tag.div(id: section.id(suffix: 'section'), class: section.classes, **section.html_attributes) do %>
<div class="govuk-accordion__section-header">
<h2 class="govuk-accordion__section-heading">
<%= tag.span(section.title, id: section.id, class: "govuk-accordion__section-button", aria: { expanded: section.expanded?, controls: section.id(suffix: 'content') }) %>
</h2>
<% if section.summary.present? %>
<%= tag.div(section.summary, id: section.id(suffix: 'summary'), class: %w(govuk-accordion__section-summary govuk-body)) %>
<% end %>
</div>
<%= section %>
<% end %>
<%= section %>
<% end %>
<% end %>
49 changes: 18 additions & 31 deletions app/components/govuk_component/accordion_component.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
class GovukComponent::AccordionComponent < GovukComponent::Base
renders_many :sections, "Section"
renders_many :sections, ->(heading_text: nil, summary_text: nil, expanded: false, classes: [], html_attributes: {}, &block) do
GovukComponent::AccordionComponent::SectionComponent.new(
classes: classes,
expanded: expanded,
heading_level: heading_level, # set once at parent level, passed to all children
html_attributes: html_attributes,
summary_text: summary_text,
heading_text: heading_text,
&block
)
end

attr_reader :id
attr_reader :id, :heading_level

def initialize(id: nil, classes: [], html_attributes: {})
def initialize(id: nil, heading_level: 2, classes: [], html_attributes: {})
super(classes: classes, html_attributes: html_attributes)

@id = id
@id = id
@heading_level = heading_tag(heading_level)
end

private
Expand All @@ -15,33 +26,9 @@ def default_classes
%w(govuk-accordion)
end

class Section < GovukComponent::Base
attr_reader :title, :summary, :expanded

alias_method :expanded?, :expanded

def initialize(title:, summary: nil, expanded: false, classes: [], html_attributes: {})
super(classes: classes, html_attributes: html_attributes)

@title = title
@summary = summary
@expanded = expanded
end

def id(suffix: nil)
[title.parameterize, suffix].compact.join('-')
end

def call
tag.div(content, id: id(suffix: 'content'), class: %w(govuk-accordion__section-content), aria: { labelledby: id })
end

private
def heading_tag(level)
fail(ArgumentError, "heading_level must be 1-6") unless level.in?(1..6)

def default_classes
%w(govuk-accordion__section).tap do |classes|
classes.append("govuk-accordion__section--expanded") if expanded?
end
end
%(h#{level})
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%= tag.div(id: id(suffix: 'section'), class: classes, **html_attributes) do %>
<div class="govuk-accordion__section-header">
<%= content_tag(heading_level, class: "govuk-accordion__section-heading") do %>
<%= tag.span(heading_content, id: id, class: "govuk-accordion__section-button", aria: { expanded: expanded?, controls: id(suffix: 'content') }) %>
<% end %>
<% if summary_content.present? %>
<%= tag.div(summary_content, id: id(suffix: 'summary'), class: %w(govuk-accordion__section-summary govuk-body)) %>
<% end %>
</div>
<%= tag.div(content, id: id(suffix: 'content'), class: %w(govuk-accordion__section-content), aria: { labelledby: id }) %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class GovukComponent::AccordionComponent::SectionComponent < GovukComponent::Base
attr_reader :heading_text, :summary_text, :expanded, :heading_level

renders_one :heading_html
renders_one :summary_html

alias_method :expanded?, :expanded

def initialize(heading_text:, summary_text:, expanded:, heading_level:, classes: [], html_attributes: {})
super(classes: classes, html_attributes: html_attributes)

@heading_text = heading_text
@summary_text = summary_text
@expanded = expanded
@heading_level = heading_level
end

def id(suffix: nil)
# generate a random number if we don't have heading_text to avoid attempting
# to parameterize a potentially-huge chunk of HTML
@prefix ||= heading_text&.parameterize || SecureRandom.hex(4)

[@prefix, suffix].compact.join('-')
end

def heading_content
heading_html || heading_text || fail(ArgumentError, "no heading_text or heading_html")
end

def summary_content
summary_html || summary_text
end

private

def default_classes
%w(govuk-accordion__section).tap do |classes|
classes.append("govuk-accordion__section--expanded") if expanded?
end
end
end
142 changes: 121 additions & 21 deletions spec/components/govuk_component/accordion_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
let(:kwargs) { { id: id } }

subject! do
render_inline(GovukComponent::AccordionComponent.new) do |component|
sections.each do |title, content|
component.section(title: title) { content }
end
render_inline(GovukComponent::AccordionComponent.new(**kwargs)) do |component|
helper.safe_join(
sections.map do |heading_text, content|
component.section(heading_text: heading_text) { content }
end
)
end
end

specify 'renders an container div with the right class' do
specify 'renders a container div with the right class' do
expect(rendered_component).to have_tag('div', with: { class: component_css_class }) do
with_tag('div', with: { class: 'govuk-accordion__section' })
end
Expand All @@ -42,55 +44,153 @@
end

describe 'for each section' do
specify 'the title and content is present' do
sections.each do |title, content|
expect(rendered_component).to have_tag('div', with: { class: 'govuk-accordion__section', id: %(#{title.parameterize}-section) }) do
with_tag('span', with: { id: title.parameterize, class: 'govuk-accordion__section-button' })
with_text(content)
specify 'the heading text and content is present' do
sections.each do |heading_text, content|
expect(rendered_component).to have_tag('div', with: { class: 'govuk-accordion__section', id: %(#{heading_text.parameterize}-section) }) do
with_tag('h2', class: 'govuk-accordion__section-heading') do
with_tag('span', text: heading_text, with: { id: heading_text.parameterize, class: 'govuk-accordion__section-button' })
end

with_tag('div', with: { id: %(#{heading_text.parameterize}-content), class: 'govuk-accordion__section-content' }, text: content)
end
end
end

specify 'each section ID matches the content aria-labelledby' do
sections.each_key do |title|
id = title.parameterize
sections.each_key do |heading_text|
id = heading_text.parameterize

expect(rendered_component).to have_tag('span', with: { id: id, class: 'govuk-accordion__section-button' })
expect(rendered_component).to have_tag('div', with: { 'aria-labelledby' => id })
end
end

specify 'each section ID matches the button aria-controls' do
sections.each_key do |title|
id = title.parameterize
sections.each_key do |heading_text|
id = heading_text.parameterize

expect(rendered_component).to have_tag('div', with: { id: %(#{id}-content) })
expect(rendered_component).to have_tag('span', with: { 'aria-controls' => %(#{id}-content) })
end
end
end

describe 'overriding the section heading level' do
let(:kwargs) { { heading_level: 3 } }

specify 'has the overriden level' do
expect(rendered_component).to have_tag('h3', with: { class: 'govuk-accordion__section-heading' })
end

context 'when the heading level is invalid' do
specify 'has the overriden level' do
expected_message = "heading_level must be 1-6"

expect { GovukComponent::AccordionComponent.new(heading_level: 8) }.to raise_error(ArgumentError, expected_message)
end
end
end

describe 'overriding the section heading with HTML' do
let(:custom_tag) { :marquee }
let(:custom_text) { "Fanciest accordion heading" }
let(:custom_class) { "purple" }
let(:custom_content) { "What a nice accordion!" }

subject! do
render_inline(GovukComponent::AccordionComponent.new(**kwargs)) do |component|
component.section do |section|
section.heading_html do
helper.content_tag(custom_tag, custom_text, class: custom_class)
end

custom_content
end
end
end

specify "renders the custom heading content" do
expect(rendered_component).to have_tag("h2", with: { class: "govuk-accordion__section-heading" }) do
with_tag(custom_tag, text: custom_text, with: { class: custom_class })
end
end

specify "renders the custom content" do
expect(rendered_component).to have_tag("div", with: { class: "govuk-accordion__section-content" }, text: custom_content)
end

specify "uses a random string as an identifier to link the heading and content together" do
button_identifier = html.at_css('span.govuk-accordion__section-button').attribute('id').value
content_identifier = %(#{button_identifier}-content)

expect(rendered_component).to have_tag('span', with: { id: button_identifier, 'aria-controls' => content_identifier })
expect(rendered_component).to have_tag('div', with: { id: content_identifier, 'aria-labelledby' => button_identifier })
end
end

describe 'when no heading text or HTML is supplied' do
specify "raises an appropriate error" do
expect {
render_inline(GovukComponent::AccordionComponent.new(**kwargs)) do |component|
component.section(summary_text: "A summary")
end
}.to raise_error(ArgumentError, /no heading_text or heading_html/)
end
end

describe 'summaries' do
specify 'no summary by default' do
expect(rendered_component).not_to have_tag('.govuk-accordion__section-summary')
end

context 'when a summary text is provided' do
let(:title) { 'a thing' }
let(:summary_content) { 'some summary content' }
let(:heading_text) { 'a thing' }
let(:summary_text) { 'some summary content' }
let(:expected_classes) { %w(govuk-accordion__section-summary govuk-body) }

subject! do
render_inline(GovukComponent::AccordionComponent.new) do |component|
component.section(title: title, summary: summary_content) { 'abc' }
component.section(heading_text: heading_text, summary_text: summary_text) { 'abc' }
end
end

specify 'the summary is rendered with the right id, class and text' do
expect(rendered_component).to have_tag('.govuk-accordion__section-header') do
with_tag('div', with: { id: %(#{title.parameterize}-summary), class: expected_classes }, text: summary_content)
with_tag('div', with: { id: %(#{heading_text.parameterize}-summary), class: expected_classes }, text: summary_text)
end
end
end

describe 'overriding the section heading with HTML' do
let(:custom_tag) { :strong }
let(:custom_text) { "This is a summary" }
let(:custom_class) { "special" }
let(:custom_content) { "What a nice summary!" }
let(:heading_text) { "some heading" }

subject! do
render_inline(GovukComponent::AccordionComponent.new(**kwargs)) do |component|
component.section(heading_text: heading_text) do |section|
section.summary_html do
helper.content_tag(custom_tag, custom_text, class: custom_class)
end

custom_content
end
end
end

specify "renders the custom summary content" do
expect(rendered_component).to have_tag("div", with: { class: "govuk-accordion__section-header" }) do
with_tag("div", with: { class: "govuk-accordion__section-summary" }) do
with_tag(custom_tag, text: custom_text, with: { class: custom_class })
end
end
end

specify "renders the custom content" do
expect(rendered_component).to have_tag("div", with: { class: "govuk-accordion__section-content" }, text: custom_content)
end
end
end

Expand All @@ -100,15 +200,15 @@
context 'slot arguments' do
let(:slot) { :section }
let(:content) { -> { 'some swanky accordion content' } }
let(:slot_kwargs) { { title: 'A title', summary: 'A summary' } }
let(:slot_kwargs) { { heading_text: 'A heading_text', summary_text: 'A summary' } }

it_behaves_like 'a component with a slot that accepts custom classes'
it_behaves_like 'a component with a slot that accepts custom html attributes'

specify 'sections have the correct expanded states' do
render_inline(GovukComponent::AccordionComponent.new) do |component|
component.section(expanded: true, title: 'section 1', html_attributes: { id: 'section_1' }) { 'abc' }
component.section(title: 'section 2', html_attributes: { id: 'section_2' }) { 'def' }
component.section(expanded: true, heading_text: 'section 1', html_attributes: { id: 'section_1' }) { 'abc' }
component.section(heading_text: 'section 2', html_attributes: { id: 'section_2' }) { 'def' }
end

expect(rendered_component).to have_tag('div', with: { id: 'section_1', class: %w(govuk-accordion__section govuk-accordion__section--expanded) })
Expand Down