Skip to content

Commit d937a27

Browse files
authored
Merge pull request #402 from torchbox/feature/gateway-services
[TWE-661] Add sticky call to action and new link type for all call to actions
2 parents 21f545f + 51fa248 commit d937a27

File tree

13 files changed

+1656
-219
lines changed

13 files changed

+1656
-219
lines changed

package-lock.json

Lines changed: 1293 additions & 216 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"dependencies": {
7171
"js-cookie": "^3.0.5",
7272
"lite-youtube-embed": "^0.3.2",
73+
"micromodal": "^0.6.1",
7374
"swiper": "^11.2.1"
7475
}
7576
}

tbx/core/blocks.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def get_button_link(self):
221221
# Ensure page exists and is live.
222222
if block.value and block.value.live:
223223
return block.value.url
224-
elif block_type == "external_link":
224+
elif block_type == "external_link" or block_type == "modal_iframe":
225225
return block.value
226226
elif block_type == "email":
227227
return f"mailto:{block.value}"
@@ -268,6 +268,26 @@ class Meta:
268268
template = "patterns/molecules/streamfield/blocks/contact_call_to_action.html"
269269

270270

271+
class StickyCTABlock(blocks.StructBlock):
272+
sticky_text = blocks.CharBlock(max_length=40)
273+
sticky_subtext = blocks.CharBlock(max_length=55)
274+
button_link = blocks.StreamBlock(
275+
[
276+
("internal_link", blocks.PageChooserBlock()),
277+
("external_link", blocks.URLBlock()),
278+
("email", blocks.EmailBlock()),
279+
("document_link", DocumentChooserBlock()),
280+
("modal_iframe", blocks.URLBlock()),
281+
],
282+
required=True,
283+
max_num=1,
284+
)
285+
286+
class Meta:
287+
template = "patterns/molecules/streamfield/blocks/sticky_call_to_action.html"
288+
value_class = ButtonLinkStructValue
289+
290+
271291
class DynamicHeroBlock(blocks.StructBlock):
272292
"""
273293
This block displays text that will be cycled through.
@@ -1233,6 +1253,11 @@ class StoryBlock(blocks.StreamBlock):
12331253
template="patterns/molecules/streamfield/blocks/contact_call_to_action.html",
12341254
group="Calls to action",
12351255
)
1256+
sticky_call_to_action = StickyCTABlock(
1257+
label="Sticky Call to Action",
1258+
template="patterns/molecules/streamfield/blocks/sticky_call_to_action.html",
1259+
group="Calls to action",
1260+
)
12361261
pullquote = PullQuoteBlock(
12371262
template="patterns/molecules/streamfield/blocks/pullquote_block.html",
12381263
group="Basics",

tbx/project_styleguide/templates/patterns/atoms/sprites/sprites.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
<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"/>
5656
</symbol>
5757

58+
<symbol id="close" viewBox="0 0 32 24" fill="none">
59+
<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)"/>
60+
</symbol>
61+
5862
{# Logo: Torchbox #}
5963
<symbol id="logo-torchbox" viewBox="0 0 830 220" fill="none">
6064
<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"/>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div class="modal" id="iframe-embed-modal" aria-hidden="true">
2+
<div class="modal__overlay" data-micromodal-close></div>
3+
<div class="modal__container"
4+
role="dialog"
5+
aria-modal="true"
6+
aria-labelledby="modal-title">
7+
<header class="modal__header">
8+
<h2 class="sr-only" id="modal-title">Service Enquiry</h2>
9+
<button type="button"
10+
class="modal__close-button"
11+
data-micromodal-close
12+
aria-label="Close modal">{% include "patterns/atoms/icons/icon.html" with name="close" %}</button>
13+
</header>
14+
<div class="modal__content" id="filters-content">
15+
<iframe src="{{ iframe_src }}"
16+
width="100%"
17+
height="650px"
18+
frameborder="0"
19+
title="Service enquiry form">
20+
</iframe>
21+
</div>
22+
</div>
23+
</div>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% load wagtailcore_tags %}
2+
{# call_to_action is a required field (streamblock) in the block definition #}
3+
4+
<div class="call-to-action__sticky" data-sticky-cta>
5+
<div class="call-to-action__inner">
6+
{% if value.get_button_link_block.block_type == "modal_iframe" %}
7+
<button class="call-to-action__stickybutton button" data-micromodal-trigger="iframe-embed-modal">
8+
<span class="call-to-action__stickyheading">{{ value.sticky_text }}</span>
9+
{% if value.sticky_subtext %}
10+
<br><span class="call-to-action__stickysubtext">{{ value.sticky_subtext }}</span>
11+
{% endif %}
12+
</button>
13+
{% else %}
14+
<a href="{{ value.get_button_link }}" class="call-to-action__button button">
15+
<span class="call-to-action__stickyheading">{{ value.sticky_text }}</span>
16+
{% if value.get_button_link_block.block_type == "document_link" %}
17+
({{ value.get_button_file_size|filesizeformat }})
18+
{% endif %}
19+
{% if value.sticky_subtext %}
20+
<br><span class="call-to-action__stickysubtext">{{ value.sticky_subtext }}</span>
21+
{% endif %}
22+
</a>
23+
{% endif %}
24+
</div>
25+
</div>
26+
27+
{% if value.get_button_link_block.block_type == "modal_iframe" %}
28+
{% include "patterns/molecules/iframe_modal/iframe_modal.html" with iframe_src=value.get_button_link %}
29+
{% endif %}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
context:
2+
value:
3+
sticky_text: Get in touch
4+
sticky_subtext: learn about our journey
5+
button_link:
6+
- modal_iframe: 'https://example.com'
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import MicroModal from 'micromodal'; // es6 module
2+
3+
// Markup required
4+
// Trigger:
5+
// <button type="button" data-micromodal-trigger="iframe-embed-modal">Open</button>
6+
//
7+
// Modal:
8+
// <div class="modal" id="iframe-embed-modal" aria-hidden="true">
9+
// <div class="modal__overlay" data-micromodal-close></div>
10+
// <div class="modal__container"
11+
// role="dialog"
12+
// aria-modal="true"
13+
// aria-labelledby="modal-title">
14+
// <header class="modal__header">
15+
// <h2 id="modal-title">Service enquiry</h2>
16+
// <button type="button" data-micromodal-close aria-label="Close dialog"></button>
17+
// </header>
18+
// <div class="modal__content">...</div>
19+
// </div>
20+
// </div>
21+
22+
class Modal {
23+
static selector() {
24+
return '[data-micromodal-trigger]';
25+
}
26+
27+
constructor() {
28+
if (typeof MicroModal !== 'undefined') {
29+
MicroModal.init({
30+
openTrigger: 'data-micromodal-trigger',
31+
disableScroll: true,
32+
});
33+
}
34+
35+
Modal.bindEvents();
36+
}
37+
38+
static bindEvents() {
39+
// Listen for clicks on the document instead of using micromodel default, which doesn't work with htmx
40+
document.body.addEventListener('click', Modal.handleEvent);
41+
document.body.addEventListener('touchstart', Modal.handleEvent);
42+
document.body.addEventListener('keydown', Modal.handleKeyDown);
43+
}
44+
45+
static handleEvent(event) {
46+
const trigger = event.target.closest('[data-micromodal-trigger]');
47+
if (trigger) {
48+
event.preventDefault();
49+
event.stopPropagation(); // Stop the event from bubbling up
50+
51+
// Get the modal ID and open the correct modal
52+
const modalId = trigger.getAttribute('data-micromodal-trigger');
53+
if (modalId && typeof MicroModal !== 'undefined') {
54+
MicroModal.show(modalId);
55+
// Ensure tabbing forward from iframes stays within the modal
56+
Modal.ensurePostIframeFocusTrap(modalId);
57+
}
58+
}
59+
60+
// Close modal when clicking on close buttons
61+
const closeButton = event.target.closest('[data-micromodal-close]');
62+
if (closeButton) {
63+
const modal = closeButton.closest('.modal');
64+
if (modal) {
65+
MicroModal.close(modal.id); // Close the modal
66+
}
67+
}
68+
}
69+
70+
// Prevent Enter from closing modal unless on button
71+
static handleKeyDown(event) {
72+
if (event.key === 'Enter') {
73+
const modal = event.target.closest('.modal');
74+
if (modal) {
75+
// Allow Enter on buttons and prevent it on everything else
76+
if (event.target.tagName !== 'BUTTON') {
77+
event.preventDefault();
78+
event.stopPropagation();
79+
}
80+
}
81+
}
82+
}
83+
84+
// When a modal contains an iframe, browser-level tabbing inside the iframe
85+
// does not bubble key events to the parent, so focus-trap libraries
86+
// cannot reliably intercept the Tab press. Add a focus sentinel immediately
87+
// after the iframe that redirects focus to the Close button.
88+
static ensurePostIframeFocusTrap(modalId) {
89+
const modal = document.getElementById(modalId);
90+
if (!modal) return;
91+
const container = modal.querySelector('.modal__container');
92+
if (!container) return;
93+
94+
const iframe = container.querySelector('iframe');
95+
if (!iframe) return;
96+
97+
// Only add once per modal instance
98+
if (container.querySelector('.modal__focus-sentinel')) return;
99+
100+
const sentinel = document.createElement('span');
101+
sentinel.tabIndex = 0;
102+
103+
sentinel.addEventListener('focus', () => {
104+
const closeButton = modal.querySelector('[data-micromodal-close]');
105+
if (closeButton) {
106+
closeButton.focus();
107+
}
108+
});
109+
110+
iframe.parentNode.insertBefore(sentinel, iframe.nextSibling);
111+
}
112+
}
113+
114+
export default Modal;

tbx/static_src/javascript/main.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CookieWarning from './components/cookie-message';
88
import YouTubeConsentManager from './components/youtube-embed';
99
import Tabs from './components/tabs';
1010
import TableHint from './components/table-hint';
11+
import Modal from './components/modal';
1112
import ModeSwitcher from './components/mode-switcher';
1213

1314
// IE11 polyfills
@@ -38,6 +39,15 @@ document.addEventListener('DOMContentLoaded', () => {
3839
initComponent(YouTubeConsentManager);
3940
initComponent(Tabs);
4041
initComponent(TableHint);
42+
initComponent(Modal);
4143
initComponent(ModeSwitcher);
4244
new DesktopCloseMenus();
45+
46+
// Move sticky CTA(s) to the end of the main content for natural tab order
47+
const main = document.getElementById('main-content') || document.body;
48+
if (main) {
49+
document.querySelectorAll('[data-sticky-cta]').forEach((element) => {
50+
main.appendChild(element);
51+
});
52+
}
4353
});

tbx/static_src/sass/components/_call-to-action.scss

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,31 @@
4545
@include focus-style($outline-color: var(--color--background));
4646
}
4747
}
48+
49+
&__sticky {
50+
@include z-index(sticky);
51+
position: fixed;
52+
bottom: $spacer-medium;
53+
right: 0;
54+
background-color: var(--color--theme-primary);
55+
padding: $spacer-mini;
56+
}
57+
58+
&__stickybutton {
59+
@include font-size('size-five');
60+
color: var(--color--white);
61+
border: 1px solid var(--color--white);
62+
margin-top: 0;
63+
64+
&:focus {
65+
@include focus-style($outline-color: var(--color--background));
66+
}
67+
}
68+
69+
&__stickyheading {
70+
font-weight: $weight--semibold;
71+
}
72+
&__stickysubtext {
73+
font-weight: $weight--extralight;
74+
}
4875
}

0 commit comments

Comments
 (0)