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

Add contents list with body component #4636

Merged
merged 4 commits into from
Feb 12, 2025
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 app/assets/javascripts/modules/main-navigation.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* istanbul ignore next */
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
'use strict'

/* istanbul ignore next */
function MainNavigation (module) {
this.module = module
this.module.button = this.module.querySelector('button')
Expand Down
118 changes: 118 additions & 0 deletions app/assets/javascripts/modules/sticky-element-container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
This module will cause a child in the target element to:
- hide when the top of the target element is visible;
- stick to the bottom of the window while the parent element is in view;
- stick to the bottom of the target when the user scrolls past the bottom.

Use 'data-module="sticky-element-container"' to instantiate, and add
`[data-sticky-element]` to the child you want to position.
*/

window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

/* istanbul ignore next */
(function (Modules) {
function StickyElementContainer (element) {
this.wrapper = element
this.stickyElement = this.wrapper.querySelector('[data-sticky-element]')
this.hasResized = true
this.hasScrolled = true
this.interval = 50
this.windowVerticalPosition = 1
this.startPosition = 0
this.stopPosition = 0
}

StickyElementContainer.prototype.init = function () {
if (!this.stickyElement) return

window.onresize = this.onResize.bind(this)
window.onscroll = this.onScroll.bind(this)
setInterval(this.checkResize.bind(this), this.interval)
setInterval(this.checkScroll.bind(this), this.interval)
this.checkResize()
this.checkScroll()
this.stickyElement.classList.add('sticky-element--enabled')
}

StickyElementContainer.prototype.getWindowDimensions = function () {
return {
height: window.innerHeight,
width: window.innerWidth
}
}

StickyElementContainer.prototype.getWindowPositions = function () {
return {
scrollTop: window.scrollY
}
}

StickyElementContainer.prototype.onResize = function () {
this.hasResized = true
}

StickyElementContainer.prototype.onScroll = function () {
this.hasScrolled = true
}

StickyElementContainer.prototype.checkResize = function () {
if (this.hasResized) {
this.hasResized = false
this.hasScrolled = true

var windowDimensions = this.getWindowDimensions()
var elementHeight = this.wrapper.offsetHeight || parseFloat(this.wrapper.style.height.replace('px', ''))
this.startPosition = this.wrapper.offsetTop
this.stopPosition = this.wrapper.offsetTop + elementHeight - windowDimensions.height
}
}

StickyElementContainer.prototype.checkScroll = function () {
if (this.hasScrolled) {
this.hasScrolled = false

this.windowVerticalPosition = this.getWindowPositions().scrollTop

this.updateVisibility()
this.updatePosition()
}
}

StickyElementContainer.prototype.updateVisibility = function () {
var isPastStart = this.startPosition < this.windowVerticalPosition
if (isPastStart) {
this.show()
} else {
this.hide()
}
}

StickyElementContainer.prototype.updatePosition = function () {
var isPastEnd = this.stopPosition < this.windowVerticalPosition
if (isPastEnd) {
this.stickToParent()
} else {
this.stickToWindow()
}
}

StickyElementContainer.prototype.stickToWindow = function () {
this.stickyElement.classList.add('sticky-element--stuck-to-window')
}

StickyElementContainer.prototype.stickToParent = function () {
this.stickyElement.classList.remove('sticky-element--stuck-to-window')
}

StickyElementContainer.prototype.show = function () {
this.stickyElement.classList.remove('sticky-element--hidden')
}

StickyElementContainer.prototype.hide = function () {
this.stickyElement.classList.add('sticky-element--hidden')
}

Modules.StickyElementContainer = StickyElementContainer
})(window.GOVUK.Modules)
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ $govuk-include-default-font-face: false;

// Helper stylesheets (things on more than one page layout)
@import "helpers/content-bottom-margin";
@import "helpers/sticky-element-container";

// frontend mixins
@import "mixins/margins";
Expand Down
49 changes: 49 additions & 0 deletions app/assets/stylesheets/components/_contents-list-with-body.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@import "govuk_publishing_components/individual_component_support";

.app-c-contents-list-with-body__link-container {
margin: 0 auto;
padding: 0;

@include govuk-media-query($from: tablet) {
max-width: 1024px;
}

.app-c-back-to-top {
margin-left: 0;
margin-right: 0;
}
}

.app-c-contents-list-with-body__link-wrapper {
padding-bottom: govuk-spacing(2);

.app-c-back-to-top {
padding-bottom: govuk-spacing(2);
}
}

.app-c-contents-list-with-body__link-wrapper.sticky-element--stuck-to-window {
background-color: govuk-colour("light-grey");
bottom: -1px; // 'Fix' for anomalous 1px margin which sporadically appears below this element.
left: 0;
margin: 0;
padding: 0;
padding-left: 0;
position: fixed;
width: 100%;
z-index: 10;

@include govuk-media-query($from: tablet) {
padding-left: govuk-spacing(2);
}

.app-c-back-to-top {
margin-bottom: 0;
padding: govuk-spacing(3);
width: 66%;

@include govuk-media-query($from: tablet) {
padding: govuk-spacing(4);
}
}
}
23 changes: 23 additions & 0 deletions app/assets/stylesheets/helpers/_sticky-element-container.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.govuk-frontend-supported .sticky-element {
position: absolute;
bottom: 0;

&--stuck-to-window {
bottom: 0;
position: fixed;
}

&--enabled {
transition: opacity, .3s, ease;
opacity: 1;

@include govuk-media-query($until: tablet) {
position: static;
}
}

&--hidden {
opacity: 0;
pointer-events: none;
}
}
29 changes: 29 additions & 0 deletions app/views/components/_contents_list_with_body.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<% add_app_component_stylesheet("contents-list-with-body") %>
<% block = yield %>
<% unless block.empty? %>
<%
contents ||= []
sticky_attr = "sticky-element-container" if contents.any?
%>
<%= tag.div(
id: "contents",
class: "app-c-contents-list-with-body",
data: {
module: sticky_attr,
},
) do %>
<% if contents.any? %>
<div class="responsive-bottom-margin">
<%= render "govuk_publishing_components/components/contents_list", contents: contents %>
</div>
<% end %>
<%= block %>
<% if contents.any? %>
<div data-sticky-element class="app-c-contents-list-with-body__link-wrapper">
<div class="app-c-contents-list-with-body__link-container">
<%= render "components/back_to_top", href: "#contents" %>
</div>
</div>
<% end %>
<% end %>
<% end %>
89 changes: 89 additions & 0 deletions app/views/components/docs/contents_list_with_body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Contents list with body
description: Combines contents-list and back-to-top components with block of body markup
body: |
Wraps the HTML/ERB block passed with a content list and back to top link.

Expects one argument:

* `contents` - The contents to build a contents list, if this item is empty the contents-list and back-to-top links are excluded but the block passed is still rendered.

accessibility_criteria: |
- The component must have a text contrast ratio higher than 4.5:1 against the background colour to meet WCAG AA.
- The component embeds contents-list and back-to-top components. Please see the relevant accessibility criteria:

* [back-to-top](/component-guide/back_to_top)
* [contents-list](/component-guide/contents_list)
shared_accessibility_criteria:
- link
examples:
default:
data:
contents:
- href: "#reference-unless-undertakings-accepted"
text: Reference unless undertakings accepted
- href: "#notice-of-extension-of-the-preliminary-assessment-period"
text: Notice of extension of the preliminary assessment period
- href: "#invitation-to-comment-closes-13-november-2017"
text: "Invitation to comment: closes 13 November 2017"
- href: "#launch-of-merger-inquiry"
text: Launch of merger inquiry
block: |
<div class="govuk-govspeak direction-ltr">
<h2 id="statutory-timetable">Statutory timetable</h2>
<table>
<thead>
<tr>
<th>Phase 1 date</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>3 January 2018</td>
<td>Decision announced</td>
</tr>
<tr>
<td>30 October to 13 November 2017</td>
<td>Invitation to comment</td>
</tr>
<tr>
<td>27 October 2017</td>
<td>Launch of merger inquiry</td>
</tr>
</tbody>
</table>

<h2 id="phase-1">Phase 1</h2>

<h3 id="reference-unless-undertakings-accepted">Reference unless undertakings accepted</h3>

<p>3 January 2018: The CMA has decided, on the information currently available to it, that it is or may be the case that this merger may be expected to result in a substantial lessening of competition within a market or markets in the United Kingdom. This merger will be referred for a phase 2 investigation unless the parties offer acceptable undertakings to address these competition concerns. The full text of the decision will be available shortly.</p>
<ul>
<li>Press release: <a href="https://www.gov.uk/government/news/shoppers-could-face-higher-prices-due-to-soft-drink-merger">Shoppers could face higher prices due to soft drink merger</a> (3.1.18)</li>
</ul>
<h3 id="notice-of-extension-of-the-preliminary-assessment-period">Notice of extension of the preliminary assessment period</h3>
<ul>
<li>
<span class="attachment-inline"><a href="https://assets.publishing.service.gov.uk/media/5a311ebfe5274a4936ee7776/refresco-notice-of-termination-of-extension.pdf">Notice of termination of extension (Refresco) </a></span> (13.12.17)</li>
<li>
<span class="attachment-inline"><a href="https://assets.publishing.service.gov.uk/media/5a26693240f0b659d1fca8d0/refresco-extension-notice.pdf">Notice of extension (Refresco) </a></span> (5.12.17)</li>
</ul>
<h3 id="invitation-to-comment-closes-13-november-2017">Invitation to comment: closes 13 November 2017</h3>
<p>30 October 2017: The Competition and Markets Authority (CMA) is considering whether it is or may be the case that this transaction, if carried into effect, will result in the creation of a relevant merger situation under the merger provisions of the Enterprise Act 2002 and, if so, whether the creation of that situation may be expected to result in a substantial lessening of competition within any market or markets in the United Kingdom for goods or services.</p>
<h3 id="launch-of-merger-inquiry">Launch of merger inquiry</h3>
<p>27 October 2017: The CMA announced the launch of its merger inquiry by notice to the parties.</p>
<ul>
<li>
<span class="attachment-inline"><a href="https://assets.publishing.service.gov.uk/media/59f6f28d40f0b66bbc806ed1/notice_of_commencement_of_initial_period.pdf">Commencement of initial period notice</a></span> (30.10.17)</li>
</ul>
<h3 id="contact">Contact</h3>
<p>Please send written representations about any competition issues to:</p>
<div class="address"><div class="adr org fn"><p>
<br>Competition and Markets Authority
<br>Victoria House
<br>Southampton Row
<br>London
<br>WC1B 4AD
<br>
</p></div></div>
</div>
1 change: 1 addition & 0 deletions config/initializers/dartsass.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"static-error-pages.scss" => "static-error-pages.css",
"components/_back-to-top.scss" => "components/_back-to-top.css",
"components/_calendar.scss" => "components/_calendar.css",
"components/_contents-list-with-body.scss" => "components/_contents-list-with-body.css",
"components/_download-link.scss" => "components/_download-link.css",
"components/_figure.scss" => "components/_figure.css",
"components/_published-dates.scss" => "components/_published-dates.css",
Expand Down
53 changes: 53 additions & 0 deletions spec/components/contents_list_with_body_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
RSpec.describe "ContentsListWithBodyComponent", type: :view do
def component_name
"contents_list_with_body"
end

let(:block) { "<p>Foo</p>".html_safe }

let(:contents_list) do
[
{ href: "/one", text: "1. One" },
{ href: "/two", text: "2. Two" },
]
end

it "renders nothing without a block" do
render_component(contents: contents_list)
expect(rendered).to be_empty
end

it "yields the block without contents data" do
render_component({}) { block }
expect(rendered).to include(block)
end

it "renders a sticky-element-container" do
render_component(contents: contents_list) { block }

expect(rendered).to have_css("#contents.app-c-contents-list-with-body")
expect(rendered).to have_css("#contents[data-module='sticky-element-container']")
end

it "does not apply the sticky-element-container data-module without contents data" do
render_component({}) { block }

expect(rendered).to have_css("#contents[data-module='sticky-element-container']", count: 0)
end

it "renders a contents-list component" do
render_component(contents: contents_list) { block }

expect(rendered).to have_css(".app-c-contents-list-with-body .gem-c-contents-list")
expect(rendered).to have_css(".gem-c-contents-list__link[href='/one']", text: "1. One")
end

it "renders a back-to-top component" do
render_component(contents: contents_list) { block }

expect(rendered).to have_css(%(.app-c-contents-list-with-body
.app-c-contents-list-with-body__link-wrapper
.app-c-contents-list-with-body__link-container
.app-c-back-to-top[href='#contents']))
end
end
Loading