Skip to content
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
1,509 changes: 1,293 additions & 216 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"dependencies": {
"js-cookie": "^3.0.5",
"lite-youtube-embed": "^0.3.2",
"micromodal": "^0.6.1",
"swiper": "^11.2.1"
}
}
27 changes: 26 additions & 1 deletion tbx/core/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def get_button_link(self):
# Ensure page exists and is live.
if block.value and block.value.live:
return block.value.url
elif block_type == "external_link":
elif block_type == "external_link" or block_type == "modal_iframe":
return block.value
elif block_type == "email":
return f"mailto:{block.value}"
Expand Down Expand Up @@ -268,6 +268,26 @@ class Meta:
template = "patterns/molecules/streamfield/blocks/contact_call_to_action.html"


class StickyCTABlock(blocks.StructBlock):
sticky_text = blocks.CharBlock(max_length=40)
sticky_subtext = blocks.CharBlock(max_length=55)
button_link = blocks.StreamBlock(
[
("internal_link", blocks.PageChooserBlock()),
("external_link", blocks.URLBlock()),
("email", blocks.EmailBlock()),
("document_link", DocumentChooserBlock()),
("modal_iframe", blocks.URLBlock()),
],
required=True,
max_num=1,
)

class Meta:
template = "patterns/molecules/streamfield/blocks/sticky_call_to_action.html"
value_class = ButtonLinkStructValue


class DynamicHeroBlock(blocks.StructBlock):
"""
This block displays text that will be cycled through.
Expand Down Expand Up @@ -1233,6 +1253,11 @@ class StoryBlock(blocks.StreamBlock):
template="patterns/molecules/streamfield/blocks/contact_call_to_action.html",
group="Calls to action",
)
sticky_call_to_action = StickyCTABlock(
label="Sticky Call to Action",
template="patterns/molecules/streamfield/blocks/sticky_call_to_action.html",
group="Calls to action",
)
pullquote = PullQuoteBlock(
template="patterns/molecules/streamfield/blocks/pullquote_block.html",
group="Basics",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
<path d="M3.367 10.547a10 10 0 016.772-6.771l2.927-.861a10 10 0 015.644 0l2.927.86a10 10 0 016.772 6.772l.86 2.928a10 10 0 010 5.643l-.86 2.927a10 10 0 01-6.772 6.772l-2.927.861a10 10 0 01-5.644 0l-2.927-.86a10 10 0 01-6.772-6.773l-.86-2.927a10 10 0 010-5.643l.86-2.928z" fill="none" stroke="currentColor" stroke-width="4"/>
</symbol>

<symbol id="close" viewBox="0 0 32 24" fill="none">
<path d="M4 10H28" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" transform="rotate(45 16 10)"/><path d="M4 10H28" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" transform="rotate(-45 16 10)"/>
</symbol>

{# Logo: Torchbox #}
<symbol id="logo-torchbox" viewBox="0 0 830 220" fill="none">
<path d="M119.519 103.815C120.273 102.498 122.253 102.874 122.442 104.38C122.913 108.709 123.479 115.39 122.536 122.636C120.462 139.104 106.789 165.924 77.6512 173.452C76.331 173.828 75.2938 172.323 76.0481 171.099C78.6884 166.959 80.2915 161.124 80.2915 153.502C80.2915 136.093 61.4323 109.65 61.4323 109.65C61.4323 109.65 42.5731 136.093 42.5731 153.502C42.5731 161.03 44.1761 166.865 46.8164 171.005C47.665 172.229 46.6278 173.734 45.2133 173.358C15.2272 165.359 -2.21751 135.34 0.234189 105.98C1.27144 93.8401 8.06076 82.7358 12.0212 77.2778C12.8699 76.1485 14.7558 76.7132 14.8501 78.2188C15.133 84.5238 16.5474 96.0045 22.8652 105.603L23.0538 105.791C26.1655 99.204 29.466 94.4047 29.466 94.4047C32.2005 90.2641 35.4066 86.1236 38.3297 82.924C53.7 65.5148 57.0947 47.6351 47.9479 28.7202C47.2879 27.3086 48.8909 25.897 50.2111 26.744C64.1669 35.4015 103.771 65.0443 104.526 119.719V120.566C108.863 118.495 115.087 111.72 119.519 103.815Z" fill="currentColor"/><path d="M173.985 67.8179V171.335H198.352V67.8179H238.194V46.2963H134.143V67.8179H173.985Z" fill="currentColor"/><path d="M520.864 122.067V171.336H544.164V116.909C544.164 110.388 542.741 104.637 539.895 99.6566C537.05 94.6763 533.078 90.7633 527.979 87.9175C522.999 85.0717 517.248 83.6487 510.726 83.6487C504.204 83.6487 498.394 85.0717 493.295 87.9175C490.691 89.4055 488.379 91.2014 486.359 93.3054V46.2998H463.058V171.336H486.359V122.067C486.359 118.629 487.07 115.605 488.493 112.996C490.035 110.388 492.11 108.372 494.718 106.949C497.327 105.526 500.291 104.815 503.612 104.815C508.71 104.815 512.86 106.415 516.062 109.617C519.264 112.819 520.864 116.969 520.864 122.067Z" fill="currentColor"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div class="modal" id="iframe-embed-modal" aria-hidden="true">
<div class="modal__overlay" data-micromodal-close></div>
<div class="modal__container"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title">
<header class="modal__header">
<h2 class="sr-only" id="modal-title">Service Enquiry</h2>
<button type="button"
class="modal__close-button"
data-micromodal-close
aria-label="Close modal">{% include "patterns/atoms/icons/icon.html" with name="close" %}</button>
</header>
<div class="modal__content" id="filters-content">
<iframe src="{{ iframe_src }}"
width="100%"
height="650px"
frameborder="0"
title="Service enquiry form">
</iframe>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% load wagtailcore_tags %}
{# call_to_action is a required field (streamblock) in the block definition #}

<div class="call-to-action__sticky" data-sticky-cta>
<div class="call-to-action__inner">
{% if value.get_button_link_block.block_type == "modal_iframe" %}
<button class="call-to-action__stickybutton button" data-micromodal-trigger="iframe-embed-modal">
<span class="call-to-action__stickyheading">{{ value.sticky_text }}</span>
{% if value.sticky_subtext %}
<br><span class="call-to-action__stickysubtext">{{ value.sticky_subtext }}</span>
{% endif %}
</button>
{% else %}
<a href="{{ value.get_button_link }}" class="call-to-action__button button">
<span class="call-to-action__stickyheading">{{ value.sticky_text }}</span>
{% if value.get_button_link_block.block_type == "document_link" %}
({{ value.get_button_file_size|filesizeformat }})
{% endif %}
{% if value.sticky_subtext %}
<br><span class="call-to-action__stickysubtext">{{ value.sticky_subtext }}</span>
{% endif %}
</a>
{% endif %}
</div>
</div>

{% if value.get_button_link_block.block_type == "modal_iframe" %}
{% include "patterns/molecules/iframe_modal/iframe_modal.html" with iframe_src=value.get_button_link %}
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
context:
value:
sticky_text: Get in touch
sticky_subtext: learn about our journey
button_link:
- modal_iframe: 'https://example.com'
114 changes: 114 additions & 0 deletions tbx/static_src/javascript/components/modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import MicroModal from 'micromodal'; // es6 module

// Markup required
// Trigger:
// <button type="button" data-micromodal-trigger="iframe-embed-modal">Open</button>
//
// Modal:
// <div class="modal" id="iframe-embed-modal" aria-hidden="true">
// <div class="modal__overlay" data-micromodal-close></div>
// <div class="modal__container"
// role="dialog"
// aria-modal="true"
// aria-labelledby="modal-title">
// <header class="modal__header">
// <h2 id="modal-title">Service enquiry</h2>
// <button type="button" data-micromodal-close aria-label="Close dialog"></button>
// </header>
// <div class="modal__content">...</div>
// </div>
// </div>

class Modal {
static selector() {
return '[data-micromodal-trigger]';
}

constructor() {
if (typeof MicroModal !== 'undefined') {
MicroModal.init({
openTrigger: 'data-micromodal-trigger',
disableScroll: true,
});
}

Modal.bindEvents();
}

static bindEvents() {
// Listen for clicks on the document instead of using micromodel default, which doesn't work with htmx
document.body.addEventListener('click', Modal.handleEvent);
document.body.addEventListener('touchstart', Modal.handleEvent);
document.body.addEventListener('keydown', Modal.handleKeyDown);
}

static handleEvent(event) {
const trigger = event.target.closest('[data-micromodal-trigger]');
if (trigger) {
event.preventDefault();
event.stopPropagation(); // Stop the event from bubbling up

// Get the modal ID and open the correct modal
const modalId = trigger.getAttribute('data-micromodal-trigger');
if (modalId && typeof MicroModal !== 'undefined') {
MicroModal.show(modalId);
// Ensure tabbing forward from iframes stays within the modal
Modal.ensurePostIframeFocusTrap(modalId);
}
}

// Close modal when clicking on close buttons
const closeButton = event.target.closest('[data-micromodal-close]');
if (closeButton) {
const modal = closeButton.closest('.modal');
if (modal) {
MicroModal.close(modal.id); // Close the modal
}
}
}

// Prevent Enter from closing modal unless on button
static handleKeyDown(event) {
if (event.key === 'Enter') {
const modal = event.target.closest('.modal');
if (modal) {
// Allow Enter on buttons and prevent it on everything else
if (event.target.tagName !== 'BUTTON') {
event.preventDefault();
event.stopPropagation();
}
}
}
}

// When a modal contains an iframe, browser-level tabbing inside the iframe
// does not bubble key events to the parent, so focus-trap libraries
// cannot reliably intercept the Tab press. Add a focus sentinel immediately
// after the iframe that redirects focus to the Close button.
static ensurePostIframeFocusTrap(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
const container = modal.querySelector('.modal__container');
if (!container) return;

const iframe = container.querySelector('iframe');
if (!iframe) return;

// Only add once per modal instance
if (container.querySelector('.modal__focus-sentinel')) return;

const sentinel = document.createElement('span');
sentinel.tabIndex = 0;

sentinel.addEventListener('focus', () => {
const closeButton = modal.querySelector('[data-micromodal-close]');
if (closeButton) {
closeButton.focus();
}
});

iframe.parentNode.insertBefore(sentinel, iframe.nextSibling);
}
}

export default Modal;
10 changes: 10 additions & 0 deletions tbx/static_src/javascript/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CookieWarning from './components/cookie-message';
import YouTubeConsentManager from './components/youtube-embed';
import Tabs from './components/tabs';
import TableHint from './components/table-hint';
import Modal from './components/modal';
import ModeSwitcher from './components/mode-switcher';

// IE11 polyfills
Expand Down Expand Up @@ -38,6 +39,15 @@ document.addEventListener('DOMContentLoaded', () => {
initComponent(YouTubeConsentManager);
initComponent(Tabs);
initComponent(TableHint);
initComponent(Modal);
initComponent(ModeSwitcher);
new DesktopCloseMenus();

// Move sticky CTA(s) to the end of the main content for natural tab order
const main = document.getElementById('main-content') || document.body;
if (main) {
document.querySelectorAll('[data-sticky-cta]').forEach((element) => {
main.appendChild(element);
});
}
});
27 changes: 27 additions & 0 deletions tbx/static_src/sass/components/_call-to-action.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,31 @@
@include focus-style($outline-color: var(--color--background));
}
}

&__sticky {
@include z-index(sticky);
position: fixed;
bottom: $spacer-medium;
right: 0;
background-color: var(--color--theme-primary);
padding: $spacer-mini;
}

&__stickybutton {
@include font-size('size-five');
color: var(--color--white);
border: 1px solid var(--color--white);
margin-top: 0;

&:focus {
@include focus-style($outline-color: var(--color--background));
}
}

&__stickyheading {
font-weight: $weight--semibold;
}
&__stickysubtext {
font-weight: $weight--extralight;
}
}
Loading