Skip to content

Commit

Permalink
Merge pull request #203 from DFE-Digital/version-2.0.0-pp-accordion
Browse files Browse the repository at this point in the history
Give accordion feature parity with nunjucks macros
  • Loading branch information
peteryates authored Jul 4, 2021
2 parents 4c3bf7a + d5b4985 commit 3816a6b
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 63 deletions.
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

0 comments on commit 3816a6b

Please sign in to comment.