diff --git a/app/components/navigation/structures/admin_sub_navigation.rb b/app/components/navigation/structures/admin_sub_navigation.rb new file mode 100644 index 00000000..a86e74a7 --- /dev/null +++ b/app/components/navigation/structures/admin_sub_navigation.rb @@ -0,0 +1,51 @@ +module Navigation + module Structures + class AdminSubNavigation < Navigation::Structures::BaseSubNavigation + def get + [ + Node.new( + name: "Admin", + href: "#", + prefix: "/admin", + nodes: [ + Node.new( + name: "Admin 1.1", + href: '#', + prefix: "/admin/1.1" + ), + Node.new( + name: "Admin 1.2", + href: '#', + prefix: "/admin/1.2" + ) + ] + ), + Node.new( + name: "Sub Nav 2", + href: '#', + prefix: "/sub-nav-2", + nodes: [ + Node.new( + name: "Sub Nav 2.1", + href: '#', + prefix: "/sub-nav-2.1" + ), + ] + ), + Node.new( + name: "Sub Nav 3", + href: '#', + prefix: "/sub-nav-3", + nodes: [ + Node.new( + name: "Sub Nav 3.1", + href: '#', + prefix: "/sub-nav-3.1" + ), + ] + ), + ] + end + end + end +end diff --git a/app/components/navigation/structures/base_sub_navigation.rb b/app/components/navigation/structures/base_sub_navigation.rb new file mode 100644 index 00000000..5baa7a46 --- /dev/null +++ b/app/components/navigation/structures/base_sub_navigation.rb @@ -0,0 +1,19 @@ +module Navigation + module Structures + class BaseSubNavigation + include Rails.application.routes.url_helpers + + # A Node is an entry in a navigation list, it contains: + # + # * name - the hyperlink text which appears in the list + # * href - the hyperlink href + # * prefix - the beginning of a path which will trigger the node to be marked + # 'current' and highlighted in the nav + # * current - a boolean value where the result of the prefix match is stored, + # this isn't done on the fly so we can pass the value along from + # the primary nav to the sub nav + # * nodes - a list of nodes that sit under this one in the structure + Node = Struct.new(:name, :href, :prefix, :nodes) + end + end +end diff --git a/app/components/navigation/sub_navigation_component.html.erb b/app/components/navigation/sub_navigation_component.html.erb new file mode 100644 index 00000000..140ef3f1 --- /dev/null +++ b/app/components/navigation/sub_navigation_component.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/components/navigation/sub_navigation_component.rb b/app/components/navigation/sub_navigation_component.rb new file mode 100644 index 00000000..4b9a034d --- /dev/null +++ b/app/components/navigation/sub_navigation_component.rb @@ -0,0 +1,40 @@ +require 'debug' + +module Navigation + class SubNavigationComponent < ViewComponent::Base + attr_accessor :current_path, :current_section, :structure + + def initialize(current_path, structure:) + @current_path = current_path + @structure = structure + end + + def render? + structure.present? + end + + def navigation_link(section) + link_to( + section.name, + section.href, + class: "x-govuk-sub-navigation__link", + aria: { current: current?(section.prefix) }, + ) + end + + def navigation_item_classes(section) + class_names( + "x-govuk-sub-navigation__section-item", + "x-govuk-sub-navigation__section-item--current" => current?(section.prefix), + ) + end + + private + + def current?(prefix) + # return nil instead of false so Rails' link helper drops the + # attribute rather than setting "current='false'" + current_path.start_with?(prefix) || nil + end + end +end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb new file mode 100644 index 00000000..86d5178e --- /dev/null +++ b/app/helpers/admin_helper.rb @@ -0,0 +1,5 @@ +module AdminHelper + def admin_sub_navigation_structure + @admin_navigation_structure ||= Navigation::Structures::AdminSubNavigation.new.get + end +end diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index c0701495..b285efa7 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -12,3 +12,13 @@ { key: { text: "Session expires" }, value: { text: session_manager.expires_at.localtime.to_fs(:govuk) } }, ] ) %> + +<%= content_for(:sidebar) do %> + <%= + render Navigation::SubNavigationComponent.new( + request.path, + structure: admin_sub_navigation_structure + ) + %> +<% end %> + diff --git a/spec/components/navigation/sub_navigation_component_spec.rb b/spec/components/navigation/sub_navigation_component_spec.rb new file mode 100644 index 00000000..98dee03a --- /dev/null +++ b/spec/components/navigation/sub_navigation_component_spec.rb @@ -0,0 +1,146 @@ +require "rails_helper" + +class TestSubNavigationStructureTwoLevels < Navigation::Structures::BaseSubNavigation + def get + [ + Node.new( + name: "This is the Sub Nav 1 page", + href: "/sub-nav-1", + prefix: "/sub-nav-1", + nodes: [ + Node.new( + name: "This is the Sub Nav 1.1 page", + href: "/sub-nav-1.1", + prefix: "/sub-nav-1.1" + ), + ] + ), + Node.new( + name: "This is the Sub Nav 2 page", + href: "/sub-nav-2", + prefix: "/sub-nav-2", + nodes: [ + Node.new( + name: "This is the Sub Nav 2.1 page", + href: '/sub-nav-2.1', + prefix: "/sub-nav-2.1" + ), + ] + ), + ] + end +end + +class TestSubNavigationStructureOneLevel < Navigation::Structures::BaseSubNavigation + def get + [ + Node.new( + name: "This is the Sub Nav 1 page", + href: '/sub-nav-1', + prefix: "/sub-nav-1", + ) + ] + end +end + +RSpec.describe Navigation::SubNavigationComponent, type: :component do + describe "a nested navigation structure with two levels" do + let(:current_path) { "/some-path" } + let(:structure) { TestSubNavigationStructureTwoLevels.new } + + subject do + Navigation::SubNavigationComponent.new(current_path, structure: structure.get) + end + + it "renders a visually hidden h2 heading" do + render_inline(subject) + + expect(rendered_content).to have_css("h2.govuk-visually-hidden", text: "Navigation") + end + + it "renders top level navigation items" do + render_inline(subject) + + selector = "li.x-govuk-sub-navigation__section-item > a.x-govuk-sub-navigation__link" + + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 1 page") + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 2 page") + end + + it "renders second level navigation items" do + render_inline(subject) + + selector = %w[ + li.x-govuk-sub-navigation__section-item + ul.x-govuk-sub-navigation__section--nested + li.x-govuk-sub-navigation__section-item + a.x-govuk-sub-navigation__link + ].join(" > ") + + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 1.1 page") + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 2.1 page") + end + end + + describe "a navigation structure with only one level" do + let(:structure) { TestSubNavigationStructureOneLevel.new } + let(:current_path) { "/some-path" } + + subject do + Navigation::SubNavigationComponent.new(current_path, structure: structure.get) + end + + it "renders top level navigation items" do + render_inline(subject) + + selector = "li.x-govuk-sub-navigation__section-item > a.x-govuk-sub-navigation__link" + + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 1 page") + end + end + + describe "highlighting the current top level nav item" do + context "when the prefix matches the start of the current path" do + let(:current_path) { "/sub-nav-1" } + let(:structure) { TestSubNavigationStructureTwoLevels.new } + + subject do + Navigation::SubNavigationComponent.new(current_path, structure: structure.get) + end + + it "marks only the nav item with the matching prefix as 'current'" do + render_inline(subject) + + selector = "li.x-govuk-sub-navigation__section-item--current" + + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 1 page") + expect(rendered_content).to have_css(selector, count: 1) + end + end + end + + describe "highlighting the current second level nav item" do + context "when the prefix matches the start of the current path" do + let(:current_path) { "/sub-nav-1.1" } + let(:structure) { TestSubNavigationStructureTwoLevels.new } + + subject do + Navigation::SubNavigationComponent.new(current_path, structure: structure.get) + end + + it "marks only the section as current" do + render_inline(subject) + + selector = %w[ + li.x-govuk-sub-navigation__section-item + ul.x-govuk-sub-navigation__section--nested + li.x-govuk-sub-navigation__section-item + a.x-govuk-sub-navigation__link[aria-current="true"] + ].join(" > ") + + expect(rendered_content).to have_css(selector, text: "This is the Sub Nav 1.1 page") + expect(rendered_content).to have_css(selector, count: 1) + end + end + end +end