diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 353e96c1..d899b204 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,205 +7,11 @@ on:
pull_request:
branches:
- '1.x'
-
-env:
- LOCALGOV_DRUPAL_PROJECT: localgovdrupal/localgov_base
- LOCALGOV_DRUPAL_PROJECT_PATH: web/themes/contrib/localgov_base
+ workflow_dispatch:
jobs:
-
- build:
- name: Install LocalGov Drupal
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- localgov-version:
- - '3.x'
- drupal-version:
- - '~10.0'
- php-version:
- - '8.1'
- - '8.2'
-
- steps:
-
- - name: Save git branch and git repo names to env if this is not a pull request
- if: github.event_name != 'pull_request'
- run: |
- echo "GIT_BASE=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- echo "GIT_BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
- echo "HEAD_USER=localgovdrupal" >> $GITHUB_ENV
-
- - name: Save git branch and git repo names to env if this is a pull request
- if: github.event_name == 'pull_request'
- run: |
- echo "GIT_BASE=${GITHUB_BASE_REF}" >> $GITHUB_ENV
- echo "GIT_BRANCH=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
- export HEAD="${{ github.event.pull_request.head.label }}"
- echo "HEAD_USER=${HEAD%%:*}" >> $GITHUB_ENV
-
- - name: Set composer branch reference for version branches
- if: endsWith(github.ref, '.x')
- run: echo "COMPOSER_REF=${GIT_BRANCH}-dev" >> $GITHUB_ENV
-
- - name: Set composer branch reference for non-version branches
- if: endsWith(github.ref, '.x') == false
- run: echo "COMPOSER_REF=dev-${GIT_BRANCH}" >> $GITHUB_ENV
-
- - name: Get the latest tagged release for branch version
- run: |
- LATEST_RELEASE=$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${GIT_BASE%'.x'} | grep -Po '(?<=refs/tags/)[^"]+' | tail -1)
- if [ -z $LATEST_RELEASE ]; then LATEST_RELEASE=1; fi
- echo "LATEST_RELEASE=${LATEST_RELEASE}" >> $GITHUB_ENV
-
- - name: Cached workspace
- uses: actions/cache@v2
- with:
- path: ./html
- key: localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-${{ github.run_id }}-${{ secrets.CACHE_VERSION }}
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-version }}
-
- - name: Clone drupal_container
- uses: actions/checkout@v2
- with:
- repository: localgovdrupal/drupal-container
- ref: php${{ matrix.php-version }}
-
- - name: Create LocalGov Drupal project
- run: |
- composer create-project --stability dev --no-install localgovdrupal/localgov-project ./html "${{ matrix.localgov-version }}"
- composer --working-dir=./html require --no-install localgovdrupal/localgov:${{ matrix.localgov-version }}-dev
- composer --working-dir=./html require --no-install drupal/core-recommended:${{ matrix.drupal-version }} drupal/core-composer-scaffold:${{ matrix.drupal-version }} drupal/core-project-message:${{ matrix.drupal-version }} drupal/core-dev:${{ matrix.drupal-version }}
- composer --working-dir=./html install
-
- - name: Obtain the test target using Composer
- if: env.HEAD_USER == 'localgovdrupal'
- run: |
- composer --working-dir=html config repositories.1 vcs git@github.com:${LOCALGOV_DRUPAL_PROJECT}.git
- composer global config github-oauth.github.com ${{ github.token }}
- composer --working-dir=./html require --with-all-dependencies ${LOCALGOV_DRUPAL_PROJECT}:"${COMPOSER_REF} as ${LATEST_RELEASE}"
-
- - name: Obtain the test target using Git
- if: env.HEAD_USER != 'localgovdrupal'
- uses: actions/checkout@v2
- with:
- path: ${{ env.LOCALGOV_DRUPAL_PROJECT_PATH }}
-
- phpcs:
- name: Coding standards checks
- needs: build
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- localgov-version:
- - '3.x'
- drupal-version:
- - '~10.0'
- php-version:
- - '8.1'
- - '8.2'
-
- steps:
-
- - name: Cached workspace
- uses: actions/cache@v2
- with:
- path: ./html
- key: localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-${{ github.run_id }}-${{ secrets.CACHE_VERSION }}
- restore-keys: |
- localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-version }}
-
- - name: Run coding standards checks
- run: |
- cd html
- ./bin/phpcs -p ${LOCALGOV_DRUPAL_PROJECT_PATH}
-
- phpstan:
- name: Deprecated code checks
- needs: build
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- localgov-version:
- - '3.x'
- drupal-version:
- - '~10.0'
- php-version:
- - '8.1'
- - '8.2'
-
- steps:
-
- - name: Cached workspace
- uses: actions/cache@v2
- with:
- path: ./html
- key: localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-${{ github.run_id }}-${{ secrets.CACHE_VERSION }}
- restore-keys: |
- localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-version }}
-
- - name: Run deprecated code checks
- run: |
- cd html
- ./bin/phpstan analyse -c ./phpstan.neon ${LOCALGOV_DRUPAL_PROJECT_PATH}
- phpunit:
- name: PHPUnit tests
- needs: build
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- localgov-version:
- - '3.x'
- drupal-version:
- - '~10.0'
- php-version:
- - '8.1'
- - '8.2'
-
- steps:
-
- - name: Clone Drupal container
- uses: actions/checkout@v2
- with:
- repository: localgovdrupal/drupal-container
- ref: php${{ matrix.php-version }}
-
- - name: Cached workspace
- uses: actions/cache@v2
- with:
- path: ./html
- key: localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-${{ github.run_id }}-${{ secrets.CACHE_VERSION }}
- restore-keys: |
- localgov-build-${{ matrix.localgov-version }}-${{ matrix.drupal-version }}-${{ matrix.php-version }}-
-
- - name: Start Docker environment
- run: docker-compose -f docker-compose.yml up -d
-
- - name: Run PHPUnit tests
- run: |
- mkdir -p ./html/web/sites/simpletest && chmod 777 ./html/web/sites/simpletest
- sed -i "s#http://localgov.lndo.site#http://drupal#" ./html/phpunit.xml.dist
- docker exec -t drupal bash -c 'chown docker:docker -R /var/www/html'
- docker exec -u docker -t drupal bash -c "cd /var/www/html && ./bin/paratest --processes=4 /var/www/html/${{ env.LOCALGOV_DRUPAL_PROJECT_PATH }}"
+ tests:
+ uses: localgovdrupal/localgov_shared_workflows/.github/workflows/test-module.yml@1.x
+ with:
+ project: 'localgovdrupal/localgov_base'
+ project_path: 'web/themes/contrib/localgov_base'
diff --git a/config/install/localgov_base.settings.yml b/config/install/localgov_base.settings.yml
index 5e4015f4..7e8914b3 100644
--- a/config/install/localgov_base.settings.yml
+++ b/config/install/localgov_base.settings.yml
@@ -2,3 +2,4 @@ localgov_base_remove_css: FALSE
localgov_base_remove_js: FALSE
localgov_base_add_unpublished_background_colour: TRUE
localgov_base_add_draft_note_to_unpublished_content: FALSE
+localgov_base_header_behaviour: 'default'
diff --git a/config/schema/localgov_base.schema.yml b/config/schema/localgov_base.schema.yml
index c6148cc3..5dccf790 100644
--- a/config/schema/localgov_base.schema.yml
+++ b/config/schema/localgov_base.schema.yml
@@ -14,3 +14,9 @@ localgov_base.settings:
localgov_base_add_draft_note_to_unpublished_content:
type: boolean
label: 'Add "[Draft]" to title of unpublished content.'
+ localgov_base_header_behaviour:
+ type: string
+ label: 'Header behaviour'
+ description: 'Choose whether the header should be sticky or not.'
+ constraints:
+ Regex: '^(default|sticky|appears_on_scroll)$'
diff --git a/css/base/base.css b/css/base/base.css
index b651e2b1..ca98ed79 100644
--- a/css/base/base.css
+++ b/css/base/base.css
@@ -16,6 +16,10 @@ html {
margin-top: 0; /* Removing top margin, for better vertical rhythm layout */
}
+dialog {
+ margin: auto;
+}
+
body {
margin: 0;
color: var(--color-text);
@@ -158,6 +162,13 @@ ol ul {
margin-bottom: 0;
}
+blockquote:not(.pull-out-quote__content) {
+ padding-inline-start: var(--quote-padding);
+ padding-left: var(--quote-padding-left);
+ border-inline-start: var(--quote-border);
+ border-color: var(--color-accent);
+}
+
sub,
sup {
position: relative;
diff --git a/css/base/ckeditor5.css b/css/base/ckeditor5.css
index 8fb8a07a..f6662058 100644
--- a/css/base/ckeditor5.css
+++ b/css/base/ckeditor5.css
@@ -52,7 +52,7 @@
position: relative;
top: 7px;
margin-left: 0.5rem;
- content: "\203A";
+ content: "\203A" / "";
font-size: 2.875rem;
}
diff --git a/css/base/variables.css b/css/base/variables.css
index db52962d..c0660ca7 100644
--- a/css/base/variables.css
+++ b/css/base/variables.css
@@ -141,6 +141,9 @@ body {
/* Animation */
--transition-time: 0.3s;
+
+ /* Dialog */
+ --dialog-backdrop-color: rgba(0, 0, 0, 0.8);
}
/*
@@ -540,4 +543,8 @@ body {
--add-to-calendar-dialog-backdrop-opacity: 0.75;
--add-to-calendar--icon-color: var(--color-white);
--add-to-calendar--icon-color-hover: var(--color-accent);
+
+ /* Accordion */
+ --accordion-icon-closed: "+";
+ --accordion-icon-opened: "-";
}
diff --git a/css/components/accordion.css b/css/components/accordion.css
new file mode 100644
index 00000000..4ccfa498
--- /dev/null
+++ b/css/components/accordion.css
@@ -0,0 +1,33 @@
+/**
+ * @file
+ * Style rules for accordions.
+ */
+
+/* Default */
+.accordion-pane__title button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.accordion-pane__title button:hover,
+.accordion-pane__title button:focus {
+ text-decoration: none;
+}
+
+.accordion-icon {
+ display: block;
+}
+
+span.accordion-icon::after {
+ display: block;
+ font-size: 150%;
+}
+
+.accordion-pane__title button[aria-expanded="false"] > .accordion-icon::after {
+ content: var(--accordion-icon-closed);
+}
+
+.accordion-pane__title button[aria-expanded="true"] > .accordion-icon::after {
+ content: var(--accordion-icon-opened);
+}
diff --git a/css/components/alert-banner.css b/css/components/alert-banner.css
index 191dc4f5..abb69446 100644
--- a/css/components/alert-banner.css
+++ b/css/components/alert-banner.css
@@ -8,6 +8,14 @@
background-color: var(--alert-banner-bg-color);
}
+dialog.localgov-alert-banner {
+ margin: auto;
+}
+
+dialog.localgov-alert-banner::backdrop {
+ background-color: var(--dialog-backdrop-color);
+}
+
.localgov-alert-banner,
.localgov-alert-banner a {
color: var(--alert-banner-color);
diff --git a/css/components/back-to-top.css b/css/components/back-to-top.css
new file mode 100644
index 00000000..96f6ee8f
--- /dev/null
+++ b/css/components/back-to-top.css
@@ -0,0 +1,41 @@
+.back-to-top {
+ background: var(--button-link-bg-color);
+ border: 3px solid var(--color-white);
+ bottom: 2em;
+ color: var(--button-link-color);
+ display: flex;
+ font-weight: bold;
+ gap: var(--spacing-small);
+ padding: var(--button-link-padding);
+ position: fixed;
+ right: 2em;
+ text-decoration: none;
+ transition: var(--transition-time);
+ opacity: 1;
+}
+
+.back-to-top[hidden] {
+ opacity: 0;
+}
+
+.back-to-top:hover,
+.back-to-top:focus {
+ text-decoration: underline;
+}
+
+.back-to-top__icon svg path {
+ fill: currentcolor;
+}
+
+.back-to-top-target {
+ max-height: 1px;
+ max-width: 1px;
+ overflow: hidden;
+}
+
+@media screen and (min-width: 48rem) {
+ .back-to-top {
+ bottom: 5em;
+ right: 5em;
+ }
+}
diff --git a/css/components/form-items.css b/css/components/form-items.css
index 05af79bf..c28e313e 100644
--- a/css/components/form-items.css
+++ b/css/components/form-items.css
@@ -360,3 +360,25 @@ input[type="file"]:hover,
.form-item-managed-file-button .description br {
margin-bottom: var(--spacing);
}
+
+.facets-form fieldset {
+ padding: 0;
+ border: none;
+}
+
+.facets-form .facets-widget > ul,
+.facets-form .facets-widget > ul ul {
+ list-style: none;
+ padding: 0;
+}
+
+.facets-form .facets-widget > ul > li + li {
+ margin-block-start: var(--spacing-largest);
+}
+
+.facets-form .form-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing);
+ align-items: center;
+}
diff --git a/css/components/guide-nav.css b/css/components/guide-nav.css
index c1023dfa..6e1acefb 100644
--- a/css/components/guide-nav.css
+++ b/css/components/guide-nav.css
@@ -6,6 +6,7 @@
.lgd-guide-nav__list {
margin-bottom: 0;
+ word-wrap: break-word;
}
@media screen and (min-width: 48rem) {
diff --git a/css/components/header.css b/css/components/header.css
index a495be8c..028159a2 100644
--- a/css/components/header.css
+++ b/css/components/header.css
@@ -104,7 +104,7 @@
.lgd-header__toggle-icon::after {
display: inline-block;
margin-left: var(--spacing-smaller);
- content: "\203A";
+ content: "\203A" / "";
transition: var(--transition-time);
transform: rotate(90deg);
font-size: var(--font-size-larger);
diff --git a/css/components/pager.css b/css/components/pager.css
index 45b20f91..188148bb 100644
--- a/css/components/pager.css
+++ b/css/components/pager.css
@@ -14,3 +14,7 @@
.pager__item::marker {
color: transparent;
}
+
+.pager__item > a {
+ padding-inline: var(--spacing-smaller);
+}
diff --git a/css/components/quote.css b/css/components/quote.css
index d3babca1..28d63c67 100644
--- a/css/components/quote.css
+++ b/css/components/quote.css
@@ -5,7 +5,7 @@
padding: var(--quote-padding);
padding-left: var(--quote-padding-left);
border-color: var(--quote-border-color);
- border-left: var(--quote-border);
+ border-inline-start: var(--quote-border);
background-color: var(--quote-bg-color);
}
diff --git a/css/components/service-statuses.css b/css/components/service-statuses.css
index 1b9332d1..829bf06c 100644
--- a/css/components/service-statuses.css
+++ b/css/components/service-statuses.css
@@ -18,6 +18,7 @@
.service-statuses__label {
margin-bottom: 0;
+ color: var(--service-statuses-container-text-color);
}
.service-statuses__link {
diff --git a/css/components/sticky-header.css b/css/components/sticky-header.css
new file mode 100644
index 00000000..d45d3692
--- /dev/null
+++ b/css/components/sticky-header.css
@@ -0,0 +1,15 @@
+.sticky-header--sticky .lgd-header {
+ top: var(--lgd-sticky-header-position);
+ width: 100%;
+ z-index: 1000;
+}
+
+.sticky-header--sticky .lgd-header + * {
+ margin-top: calc(var(--lgd-sticky-header-position) + var(--lgd-sticky-header-height));
+ scroll-padding: var(--lgd-sticky-header-height);
+}
+
+.sticky-header-html,
+.sticky-header--sticky {
+ scroll-padding-top: var(--lgd-sticky-header-height);
+}
diff --git a/css/components/wysiwyg-styles.css b/css/components/wysiwyg-styles.css
index 4176603e..86ef7920 100644
--- a/css/components/wysiwyg-styles.css
+++ b/css/components/wysiwyg-styles.css
@@ -53,7 +53,7 @@
position: relative;
top: var(--btn-start-icon-top);
margin-left: 0.5rem;
- content: var(--btn-start-icon);
+ content: var(--btn-start-icon) / "";
font-size: var(--btn-start-icon-size);
line-height: 0;
}
diff --git a/css/print/print.css b/css/print/print.css
index 31fbabaf..3ee26b2c 100644
--- a/css/print/print.css
+++ b/css/print/print.css
@@ -3,7 +3,10 @@
.lgd-region__inner--header > *:not(.block-system-branding-block),
.sidebar,
.lgd-prev-next,
-.newsroom__sidebar {
+.newsroom__sidebar,
+.block-localgov-guides-contents,
+.block-views-blocklocalgov-step-by-step-navigation-steps-for-overview,
+.block-views-blocklocalgov-step-by-step-navigation-steps {
display: none;
}
diff --git a/js/back-to-top.js b/js/back-to-top.js
new file mode 100644
index 00000000..4dc4e910
--- /dev/null
+++ b/js/back-to-top.js
@@ -0,0 +1,65 @@
+/**
+ * @file Drupal behavior for 'back to top' link.
+ */
+
+(function (Drupal) {
+ Drupal.behaviors.localgovBackToTop = {
+ attach(context) {
+ /**
+ * Callback for intersection observer.
+ *
+ * Sets back to top link's hidden attribute to 'false' if the intersection
+ * target is either:
+ *
+ * - intersecting the viewport, OR
+ * - has been scrolled UP out of the viewport
+ *
+ * @param {IntersectionObserverEntry[]} entries
+ * The qualifying entries to be checked for intersection with, or
+ * past the top of the viewport. In practices, there's only one of
+ * these since we've only targeted one element.
+ */
+ function observerCallback(entries) {
+ entries.forEach((entry) => {
+ backToTop.hidden = (
+ entry.isIntersecting ||
+ (!entry.isIntersecting && entry.boundingClientRect.top <= 0)
+ ) ? false : 'until-hidden';
+ });
+ }
+
+ const [backToTop] = once('back-to-top', '.back-to-top', context);
+ const [backToTopTarget] = once(
+ 'back-to-top-target',
+ '.back-to-top-target',
+ context,
+ );
+ const minContentViewportRatio = parseFloat(
+ backToTop?.dataset?.minContentViewportRatio ?? 1.5,
+ 10,
+ );
+ const viewportHeight = window.innerHeight;
+ const documentHeight = document.documentElement.offsetHeight;
+ let intersectionObserver;
+
+ if (
+ !backToTop ||
+ !backToTopTarget ||
+ documentHeight / viewportHeight < minContentViewportRatio
+ ) {
+ return;
+ }
+
+ // Create an element absolutely positioned at our threshold.
+ backToTopTarget.style.position = 'absolute';
+ backToTopTarget.style.top = `${viewportHeight * minContentViewportRatio}px`;
+ backToTop.addEventListener('click', (event) => event.target.hidden = 'until-found');
+
+ // Create an IntersectionObserver.
+ intersectionObserver = new IntersectionObserver(observerCallback, {
+ rootMargin: '16px',
+ });
+ intersectionObserver.observe(backToTopTarget);
+ },
+ };
+})(Drupal);
diff --git a/js/header.js b/js/header.js
index e73ca44f..01638439 100644
--- a/js/header.js
+++ b/js/header.js
@@ -47,8 +47,11 @@
// The resulting region.primary.firstLink isn't used, but it's less
// difficult to add it than to add only region.secondary.firstLink.
if (region) {
- const firstLink = region.querySelector('.menu a');
- navInfo[nav] = { toggle, region, firstLink };
+ const links = region.querySelectorAll('.menu a');
+ const firstLink = links[0];
+ const lastLink = links[links.length - 1];
+
+ navInfo[nav] = { toggle, region, firstLink, lastLink };
}
});
@@ -101,12 +104,23 @@
navInfo.secondary.firstLink.addEventListener('keydown', function(e) {
if (e.shiftKey && e.key == 'Tab') {
e.preventDefault();
- handleReset();
navInfo.secondary.toggle.focus();
}
});
}
+ // When on the last link in the secondary menu, if you hit tab
+ // set focus back to the services button
+ function handleSecondaryMenuTabClick() {
+ navInfo.secondary.lastLink.addEventListener('keydown', function(e) {
+ if (e.key == 'Tab') {
+ e.preventDefault();
+ navInfo.secondary.toggle.focus();
+ }
+ });
+ }
+
+
// General function for when the ESC is clicked.
function handleEscKeyClick(buttonToFocus) {
context.addEventListener('keydown', function(e) {
@@ -143,6 +157,7 @@
if (Object.keys(navInfo).includes('secondary') && navInfo.secondary.toggle) {
navInfo.secondary.toggle.removeEventListener('click', handleSecondaryMenuToggleClick, true);
navInfo.secondary.toggle.removeEventListener('click', handleSecondaryMenuShiftTabClick, true);
+ navInfo.secondary.toggle.removeEventListener('click', handleSecondaryMenuTabClick, true);
}
if (navInfo.primary.toggle) {
navInfo.primary.toggle.addEventListener('click', handlePrimaryMenuToggleClick);
@@ -154,6 +169,7 @@
if (Object.keys(navInfo).includes('secondary') && navInfo.secondary.toggle) {
navInfo.secondary.toggle.addEventListener('click', handleSecondaryMenuToggleClick);
navInfo.secondary.toggle.addEventListener('click', handleSecondaryMenuShiftTabClick);
+ navInfo.secondary.toggle.addEventListener('keyup', handleSecondaryMenuTabClick);
}
}
}
diff --git a/js/sticky-header.js b/js/sticky-header.js
new file mode 100644
index 00000000..9ae05dba
--- /dev/null
+++ b/js/sticky-header.js
@@ -0,0 +1,65 @@
+(function stickyHeaderScript(Drupal) {
+ Drupal.behaviors.stickyHeader = {
+ attach: function (context) {
+ const headers = once('allSticyHeaders', '.lgd-header', context);
+
+ if (!headers) {
+ return;
+ }
+
+ headers.forEach(header => {
+ function calculatePositions() {
+ let tabsHeight = 0;
+ const tabs = header.closest('body').querySelector('.lgd-region--tabs');
+ if (tabs) {
+ tabsHeight = tabs.offsetHeight;
+ }
+
+ let displaceOffsetTop = 0;
+ const displaceOffsetTopValue = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--drupal-displace-offset-top').replace('px', ''));
+ if (displaceOffsetTopValue) {
+ displaceOffsetTop = displaceOffsetTopValue;
+ }
+
+ const headerHeight = header.offsetHeight;
+ const headerPosition = displaceOffsetTop + tabsHeight;
+
+ if (header.closest('body').classList.contains('sticky-header')) {
+ document.documentElement.style.setProperty('--lgd-sticky-header-position', `${headerPosition}px`);
+ document.documentElement.style.setProperty('--lgd-sticky-header-height', `${headerHeight}px`);
+ }
+
+ if (header.closest('body').classList.contains('sticky-header--sticky')) {
+ header.style.position = 'fixed';
+ }
+ }
+
+ // Initialize oldScroll, so we can use it in the scroll event.
+ let oldScroll = window.scrollY;
+
+ function handleScroll() {
+ if (oldScroll > window.scrollY) {
+ header.closest('body').classList.add('sticky-header--sticky');
+ } else {
+ header.closest('body').classList.remove('sticky-header--sticky');
+ header.style.position = 'relative';
+ }
+ // Update oldScroll to the new scroll position after the comparison
+ oldScroll = window.scrollY;
+
+ calculatePositions();
+ }
+
+ if (header.closest('body').classList.contains('sticky-header--scroll')) {
+ window.addEventListener('scroll', handleScroll);
+ }
+
+ setTimeout(() => {
+ calculatePositions();
+ }, 50);
+
+ });
+
+ }
+ };
+})(Drupal);
\ No newline at end of file
diff --git a/localgov_base.info.yml b/localgov_base.info.yml
index eca7def8..078ff519 100644
--- a/localgov_base.info.yml
+++ b/localgov_base.info.yml
@@ -36,3 +36,5 @@ regions:
libraries-extend:
localgov_eu_cookie_compliance/localgov_eu_cookie_compliance:
- localgov_base/localgov_eu_cookie_compliance
+ localgov_subsites_paragraphs/localgov_accordion:
+ - localgov_base/accordion
diff --git a/localgov_base.libraries.yml b/localgov_base.libraries.yml
index 3ce9f3be..27f388d5 100644
--- a/localgov_base.libraries.yml
+++ b/localgov_base.libraries.yml
@@ -49,15 +49,14 @@ wysiwyg-styles:
theme:
css/components/wysiwyg-styles.css: {}
-header:
- css:
- theme:
- css/components/header.css: {}
-
secondary-menu:
css:
theme:
css/components/secondary-menu.css: {}
+header:
+ css:
+ theme:
+ css/components/header.css: {}
header-js:
js:
@@ -67,6 +66,17 @@ header-js:
- core/drupal.debounce
- core/once
+sticky-header:
+ css:
+ theme:
+ css/components/sticky-header.css: {}
+ js:
+ js/sticky-header.js: {}
+ dependencies:
+ - core/drupal
+ - core/drupal.debounce
+ - core/once
+
footer:
css:
theme:
@@ -317,3 +327,18 @@ add-to-calendar:
dependencies:
- core/drupal
- core/once
+
+back-to-top:
+ css:
+ theme:
+ css/components/back-to-top.css: {}
+ js:
+ js/back-to-top.js: {}
+ dependencies:
+ - core/drupal
+ - core/once
+
+accordion:
+ css:
+ theme:
+ css/components/accordion.css: {}
diff --git a/localgov_base.theme b/localgov_base.theme
index 57ca9523..f5106ce7 100644
--- a/localgov_base.theme
+++ b/localgov_base.theme
@@ -46,6 +46,52 @@ function localgov_base_form_system_theme_settings_alter(&$form, FormStateInterfa
'#default_value' => theme_get_setting('localgov_base_add_draft_note_to_unpublished_content'),
'#description' => t('This adds the word "Draft" to the title of any unpublished content. This is useful if you have removed the pink background from unpublished content.'),
];
+ $form['localgov_base_show_back_to_top_link'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Display a "Back to Top" link on long pages.'),
+ '#default_value' => theme_get_setting('localgov_base_show_back_to_top_link'),
+ '#description' => t('This adds a link to the beginning of the page content (#main-content
).'),
+ ];
+ $form['localgov_base_header_behaviour'] = [
+ '#type' => 'radios',
+ '#title' => t('Header behaviour'),
+ '#default_value' => theme_get_setting('localgov_base_header_behaviour'),
+ '#options' => [
+ 'default' => t('Default - scrolls away with the page'),
+ 'sticky' => t('Sticky - remains at the top of the page'),
+ 'appears_on_scroll' => t('Scroll - appears when scrolling up the page'),
+ ],
+ '#description' => t('Select how you want the header to behave. This setting only apply to anonymous users.'),
+ ];
+
+}
+
+/**
+ * Implements hook_preprocess_html().
+ */
+function localgov_base_preprocess_html(&$variables) {
+ // Add the 'sticky-header' library if the sticky header setting is enabled.
+ if (theme_get_setting('localgov_base_header_behaviour') !== 'default') {
+
+ // Check if the user is logged out.
+ // We have much fewer calculations if we keep this to anonymous users only.
+ if (\Drupal::currentUser()->isAnonymous()) {
+ $variables['#attached']['library'][] = 'localgov_base/sticky-header';
+ $variables['attributes']['class'][] = 'sticky-header';
+ $variables['html_attributes']['class'] = [];
+ $variables['html_attributes']['class'][] = 'sticky-header-html';
+
+ // If 'sticky' is chosen add a class to the body element.
+ if (theme_get_setting('localgov_base_header_behaviour') === 'sticky') {
+ $variables['attributes']['class'][] = 'sticky-header--sticky';
+ }
+
+ // If 'scroll' is chosen add a class to the body element.
+ if (theme_get_setting('localgov_base_header_behaviour') === 'appears_on_scroll') {
+ $variables['attributes']['class'][] = 'sticky-header--scroll';
+ }
+ }
+ }
}
/**
@@ -94,6 +140,11 @@ function localgov_base_preprocess_page(&$variables) {
if (theme_get_setting('localgov_base_add_unpublished_background_colour') === TRUE) {
$variables['#attached']['library'][] = 'localgov_base/unpublished-bg';
}
+
+ // Render the Back to Top link or not according to the theme setting.
+ if (theme_get_setting('localgov_base_show_back_to_top_link')) {
+ $variables['back_to_top'] = TRUE;
+ }
}
/**
diff --git a/templates/_components/back-to-top.twig b/templates/_components/back-to-top.twig
new file mode 100644
index 00000000..d0b4f586
--- /dev/null
+++ b/templates/_components/back-to-top.twig
@@ -0,0 +1,51 @@
+{#
+/**
+ * Template for "back-to-top" link.
+ *
+ * The template includes two HTML elements:
+ *
+ * 1. the "back-to-top" link itself
+ * 2. a "back-to-top-target" span
+ *
+ * The javascript from localgov_base/back-to-top interacts with this template
+ * as follows:
+ *
+ * - absolutely positions the target span
+ * - creates an intersection observer to observe it
+ * - un-hides the link when the target enters the viewport
+ * - re-hides the link when the target exits the *bottom* of the viewport
+ * - re-hides the link when it's clicked [1]
+ *
+ * The template includes no wrapping