diff --git a/app/assets/javascripts/modules/main-navigation.js b/app/assets/javascripts/modules/main-navigation.js index 79a2b39db7..1959aa624c 100644 --- a/app/assets/javascripts/modules/main-navigation.js +++ b/app/assets/javascripts/modules/main-navigation.js @@ -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') diff --git a/app/assets/javascripts/modules/sticky-element-container.js b/app/assets/javascripts/modules/sticky-element-container.js new file mode 100644 index 0000000000..dfc45f8bfd --- /dev/null +++ b/app/assets/javascripts/modules/sticky-element-container.js @@ -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) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 59bca1ca84..69d082d62a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -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"; diff --git a/app/assets/stylesheets/components/_contents-list-with-body.scss b/app/assets/stylesheets/components/_contents-list-with-body.scss new file mode 100644 index 0000000000..3fe1ac5e06 --- /dev/null +++ b/app/assets/stylesheets/components/_contents-list-with-body.scss @@ -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); + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/helpers/_sticky-element-container.scss b/app/assets/stylesheets/helpers/_sticky-element-container.scss new file mode 100644 index 0000000000..a8826e85f0 --- /dev/null +++ b/app/assets/stylesheets/helpers/_sticky-element-container.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/views/components/_contents_list_with_body.html.erb b/app/views/components/_contents_list_with_body.html.erb new file mode 100644 index 0000000000..e9bc66b33c --- /dev/null +++ b/app/views/components/_contents_list_with_body.html.erb @@ -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? %> +
Phase 1 date | +Action | +
---|---|
3 January 2018 | +Decision announced | +
30 October to 13 November 2017 | +Invitation to comment | +
27 October 2017 | +Launch of merger inquiry | +
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.
+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.
+27 October 2017: The CMA announced the launch of its merger inquiry by notice to the parties.
+Please send written representations about any competition issues to:
+
+
Competition and Markets Authority
+
Victoria House
+
Southampton Row
+
London
+
WC1B 4AD
+
+
Foo
".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 diff --git a/spec/javascripts/unit/modules/sticky-element-container.spec.js b/spec/javascripts/unit/modules/sticky-element-container.spec.js new file mode 100644 index 0000000000..8c22ec2863 --- /dev/null +++ b/spec/javascripts/unit/modules/sticky-element-container.spec.js @@ -0,0 +1,73 @@ +describe('A sticky-element-container module', function () { + 'use strict' + + var GOVUK = window.GOVUK + + describe('on desktop', function () { + var $element + var $footer + var instance + + beforeEach(function () { + $element = $( + '